Correct way to configure gradle plugin extensions with groups of dynamic objects - groovy

I am trying to write my own gradle plugin and it needs to be able to configure a set of objects - how many of these objects and what they're called is up to the user.
The doco for creating custom gradle plugins with advanced customisability is quite poor. It mentions project.container() method to do this kind of thing, but I couldn't figure out how to make it work in my usecase.
This is an example of my plugin's configuration DSL as it stands:
teregrin {
terraformVersion = '0.6.6'
root("dev"){
accessKey = "flobble"
}
root("prd"){
}
}
And this is my plugin extension object that allows me to configure it:
class TeregrinPluginExtension {
boolean debug = false
boolean forceUnzip = false
String terraformVersion = null
Set<TeregrinRoot> roots = []
def root(String name, Closure c){
def newRoot = new TeregrinRoot(name)
c.setDelegate(newRoot)
c()
roots << newRoot
}
}
The extensions wired up in my plugin in the standard way:
project.extensions.create("teregrin", TeregrinPluginExtension)
This works ok, but it's a pretty ugly configuration style, not really in the style of the typical gradle DSL.
How can I change my plugin configuration DSL to be something like this:
teregrin {
terraformVersion = '0.6.6'
roots {
dev {
accessKey = "flobble"
}
prd {
}
}
}

The gradle way of implementing such DSL is by using extensions and containers:
apply plugin: SamplePlugin
whatever {
whateverVersion = '0.6.6'
conf {
dev {}
qa {}
prod {
accessKey = 'prod'
}
}
}
task printWhatever << {
println whatever.whateverVersion
whatever.conf.each { c ->
println "$c.name -> $c.accessKey"
}
}
class SamplePlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('whatever', SampleWhatever)
project.whatever.extensions.conf = project.container(SampleConf)
project.whatever.conf.all {
accessKey = 'dev'
}
}
}
class SampleWhatever {
String whateverVersion
}
class SampleConf {
final String name
String accessKey
SampleConf(String name) {
this.name = name
}
}
while groovy way of doing implementing such DSL is meta programming - you need to implement methodMissing in this particular case. Below is a very simple example that demonstrates how it works:
class SomeExtension {
def devConf = new SomeExtensionConf()
void methodMissing(String name, args) {
if ('dev'.equals(name)) {
def c = args[0]
c.resolveStrategy = Closure.DELEGATE_FIRST
c.delegate = devConf
c()
} else {
throw new MissingMethodException("Could not find $name method")
}
}
def getDev() {
devConf
}
}
class SomeExtensionConf {
def accessKey
}
project.extensions.create('some', SomeExtension)
some {
dev {
accessKey = 'lol'
}
}
assert 'lol'.equals(some.dev.accessKey)
Of course it has no error checking - so the args size and type of each argument need to be validated - it's omitted for the sake of brevity.
Of course there's no need to create a separate class for each configuration (I mean dev, prod, etc.). Create a single class that holds configuration and store them all in a Map where key is configuration name.
You can find a demo here.

Related

Gradle: How to map Lists from configuration-closure to extension settings

I have this (working) code, which is extended from the gradle documentation
https://docs.gradle.org/4.10.3/userguide/custom_plugins.html#sec:implementing_a_dsl
class User {
String name
}
class Group {
User user
}
class GreetingPluginExtension {
String message
final Group group
#javax.inject.Inject
GreetingPluginExtension(ObjectFactory objectFactory) {
group = objectFactory.newInstance(Group)
group.user = objectFactory.newInstance(User)
}
void group(Action<? super Group> action) {
action.execute(group)
}
void user(Action<? super User> action) {
action.execute(group.user)
}
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Create the extension, passing in an ObjectFactory for it to use
def extension = project.extensions.create('greeting', GreetingPluginExtension, project.objects)
project.task('hello') {
doLast {
println "${extension.message} from ${extension.group.user.name}"
}
}
}
}
The configuration closure looks like this:
greeting {
message = 'Hello'
group {
user {
name = 'tom'
}
}
}
But I would like to have a List of users an I tried:
class User {
String name
}
class Group {
ArrayList<User> users
}
class GreetingPluginExtension {
String message
final Group group
#javax.inject.Inject
GreetingPluginExtension(ObjectFactory objectFactory) {
// Create a Person instance
group = objectFactory.newInstance(Group)
group.users = []
}
void group(Action<? super Group> action) {
action.execute(group)
}
void users(Action<? super ArrayList<User>> action) {
action.execute(group.users)
}
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Create the extension, passing in an ObjectFactory for it to use
def extension = project.extensions.create('greeting', GreetingPluginExtension, project.objects)
project.task('hello') {
doLast {
extension.group.users.each {
println "${extension.message} from ${it.name}"
}
}
}
}
}
with this closure:
greeting {
message = 'Hello'
group {
users = [
{name = 'tom'} ,
{name = 'tim'}
]
}
}
my output is the following:
Hello from myProjectName
Hello from myProjectName
which is not the expected output but the rootProject.name
The output has the right number of elements, but is not referenced to the user.
How can I fix this? I would also appreciate informations about other approaches to map nested objects (and lists) into the extension-setting.
Greetings Tom
The following slightly modified version I believe does what you intended:
apply plugin: GreetingPlugin
greeting {
message = 'Hello'
group {
user(name: 'tom')
user(name: 'tim')
}
}
class User {
String name
}
class Group {
ArrayList<User> users = []
def user(props) {
users << new User(props)
}
}
class GreetingPluginExtension {
String message
final Group group
#javax.inject.Inject
GreetingPluginExtension(ObjectFactory objectFactory) {
// Create a Person instance
group = objectFactory.newInstance(Group)
}
void group(Action<? super Group> action) {
action.execute(group)
}
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Create the extension, passing in an ObjectFactory for it to use
def extension = project.extensions.create('greeting', GreetingPluginExtension, project.objects)
project.task('hello') {
doLast {
extension.group.users.each {
println "${extension.message} from ${it.name}"
}
}
}
}
}
When run, it prints:
~> gradle hello
> Task :hello
Hello from tom
Hello from tim
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Note that I have removed the method:
void users(Action<? super ArrayList<User>> action)
as it is not needed for the above to work. I also changed the dsl somewhat. I actually think the changed dsl looks more readable and idiomatic but this is of course a matter of taste.
Note also that if you want to send in more properties to the user (say email), you can do this without modifying the scaffolding code, i.e.:
greeting {
message = 'Hello'
group {
user(name: 'tom', email: 'tom#wonderland.org')
user(name: 'tim', email: 'tim#wonderland.org')
}
}
class User {
String name
String email
}
If you were specifically looking for how you make a doubly nested configuration closure for a collection work, this does not solve your problem, but it does give you a reasonably clean way of accomplishing what the build script intended.

