This spec outlines some initial stories for what's generally termed “managed model”. This is a sub stream of the “unified-configuration-model” stream.
There are several key drivers for this stream of work:
- Model insight
- Bidirectional model externalisation
- Model caching
The term “model insight” (#1) refers to deeply understanding the structure of model elements. While not a goal with immediate user benefit in itself, it is included in this list as a proxy for general usability enhancements made possible by this. For example, a model browsing tool would rely on this insight to generate visual representation of elements and facilitate browsing a model (schema and data).
The term “bidirectional model externalisation” refers to being able to serialize a data set to some “external” form (e.g. JSON, YAML, XML, Turtle etc.) for consumption by other systems, and the possibility of using such a format to construct a data set for Gradle to use.
The term “model caching” refers to the ability to safely reuse a previously “built” model element, avoiding the need to execute the user code that contributed to its final state.
Moreover, we consider owning the implementation of model elements an enabler for the general dependency based configuration model.
- Scalar type: a type that represents a single immutable value. Supported types:
- all primitive types
- all subtypes of Number in java.lang and java.math
- Boolean
- Character
- String
- File
- All subtypes of Enum
- Managed property: a property of a model element, whose implementation and state is managed by Gradle. Generally only available for
@Managed
types, but there may also be internal mechanisms to define such properties on other types. - Scalar property: a property of a model element whose type is a scalar type.
- Reference property: a property of a model element whose value references another model element.
- Add support for all primitive types.
- Add support for missing boxed types (Byte, Short, Float).
- Update user guide and Javadocs
- Can define RW properties of any scalar type
- Cannot have read only properties of scalar types.
- Fail type validation when getter uses primitive type and setter uses boxed type (and vice versa).
- Cannot mutate properties of scalar types when view is immutable (eg used as input for rule, used as subject for validation rule).
- Model report renders primitive values
- Update
PrimitiveStrategy
to support an extraction result for primitive types - Add support for missing boxed types to
ManagedProxyClassGenerator
- Add support for primitive types to
ManagedProxyClassGenerator
. Handle case where state returns null by setting a default value. - Make sure
org.gradle.api.reporting.model.internal.ModelNodeRenderer.maybePrintValue
handles primitive types in a human readable form
ModelElementState
only supports boxed types, usingget
andset
. If the model is often updated, it could lead to performance issues, in which case it could be necessary to introduce primitive versions ofget
andset
.
- Should follow the JavaBeans specification: only type
boolean
should allowis
getter style:Boolean
shouldn't be supported. - Update user guide and Javadoc
- Support
is
style accessor for properties with typeboolean
- Support type with both
is
andget
accessors for property with typeboolean
- Prohibit
is
style accessors for properties of any type other thanboolean
(including Boolean)
Add support managed properties of List<T>
and Set<T>
where T
is any non primitive scalar type (as defined above).
- Any returned collection instance will be mutable when view is mutable (eg used as subject for rule).
- Any returned collection instance will be immutable when view is immutable (eg used as input for rule, used as subject for validation rule).
- Properties of type
Set
will retain insertion order
Support read-only collection properties defined as:
@Managed
interface ReadOnlyProperty {
List<String> getItems()
}
- Default value is an empty collection
- Multiple calls to a getter may not return the same instance of a collection
Support read-write collection properties defined using both a getter and a setter:
@Managed
interface ReadWriteProperty {
List<String> getItems()
void setItems(List<String> items)
}
- Defaults to a null value
- Can set to a null value
- Multiple calls to a getter may not return the same instance of a collection
- Collection returned by getter may not be the same instance as provided to the setter
- When a setter is called, a new managed collection is created, ensuring immutability. Documentation should mention a similarity with the defensive copy pattern.
- The collection property will only be writable when the view is mutable
- Update user guide and Javadocs, add sample
- Make sure
org.gradle.api.reporting.model.internal.ModelNodeRenderer.maybePrintValue
handles collection types in a human readable form (aka, nottoString()
)
-
calling the getter of a read-only property for a created node must return an empty collection
-
calling the getter of a read-write property for a created node must return
null
-
cannot assign a collection to a read-only property
-
can assign
null
to a read-write property -
Model report renders collection values
- Format should be similar to the one of
Arrays.toString
- Format should be similar to the one of
-
For a managed type that defines a
Set<String>
read-only propertyaddAll 'b','c'
add 'd'
add 'a'
- call to getter should return a
Set
which is ordered:'b','c','d','a'
-
For a managed type
foo
that defines aSet<String>
read-write property- create a
SortedSet<String> sortedSet
containing'c','b'
- call
foo.setItems(sortedSet)
- add
'd'
tosortedSet
- call
foo.getItems().add('a')
- call again
foo.getItems()
make sure it returns aSet
which contains in that order:'b','c','a'
(effect of copy on write, semantics ofSortedSet
are not preserved)
- create a
-
Copy on write semantics:
List list = ['a', 'b'] foo.setItems(list) list.add 'c' foo.getItems() == ['a', 'b']
-
Useful error message presented when validating schema:
T
is not a scalar typeT
is not the same for getter and setter- Property type is
Collection<T>
,ArrayList<T>
,HashSet<T>
- Suggest to use interface type
List<T>
orSet<T>
if a concrete implementation is used in the interface declaration
-
Convert input value:
CharSequence
to any scalar type (egGString
toLong
,GString
toString
)CharSequence
toFile
conversion relative to project directory, as perProject.file()
.- Any scalar type to
String
.
-
Update user guide, Javadocs and sample
-
Implementation must reuse
NotationConverter
infrastructure. -
TBD: support the 'setter method' pattern from legacy domain types? eg
model {
thing {
baseDir 'some/dir' // same as baseDir = 'some/dir'
retries 12 // same as retries = 12
}
}
- Nice error message when configuring a property that does not exist, for each supported pattern.
- Nice error message when input value cannot be converted.
TBD: make some kind of 'project layout' or 'file resolver' service available as input to rules, which can convert String and friends to File.
- TBD: convert input values? eg add String values to a List?
- TBD: support the 'adder method' and 'setter replaces content' patterns from legacy domain types? eg
- Update user guide, Javadocs and samples
model {
thing {
sourceDirs 'a', 'b' // same as sourceDirs.addAll([convertToFile('a'), convertToFile('b')])
sourceDirs = ['a'] // same as sourceDirs.clear(); sourceDirs.add(convertToFile('a'))
}
}
- Nice error message when configuring a property that does not exist, for each supported pattern.
- Nice error message when input value cannot be converted.
@Managed
interface Thing {
ModelMap<Foo> getFoos();
}
- No setter for property allowed
- Element type must be
@Managed
- Type taking creation methods must support subtypes
- Rule taking methods (e.g.
all(Action)
) must throw when object is read only - All created elements implementing
Named
must havename
property populated, matching the node link name - Can depend on model map element by specific type in rules
- Element type cannot be any kind of type var
- Can be top level element
- Can be property of managed type
interface ModelSet<T> implements Set<T> {
void create(Class<? extends T> type, Action<? super T> initializer);
<O> Set<O> ofType(Class<O> type);
}
<T>
does not need to be managed type (but can be)type
given tocreate()
must be a valid managed type- All mutative methods of
Set
throw UnsupportedOperationException (likeModelSet
). create
throws exception when set has been realised (i.e. using as an input)- “read” methods (including
ofType
) throws exception when called on mutable instance - No constraints on
O
type parameter given toofType
method - set returned by
ofType
is immutable (exception thrown by mutative methods should include information about the model element of which it was derived)
The initial target for this functionality will be to replace the PlatformContainer
model element, but more functionality will be needed before this is possible.
Add methods to CollectionBuilder
to allow mutation rules to be defined for all elements or a particular container element.
Addall(Action)
toCollectionBuilder
AddafterEach(Action)
toCollectionBuilder
Addnamed(String, Action)
toCollectionBuilder
AddwithType(Class, Action)
toCollectionBuilder
AddwithType(Class)
toCollectionBuilder
to filter.Verify usable from Groovy using closures.- Verify usable from Java 8 using lambdas.
- Reasonable error message when actions fail.
- Sync up on
withType()
orofType()
. - Error message when a rule and legacy DSL both declare a task with given name is unclear as to the cause.
- Fix methods that select by type using internal interfaces, as only the public contract type is visible.
- Type registration:
- Separate out a type registry from the containers and share this.
- Type registration handlers should not invoke rule method until required.
- Remove dependencies on
ExtensionsContainer
.
Add a way to mutate a model element prior to it being exposed to 'user' code.
Add@Defaults
annotation. Apply these before@Mutate
rules.AddCollectionBuilder.beforeEach(Action)
.Apply defaults to managed object before initializer method is invoked.- Reasonable error message when actions fail
- Fail when target cannot accept defaults, eg is created using unmanaged object
@Model
method. - Handle case where defaults are applied to tasks defined using legacy DSL, e.g. fail or perhaps define rules for items in task container early.
Add a way to validate a model element prior to it being used as an input by 'user' code.
Add@Validate
annotation. Apply these after@Finalize
rules.Rename 'mutate' methods and types.- Add
CollectionBuilder.validateEach(Action)
- Don't include wrapper exception when rule fails, or add some validation failure collector.
- Add specific exception to be thrown on validation failure
- Nice error message when validation fails.
- Currently validates the element when it is closed, not when its graph is closed.
model {
components {
mylib(SomeType) {
... initialisation ...
}
beforeEach {
... invoked before initialisation ...
}
mylib {
... configuration, invoked after initialisation ...
}
afterEach {
... invoked after configuration
}
}
}
- Add factory to mix in Groovy DSL and state checking, share with managed types and managed set.
- Validate closure parameter types.
- Verify:
- Can apply rule to all elements in container using DSL
- Can apply rule to single element in container using DSL
- Can create element in container using DSL
- Decent error message when applying rule to unknown element in container
- Decent error message when using DSL to create element of unknown/incorrect type
- Currently does not extract rules or input references at compile time. Rules are added at execution time via a
CollectionBuilder
.
- Change
DefaultCollectionBuilder
implementation to register a creation rule rather than eagerly instantiating and configuring.- Verify construction and initialisation action is deferred, but happens before mutate rules when target is used as input.
- Verify initialisation action happens before
project.tasks.all { }
andproject.tasks.$name { }
actions. - Reasonable error message is received when element type cannot be created.
- Attempt to discover an unknown node by closing its parent.
- Apply consistently to all model elements of type
PolymorphicDomainObjectContainer
. - Apply DSL consistently to all model elements of type
PolymorphicDomainObjectContainer
.
Options:
- Bridge placeholders added to
TaskContainer
into the model space as the placeholders are defined. - Flip the relationship so that implicit tasks are defined in model space and bridged across to the
TaskContainer
.
Tests:
- Verify can use model rules to configure and/or override implicit tasks.
Don't do this until project configuration closes only the tasks that are required for the task graph.
Other issues:
- Remove the special case to ignore bridged container elements added after container closed.
- Handle old style rules creating tasks during DAG building.
- Add validation to prevent removing links from an immutable model element.
- Reasonable error message when action fails.
- Support 'anonymous' links
- Sync view logic with
CollectionBuilder
views. - Use same descriptor scheme as
CollectionBuilder
. - Use more efficient implementation of set query methods.
- Allow
ModelSet
to be used as a task dependency. - Fail when mutator rule for an input is not bound.
- Support passing a closure to
create()
- Validate closure parameter types.
- Rename
CollectionBuilder
toManagedMap
. - Currently it is possible to get an element via
CollectionBuilder
, to help with migration. Lock down access toget()
and other query methods.- Same for
size()
.
- Same for
- Lock down read-only view of
ManagedMap
, provide aMap
as view. - Change
ManagedMap
to extendMap
. - Mix in the DSL conveniences into the managed collections and managed objects, don't reuse the existing decorator.
- Allow a
ManagedMap
to be added to model space using a@Model
rule. - Synchronisation back to
TaskContainer
, so thatproject.tasks.all { }
andproject.tasks { $name { } }
works. - Need efficient implementation of query methods that does not scan all linked elements to check type.
- Implement
containsValue()
. - Separate out type registry from the component/binary/source set containers.
- Allow
ManagedMap
andManagedMap.values()
to be used as a task dependency. - Add
toString()
implementation.
- Currently blocked on
NativeToolChainRegistryInternal.addDefaultToolChains()
TestSuiteContainer
should extendPolymorphicDomainObjectContainer
.ProjectSourceSet
should extendPolymorphicDomainObjectContainer
.
- Add
all(Action<? super T)
method toModelSet
. - Add DSL support for
all { }
rule.
- Documentation and samples
- Value types:
- Should support
is
style getters for boolean properties. - Should support all numeric types, including
Number
. (?) - Should support primitives. (?)
- Should support more value types, such as
File
andCharSequence
- Should support
- Support parameterized non-collection types as managed types
- Mix DSL and Groovy methods into managed type implementations.
- Add DSL and Groovy type coercion for enums, closures, files, etc
- Improve 'missing property' and 'missing method' exceptions.
- 'Cannot set read-only property' error messages should report public type instead of implementation type.
- Error message attempting to set property using unsupported type should report public type and acceptable types.
- Missing method exception in DSL closure reports the method missing on the containing project, rather than the delegate object. Works for abstract Groovy class.
- Add Java API for type coercion
- Implement
add()
and treat it as adding a reference, and remove() and treat it as removing a reference. - Collections of value elements
- Collections of collections
- Map type collections
- Ordered collections
- Semi ordered collections (e.g. command line, where some elements have an order relationship)
- Maps of value elements
- Maps of model elements
- Collections of implicitly keyed elements, acting as a map
- Equality concerns when using sets
- Convenience and/or enforcement of “internal to plugin” properties and views/types
- Extending model elements with new properties
- Specializing model elements to apply new views
- Reporting on usages of deprecated and incubating properties and views/types
- ModelSchema does not hold reference to schemas for property types or collection element types
- Each time a managed object is created, need to do a cache lookup for the schema of each property.
- Each time a managed set is created, need to do a cache lookup for the schema of the element type.
- Each time a managed object property is set, need to do a cache lookup to do validation.
- Managed projections should be reused per type
- Unmanaged projections should be reused per type
- Unmanaged instance model views should be used for the life of the node (right now we create a new view each time we need the node value)
- Read only views should not strongly reference backing node once the view has been fully realized
- Managed types should be able to internally use service (with potentially build local state) to provide convenience methods (?)
- Declaring “services” (i.e. behaviour is interesting, not state) using same “techniques” as managed types?
- Audit of error messages
- Invalid managed types
- Immutability violations
- Semantics of equals/hashCode
- Support getting "address" (creation/canonical path) of a managed object
- Throw a meaningful exception instead of failing with
OutOfMemoryError
at runtime when a managed type instantiation cycle is encountered (a composite type that contains an instance of itself keeps on creating new instances indefinitely) - Attempt to call setter method in (abstract class) managed model from non-abstract getter receives error when extracting type metadata (i.e. fail sooner than runtime)