Extension objects in new model

software-model

(Schalk Cronjé) #1

Whilst trying to understand how to implemenent a new toolchain and browsing the Gradle source, I noticed that there a number of cases where extension objects are created and then added into the model. For instance (shown as simplified code):

class ToolChain {
}

class MyPluginsRules extends RuleSource {
  @Model
  ToolChain toolChains(ExtensionContainer ext) {
   ext.getByType(ToolChain) 
 }
}

class MyPlugin implements Plugin<Project> {
 
  void apply(Project project) {
    project.extensions.create 'toolChains',ToolChain
    project.apply plugin : MyPluginRules
  }
} 

Tthis means that is now possible to do both

toolChains {
  // ...
}

model {
  toolChains {
     // ...
  }
}

and both will configure the same object.

  1. I am wondering whether this is a left-over from the old model, of whther this was intended by design?
  2. Is this something that we should advocate for people writing plugins, or should they rather just create a model element?

(Mark Vieira) #2

This is a way of bridging the gap between old and new model. The primary example here is the publishing plugins, which are configured via a legacy project extension but whose tasks are created via model rules.

For new development, no. This is mainly a way to avoid breaking changes for folks that are used to configuring an existing project extension. It also allows for some interoperability between legacy plugins and rules plugins. In general you should be using model elements for this stuff, and preferably, managed models.


(Schalk Cronjé) #3

Thank you. That answers it perfectly.


(Schalk Cronjé) #4

Thinking about it now, it would be an easy migration path for someone to simply migrate their extension into a model by doing:

class MyPluginsRules extends RuleSource {
  @Model
  ToolChain toolChains() {
   new ToolChain() 
 }
}

Granted it is not @Managed, but it is a quick win.


(Mark Vieira) #5

There are some distinct differences here which are important. Most notably the use of new here means there is no decoration done to the ToolChain as would be the case with extensions.create('toolChains', ToolChain.class). Features not available include:

  1. Setter methods. Meaning in the DSL you can no longer do prop 'value'. You must use the assignment (=) operator.
  2. The model element is not itself extensible. Meaning you cannot add properties to it via ext and you can not add extension objects to it.
  3. If you have any reliance on services via @Inject this will no longer function.
  4. The GroovyObject class is no longer mixed in.

For simple extension types this is likely not a problem. Just keep in mind that it’s not necessarily a 1:1 migration path.


(Schalk Cronjé) #6

Which is fine for simple assignments, but what worries me about that is when one has a list of things i.e a List<String> and would want to append to the list. It is no longer possible to do

listProp 'value1', 'value2'
listProp 'value3', 'value4'

and expect listProp to contain ['value1,'value2','value3','value4'].


(Mark Vieira) #7

You can use the << operator to achieve this. You can also still call add() or addAll() as well.

model {
  myModelElement {
    listProp << 'value1'
    listProp.addAll 'value2', 'value3'
  }
}

(Mark Vieira) #8

By the way, setter methods are never added for collection types. The pattern you describe above is typically explicitly implemented by the extension.

List<String> listProp = new ArrayList<>()

public void listProp(String... s) {
  listProp.addAll(s)
}

(Schalk Cronjé) #9

It’s ugly.

I riding my hobby horse now. :sunglasses: I like a DSL to be as semantically clear as possible with as little possible punctuation and unnecessary words distracting from the intent.

True for the classic way of doing that, but I don’t see a way of doing that for a managed type.
(It is very possible that someone might go down the same way in migrating their code as what has been discussed in this thread, so at some point they will think that they need to convert their extension to a managed type).


(Mark Vieira) #10

This is correct. Adding some DSL decoration to managed types is still something we want to do. The model DSL in general is still very much a work in progress. The pattern above can still just as well be implemented by just marking that property as @Unmanaged.


(Schalk Cronjé) #11

Hoho. I just tested the following with Gradle 2.10 - 2.13:

@Managed
interface ExternalTool {
    String getExecutable()
    void setExecutable(String exe)
}

class ExternalToolRules extends RuleSource {

    @Model(value='externalTool')
    void tool(ExternalTool exe) {}

    @Defaults
    void toolInit(ExternalTool exe) {
        exe.executable = 'gmake'
    }
}

and it seems the decorator is there, as the following works without assignment.

model {
  externalTool {
    executable 'make'
  }
}

(Mark Vieira) #12

Correct, we do generate these methods for managed types. I was previously referring to your example of reusing an existing extension type as an unmanaged model element.


(Schalk Cronjé) #13

Right, I’m with you.

The consequence of this thread is that I realised that it would be useful for people know how to migrate an existing extension to the new model. I have managed to code up and write down the outlines of recipes that demonstrate three stages of migration, the final being a managed version. They will eventually also discuss the caveats of each stage. :wave:

The only place where I came unstack is to see if it is possible to create some hybrid (fourth) solution using @Unmanaged as you mentioned. First things is that one cannot simply stick an @Unmanaged annotation on and think that it will work. Adding the following to the ExternalTool interface from a previous posting

@Unmanaged
List<String> getExecArgs()
void setExecArgs(List<String> args)

will result in something like Invalid managed model type b.n.e.hybrid.ExternalTool: property 'execArgs' is marked as @Unmanaged, but is of @Managed type 'java.util.List<java.lang.String>'. Please remove the @Managed annotation. The very obvious solution is to use a tyoe which is clearly unmanaged and not any of the manageablle ones listed in the docs. In then needs to be accessed via a dot.

class Targets {
  void targets(String... args) { /* ... */ }
}

@Managed
interface ExternalTool {
    String getExecutable()
    void setExecutable(String exe)

    @Unmanaged
    Targets getExtras()
    void setExtras(Targets targets)
}


class ExternalToolRules extends RuleSource {

    @Model(value='externalTool')
    void tool(ExternalTool exe) {}

    @Defaults
    void toolInit(ExternalTool exe) {
        exe.executable = 'gmake'
        exe.targets = new Targets()
    }
}

model {
  externalTool {
    extras.targets '1','2','3'
  }
}

From a DSL PoV this is reasonable compromise. What is not possible is to delegate to a closure. Adding

class Target {
  def call(Closure c) { /* ... */ }
}

will not result in the following being valid

model {
  externalTool {
    extras {
      targets '1','2','3'
    }
  }
}

It is the consequence of Groovy actually doing the right thing in trying to find setExecArgs(Closure) and then ends up reposrting a missing method.

(In the longer term, a useful solution would be from the Gradle team to add decorators for LIst as previsouly mentioned)