Get variables from delegate scope in Groovy

I'm trying to write a Jenkins Job DSL script and would like to write it as declaratively / DRY-ly as possible. The Jenkins task is calling some other tasks via a MultiJob. I have Groovy that originally looks like this (everything's contained within a class because it's referenced elsewhere):
static void buildDownstream(def parentJob, String commit_a="master",
String commit_b="master") {
parentJob.with {
steps {
phase('Phase') {
job("name_1") {
prop('COMMIT_A', commit_a)
nodeLabel('NODE_LABEL', NODE_LABEL_MAP["name_1"])
killPhaseCondition('NEVER')
}
job("name_2") {
prop('COMMIT_A', commit_a)
prop('COMMIT_B', commit_b)
nodeLabel('NODE_LABEL', NODE_LABEL_MAP["name_2"])
killPhaseCondition('NEVER')
}
job("name_3") {
prop('COMMIT_A', commit_a)
prop('COMMIT_B', commit_b)
nodeLabel('NODE_LABEL', NODE_LABEL_MAP["name_3"])
killPhaseCondition('NEVER')
}
}
}
}
}
I'd like to abstract out the job creation, which contains lots of duplication. I've ended up with something strange like this:
static void buildDownstream(def parentJob, String commit_a="master",
String commit_b="master") {
parentJob.with {
steps {
phase('Phase') {
def phase = ({ owner })();
{ ->
add_node_label=true;
{ ->
commit_a = null;
def self = ({ owner })();
addJob("name_1", self).call(phase);
}
def self = ({ owner })();
addJob("name_2", self).call(phase);
addJob("name_3", self).call(phase);
}
}
}
}
}
private static Closure addJob(String job_name, Closure callingClosure) {
return { phase ->
def job_config = {
if(commit_a != null) {
prop('COMMIT_A', commit_a)
}
if(commit_b != null) {
prop('COMMIT_B', commit_b)
}
if(add_node_label == true) {
nodeLabel('NODE_LABEL', NODE_LABEL_MAP[job_name])
}
killPhaseCondition('NEVER')
}
job_config.delegate = callingClosure
job_config.resolveStrategy = Closure.DELEGATE_ONLY
phase.job(job_name, job_config)
}
}
which, probably being totally non-idiomatic Groovy (all this def self = ({ owner })() stuff doesn't sit right with me), doesn't work at all.
Basically, I want to pass all the variables in callingClosure's scope to the job_config closure without explicitly passing all of them in as arguments. (Explicitly passing a map of arguments works, but it gets unwieldy when there are lots of arguments.) How can I do this?
(P.S: Currently, Groovy is trying to resolve the commit_a variable inside job_config as coming from javaposse.jobdsl.dsl.helpers.step.PhaseContext, which I find strange; didn't I explicitly set the delegate to a closure inside that PhaseContext?)
EDIT: From another SO question, it appears that I can set phase = delegate (which defaults to owner?) instead of ({ owner })() and be fine; I don't really get this either, since job is a property of the PhaseContext, and not its parent (?)
Well, I ended up not trying to ask Groovy to implicitly resolve variables from the delegate context, and instead just passed in the parameters in a map.
static void buildDownstream(def parentJob,
String commit_a="master", String commit_b="master") {
parentJob.with {
steps {
phase('Tests') {
def params = [COMMIT_A:commit_a]
this.getTestJob(delegate, "name_1", params)
params.COMMIT_B = commit_b
this.getTestJob(delegate, "name_2", params)
this.getTestJob(delegate, "name_3", params)
continuationCondition('ALWAYS')
}
}
}
}
private static void getTestJob(def phase, String job_name,
Map properties) {
phase.job(job_name) {
properties.each { k, v -> prop(k, v) }
killPhaseCondition('NEVER')
}
}
One problem with my original method was that I was trying to access local variables in the closures, but that requires the closure to be evaluated; that turns out to be really weird, and I guess I should just not try to do that.

Groovy runtime method interception

I'm playing with Groovy and I wonder, why doesn't this piece of code works?
package test
interface A {
void myMethod()
}
class B implements A {
void myMethod() {
println "No catch"
}
}
B.metaClass.myMethod = {
println "Catch!"
}
(new B()).myMethod()
It prints out No catch, while I expect it to print Catch! instead.
It's a bug in Groovy, there is an open issue in JIRA: Cannot override methods via metaclass that are part of an interface implementation, GROOVY-3493.
Instead of rewriting B.metaClass.myMethod, try following:
B.metaClass.invokeMethod = {String methodName, args ->
println "Catch!"
}
This blog post describes it quite well.
There is a workaround but it only applies to all classes and not specific instances.
metaclass modification BEFORE construction:
interface I {
def doIt()
}
class T implements I {
def doIt() { true }
}
I.metaClass.doIt = { -> false }
T t = new T()
assert !t.doIt()
metaclass modification AFTER construction:
interface I {
def doIt()
}
class T implements I {
def doIt() { true }
}
T t = new T()
// Removing either of the following two lines breaks this
I.metaClass.doIt = { -> false }
t.metaClass.doIt = { -> false }
assert !t.doIt()

Use Groovy Category implicitly in all instance methods of class

I have simple Groovy category class which adds method to String instances:
final class SampleCategory {
static String withBraces(String self) {
"($self)"
}
}
I want to use this category in my unit tests (for example). It looks like this:
class MyTest {
#Test
void shouldDoThis() {
use (SampleCategory) {
assert 'this'.withBraces() == '(this)'
}
}
#Test
void shouldDoThat() {
use (SampleCategory) {
assert 'that'.withBraces() == '(that)'
}
}
}
What I'd like to achieve, however, is ability to specify that category SampleCategory is used in scope of each and every instance method of MyTest so I don't have to specify use(SampleCategory) { ... } in every method.
Is it possible?
You can use mixin to apply the category directly to String's metaClass. Assign null to the metaClass to reset it to groovy defaults. For example:
#Before void setUp() {
String.mixin(SampleCategory)
}
#After void tearDown() {
String.metaClass = null
}
#Test
void shouldDoThat() {
assert 'that'.withBraces() == '(that)'
}
Now you have the option to use extension modules instead of categories:
http://mrhaki.blogspot.se/2013/01/groovy-goodness-adding-extra-methods.html
On the plus side Intellij will recognize the extensions. I've just noticed that it doesn't even need to be a separate module as suggested by the link, just add META-INF/services/org.codehaus.groovy.runtime.ExtensionModule to the project:
# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = module
moduleVersion = 1.0
extensionClasses = SampleExtension
The extension class is pretty much defined like a normal category:
class SampleExtension {
static String withBraces(String self) {
"($self)"
}
}
Can be used like:
def "Sample extension"() {
expect: 'this'.withBraces() == '(this)'
}
If you are using Spock there is a #Use annotation that can be used on the specifications. The drawback with that is that Intellij will not recognize it.

Groovy DSL: handling labels

I'm implementing in Groovy a DSL for some existing file format.
In this format we have a construct like
group basic_test {
test vplan_testing {
dir: global_storage;
};
};
And here I have problem with this dir: global_storage - groovy considers "dir:" as a label, so I can't handle it.
Do you have an idea how I can receive some callback (getProperty, invokeMissingMethod) for this construct?
Thank you!
I don't believe you can achieve that this way, you need to change your dsl a bit to be able to capture that information. Here's how you could achieve that:
class Foo {
static plan = {
vplan_testing {
dir 'global_storage'
}
}
}
def closure = Foo.plan
closure.delegate = this
closure()
def methodMissing(String name, Object args) {
println "$name $args"
if(args[0] instanceof Closure)
args[0].call()
}
The output will be
dir [global_storage]
or you could defined you dsl this way:
class Foo {
static plan = {
vplan_testing {
test dir:'global_storage'
}
}
}
replace "test" by something meaningful to you domain. In this case the output would be
test [[dir:global_storage]]
Hope this helps
-ken

Resources