Multi-level DSL for plugin extension?


(Sebastian Peters) #1

Hey there,

I’ve written a plugin that takes parameters from my main build.gradle. The relevent parts of the plugin look like this:

  // Within apply()
  project.extensions.create("mypluginstuff", MyPluginExtension)

  // At the bottom
  class MyPluginExtension{
      String somestring
  }

And on my main script I got:

mypluginstuff {
    somestring = 'Hello'
}

This works totally fine without a hiccup. However, I would like to add similar plugins under the same namespace, like so:

    mypluginstuff {
        thatoneplugin {
            somestring = 'Hello'
        }
        thatotherplugin {
            someotherstring = 'Hi'
        }
    }

The two plugins would only be grouped together for better readability. However, when I tried to do so, it would obviously tell me that thatoneplugin() couldn’t be found. I assumed that touching the extension.create would fix it:

project.extensions.create("mypluginstuff.thatoneplugin", MyPluginExtension)

But now it tells me that mypluginstuff() couldn’t be found.

Any ideas?


(Dimitar Dimitrov) #2

The dot has no special meaning. In this case, you created an extension with name mypluginstuff.thatoneplugin, but when you type the same thing in the script, Gradle sees it as:

  1. get mypluginstuff property from project
  2. get thatoneplugin property from mypluginstuff

As mypluginstuff does not exist, step 1 fails.

If you want the nested syntax, you should either implement methodMissing on MyPluginExtension and hendle closure parameters (read about using closure delegates). Or if you know what nested ‘sub-things’ you want to support, declare normal methods taking closure as parameters.


(Chris Doré) #3

When an extension is created Gradle makes it also extensible.

project.extensions.create( 'mypluginstuff', ... )
project.extensions.mypluginstuff.extensions.create( 'thatoneplugin', ... )

http://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtensionAware.html


(Sebastian Peters) #4

That looked like a straightforward solution, which I was looking for. However, it doesn’t seem to work.

* What went wrong:
Execution failed for task ':sometask'.
> Could not get unknown property 'thatoneplugin' for root project 'NestedTestGradle' of type org.gradle.api.Project.

According to the page you linked to, you wrote one “extensions” too much on your second line, but even without that I get the same error.

As type I’ve used the same one, since that’s also what the page tells you, and I wouldn’t even know what else to use.


(Sebastian Peters) #5

That sounds mighty complex for such a seemingly simple concept. I might fall back to this, but for now I want something that is simpler to work with.


(Chris Doré) #6

Yes, the extra ‘extensions’ is not needed, I just prefer it as a matter of personal style since it disambiguates the task and extension containers, and all the other properties available from the project.

Here’s a quick test:

class Outer {
    String a
}
class Inner {
    String b
}
extensions.create( 'outer', Outer )
extensions.outer.extensions.create( 'inner', Inner )
outer {
    inner {
        b 'world'
    }
    a 'hello'
}
task ab << {
    println "${outer.a} ${outer.inner.b}"
}

With the following results:

$ ./gradlew -q ab
hello world

(Dimitar Dimitrov) #7

In that case let me spell it in simpler terms: Gradle does not support “Multi-level DSL for plugin extension” in the way you envision.

It is achievable, if you do some mumbo jumbo that you are not prepared to do.

That is not the worst though, the worst is that such a plugin would be running counter to the existing best practices and conventions, likely confusing your users and burdening the maintainers.

In summary: don’t do it. If you want your plugins to be visually grouped - use comments.


(Sebastian Peters) #8

Nice, got it work! At first it was being annoying once more, because I tried to migrate it to a plugin and forgot to add the “project” reference before “outer.a”. Thanks a ton, bro. :slight_smile:

Out of pure curiosity though, now I got something like this:

println "{$project.outer.inner.a}"

Which tends to become rather lengthy unmaintainable after multiple usages, especially if I should consider to add a third level of nested DSL. Got any idea how to make that more readable?


(Sebastian Peters) #9

What is so bad about that, though? Genuinely curious, since I’m still new.

From what I could gather so far, lots of plugins and Gradle functionality has nested functionality, which was more or less what I tried to achieve and also did, thanks to Chris’ suggestion.


(Chris Doré) #10

You can maintain references to the individual extension objects in your plugin.

def inner = project.outer.inner
println inner.a

Also, the create() method returns the created extension

def inner = project.outer.extensions.create( 'inner', ... )
println inner.a

Nested extension plugin written in Java
(Stefan Oehme) #11

There is nothing wrong with such a nested extension and @Chris_Dore’s recommendations were spot-on.

There’s a slight difference, because most plugins are self-contained and don’t add extension to other plugins like you wanted.

When you just want to have a nested object inside an extension, you can have a field and methods like the following:

class Outer {
  Inner inner = new Inner()

  //for property access syntax
  Inner getInner() {
    return inner;
  }

  // for curly-brace syntax
  void inner(Closure configuration) {
    ConfigureUtil.configure(inner, configuration)
  }

  //same as above, but better for Java/Kotlin clients
  void inner(Action<? super Inner> configuration) {
    configuration.execute(inner)
  }
}

The benefit being a more discoverable and type-safe API.


How can I extend the DSL dynamically?
(Sebastian Peters) #12

But I’m not trying to add plugins to other plugins, it’s all contained within the same plugin source code file. The only thing outside is the actual curly bracket DSL.

However, your approach—if definitely more confusing to me, sadly—seems to be maintainable too. Maybe I can get it to work too, though so far I’m perfectly happy with Chris’ suggestion.


(Stefan Oehme) #13

My approach is doing what the extension mechanism would do in a statically typed way.

The getter is needed so users can call

outer.inner.someThing = 'foo'

The closure method is for the curly brace syntax:

outer {
  inner {
    someThing = 'foo'
  }
}

If you are happy with using extensions, that’s perfectly fine :slight_smile: I just wanted to give you the alternative. We prefer that one as it makes it easier for others to discover the API.


(Sebastian Peters) #14

I was a little confused what you meant by “we prefer”. It was only then that I noticed your Core Dev tag. Now I’m definitely convinced to give it another try. :sweat_smile:


(Stefan Oehme) #15

I added some more comments to the code to explain what each piece does. As I said, this is just the preference in the Gradle code base, because it gives you an API that is easier to navigate with auto completion in the IDE for instance.


(Chris Doré) #16
// for curly-brace syntax
void inner(Closure configuration) {
    ConfigureUtil.configure(inner, configuration)
}
//same as above, but better for Java/Kotlin clients
void inner(Action<? super Inner> configuration) {
    configuration.execute(inner)
}

Are both versions of inner() required for Groovy, Java, and Kotlin support, or will just ‘void inner(Action<? super Inner> configuration)’ work for all three? I think I read somewhere that Gradle will decorate the class with the Closure variant of the method based on the Action variant, but I haven’t had a chance to actually try it out.


(Stefan Oehme) #17

We do add the Closure version for objects created by the Gradle Instantiator, e.g. when you call createExtension. That doesn’t work when you call a constructor yourself.

We should probably do it via bytecode manipulation at class loading time instead to make it always work.