Appendix: Understanding the legacy native software model

Managed Data Annotations

@Managed

Managed model interface
1 @Managed
2 interface ManagedDocker { 
3     String getDockerHost()
4     void setDockerHost(String host)
5 
6     File getCertPath()
7     void setCertPath(File path)
8 }
  1. Definition of a managed model is either an interface or an abstract class.

At this point the managed class can already be used in a build script as long as it is available on the classpath.

Model declaration in build script
1 model {
2     mysqlContainer(ManagedDocker) { 
3         dockerHost 'https://192.168.99.100:2376' 
4     }
5 }
  1. Register an instance of ManagedDocker model and call it mysqlContainer.
  2. 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.

Declaring a new managed model with an interface
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.

Using the new model
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.

Script equivalent
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:

Unmanaged extension
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).

Unmanaged class as a model
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.

Changing the name of the model
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.

Rules class with default values.
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]:

Example of using Groovy Power Assert
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

Example rules
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.

Access via the model registry
 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 }
  1. 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 the RuleSource which implements the appropriate interface.
  2. 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
  3. Now it is purely a case of querying the appropriate property from our class.