Advanced Techniques
Delegation Hints for Builder Closures
Generated DSL methods that accept configuration closures automatically receive the appropriate @DelegatesTo metadata,
so modern IDEs can infer the available Builder methods.
Schema-defined Mutators can also accept and forward configuration closures. Because the generated Builder type does not
exist when that source method is parsed, use @DelegatesToBuilder for those parameters. It tells the IDE and static type
checker about the generated Builder; it does not make the completed DSL Object mutable.
The optional annotation value names the DSL Object whose Builder receives the closure:
@DSL
class Container {
List<Element> elements
@Mutator
def circle(@DelegatesToBuilder(Element) Closure body) {
element(type: 'circle', body)
}
@Mutator
def square(@DelegatesToBuilder(Element) Closure body) {
element(type: 'square', body)
}
}
Here both Mutators execute on the Container Builder and delegate body to a newly created Element Builder.
@DelegatesToBuilder does not add a completed-model apply or configure path. An API that configures a DSL Object must
participate in factory/Builder construction; see Builder First Migration for the lifecycle boundary.
Behavior Models and Parameter Hints
Fields can hold behavior as a closure or interface value. This lets a Model choose an algorithm without creating a new Schema type.
Consider the following example:
class ValueProvider {
String name
String getDescription(Map<String, String> environment) {
"Value: $name: ${environment.name} -> ${environment.value}"
}
}
If different Models need different descriptions, this design could require a subclass of ValueProvider for each
algorithm. That is inconvenient when the behavior varies by Model rather than by Schema.
Make the description algorithm configurable instead (the Strategy pattern). Use either an interface or abstract class, or a closure.
Interface
interface DescriptionProvider {
String getDescription(Map<String, String> environment)
}
@DSL class ValueProvider {
String name
@Required
DescriptionProvider descriptionProvider
String getDescription(Map<String, String> environment) {
descriptionProvider.getDescription(environment)
}
}
The Model can now supply the description algorithm. In Groovy, a closure can implement a single-abstract-method (SAM) interface:
ValueProvider.Create.With {
name "Blub"
descriptionProvider { "Value: $name: $it.name -> $it.value" }
}
The closure has one Map parameter and returns String, so the compiler can check both parts of the contract.
DescriptionProvider could instead be an abstract class, for example to add Converters#factory-method-converters.
Closure Attributes
The description provider could also be a Closure itself:
@DSL class ValueProvider {
String name
Closure<String> descriptionProvider
String getDescription(Map<String, String> environment) {
descriptionProvider.getDescription(environment)
}
}
ValueProvider.Create.With {
name "Blub"
descriptionProvider { "Value: $name: $it.name -> $it.value" }
}
The Model call looks the same as the SAM-interface form. With an unannotated Closure field, however, the IDE and type
checker do not know the parameter type. Closure<String> describes the return type, not the parameter type, so they
cannot offer parameter completion.
In normal Groovy, a method parameter can carry @ClosureParams. Because KlumAST generates this setter, use
@ParameterAnnotation.ClosureHint on the field instead. The hint supplies the required parameter annotation:
@DSL class ValueProvider {
String name
@ParameterAnnotation.ClosureHint(params = @ClosureParams(value = FromString, options = "Map<String,Object>"))
Closure<String> descriptionProvider
String getDescription(Map<String, String> environment) {
descriptionProvider.getDescription(environment)
}
}
This provides parameter completion and type checking for the generated methods. @ParameterAnnotation copies annotations
from a Schema field to the generated setter or single-element adder; see the
@ParameterAnnotation API source and Javadoc
for the advanced annotation-mapping rules.
Choosing a SAM Interface or Closure
Prefer a SAM interface when only parameter typing is needed, especially with factory converters.
Use a Closure with parameter annotations when its Groovy delegate mechanism makes the DSL materially clearer; a SAM interface cannot reproduce that delegate behavior.
In short: use a SAM interface for ordinary typed behavior and a Closure with @ParameterAnnotation when delegation is
part of the DSL.