Appendix: Understanding the legacy native software model
Managed Data Annotations
@Managed
1 @Managed
2 interface ManagedDocker {
3 String getDockerHost()
4 void setDockerHost(String host)
5
6 File getCertPath()
7 void setCertPath(File path)
8 }
- Definition of a managed model is either an
interfaceor anabstract class.
At this point the managed class can already be used in a build script as long as it is available on the classpath.
1 model {
2 mysqlContainer(ManagedDocker) {
3 dockerHost 'https://192.168.99.100:2376'
4 }
5 }
- Register an instance of
ManagedDockermodel and call itmysqlContainer. - Configure the settings on the new model. Any property or property on the managed class can be configured here.
This is effectively the equivalent of what a @Model annotation will create. By keeping a mental picture of this
equivalence in mind, it will be easier for many of us plugins authors to transition from the classic Gradle way of
thinking to this new style.
RuleSource Annotations
These are special annotations that are applied to methods in a class that extends RuleSource.
@Model
When applied to a method it indicates that a new top level element is created in the model space.
This element takes the name of the method unlike the annotation is given a value. In the latter case the
provided name must start with a lowercase character and only consist of ASCII word characters (Regex \w).
The element also has to provide a type, but the way of providing the type depends on whether this is a [Managed] type or not.
1 * ***Managed***: The return type is always void. The first parameter is a type defin\
2 ed somewhere else and annotated
with @Managed. The first parameter is not allowed to be unmanaged or annotated with @Path. (See [Managed] for
more details.
Any additional parameters are considered inputs.
* Unmanaged: The return type is the model type. All parameters are considered inputs.
1 class ExampleRules extends RuleSource {
2
3 @Model
4 void docker(ManagedDocker md) {}
5
6 }
1 1. All model rule definitions must start by extending `RuleSource`.
2 1. Minimum declaration required to create a new managed model. Provided typed must b\
3 e annotated with `@Managed`.
Initialisation can be performed in the code block.
When building the model rules for a managed type, the first parameter must always be the type and the return type is
void. (This pattern is common across RuleSource rule annotation, but it is worth repeating here). The name of the
method becomes the name of the model in the model registry.
1 apply plugin : ExampleRules
1 1. The model rules class must be applied as a plugin. In many cases this will happen\
2 within the plugin class, but there
are other ways in which the rules can be automatically applied.
Reinforcing this equivalence of code and script, we should visualise this code as simply being.
1 model {
2 docker(ManagedDocker)
3 }
It is also to create a model without using a managed type. Consider that we rather wanted to implement the data class for out Docker descriptor ourselves. A simplified version of it might have looked something like this:
1 class NonManagedDocker {
2 String dockerHost = 'http://192.168.1.1:1234'
3 File certPath
4 }
In order to use this as a model, the method in our rules needs to return an instance of this class (as opposed to void
in the managed case).
1 @Model
2 NonManagedDocker dockerNonManaged() {
3 new NonManagedDocker()
4 }
1 1. The name of the method will still become the name of the toplevel model in the bu\
2 ild script.
3 1. Initialise & return the object as appropriate.
The @Model annotation also allows for setting a custom name that might be more suitable for use within a DSL
than the method name. Simply use value=NewName as parameter to the annotation.
1 @Model(value='DrDocker')
2 void docker(ManagedDocker md) {}
1 1. The toplevel model will be known as `DrDocker` instead of `docker`.
@Defaults
Use the @Defaults annotation to set default property values for a subject. The name of the method is irrelevant as far
as the build script author is concerned and should rather be named to describe the intent of setting the default value.
The first parameter of the method is always the subject, which will be mutable for the duration of the method call. The
return type is always void.
1 class DefaultExampleRules extends RuleSource {
2 @Model
3 void docker(ManagedDocker md) {}
4
5 @Defaults
6 void defaultServer(ManagedDocker md) {
7 md.dockerHost = 'https://192.168.99.100:2376'
8 }
9 }
@Mutate
These are some of the most powerful kind of rules as they are not only used to set values on model subjects, but also
to create tasks and other entities. The first parameter of a mutate method is always the subject, which will be mutable
for the duration of the method call. Additional parameters can be supplied which can be used as inputs. The return type
is always void.
@Finalize
According to gradle core developer, Mark Viera, there is very little difference between @Finalize and @Mutate rules. They are
effectively just two groups of the same things, with all @Mutate rules being executed, before any @Finalize rules[MViera1].
Anything else that has been mentioned previously for @Mutate will apply equally here. From a idiomatic point of view,
it is recommended that @Finalize rules are only used for final configuration in a similar fashion that
project.afterEvaluate has been used in the classic model.
@Validate
Validation rules are used to check the integrity of properties before execution can begin. The first parameter of the method is always the subject, which will be immutable for the duration of the method call. It is the responsibility of the plugin author to terminate execution upon validation failure by throwing an appropriate exception. Groovy power asserts can also be used as shown below [MViera2]:
1 @Validate
2 void alwaysUseHttp(ManagedDocker md) {
3 assert md.dockerHost?.startsWith('http')
4 }
1 1. Check that the condition are met and raise exception if not
Inspecting the Model
Script authors should not need to do this, but as plugin author at some stage will probably have th need to look under the
hood. This is especially the case when attempting to understand how the new model work the first time or even for some
low-level unit tests. For this purpose use a method on the internal Project class namely modelRegistry. This is best
described by means of a little Spock Framework test.
Revisiting our DefaultExampleRules class from earlier
1 class DefaultExampleRules extends RuleSource {
2 @Model
3 void docker(ManagedDocker md) {}
4
5 @Defaults
6 void defaultServer(ManagedDocker md) {
7 md.dockerHost = 'https://192.168.99.100:2376'
8 }
9 }
we can author a test to check that the dockerHost property has a default value set.
1 class DefaultExampleRulesSpec extends Specification {
2 def project = ProjectBuilder.builder().build()
3
4 def "Default rule must set dockerHost"() {
5 given: "A simple model"
6 project.allprojects {
7 apply plugin : DefaultExampleRules
8 }
9
10 def node = project.modelRegistry.find('docker',ManagedDocker)
11
12 expect: "dockerHost to have default value"
13 node?.dockerHost == 'https://192.168.99.100:2376'
14 }
15
16 def "Configuration overrides Default rules"() {
17 given: "A simple model"
18 project.allprojects {
19 apply plugin : DefaultExampleRules
20
21 model {
22 docker {
23 dockerHost 'https://192.168.99.100:1234'
24 }
25 }
26 }
27
28 when:
29 def node = project.modelRegistry.find('docker',ManagedDocker)
30
31 then:
32 node?.dockerHost == 'https://192.168.99.100:1234'
33 }
34
35 }
- Apply the rules class using
apply plugin:syntax. This might seem strange as one would not expect a rules class to be a plugin class, however since a rules class has to extend theRuleSourcewhich implements the appropriate interface. - Sneakily using
modelRegistry, we pass the model name as a string, plus the subject type to registry’s find method. If it is exists it will return the instantiated component, otherwise it will be a null value - Now it is purely a case of querying the appropriate property from our class.