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.
@Managed
interface Platform {
String getDisplayName();
void setDisplayName(String name);
OperatingSystem getOperatingSystem();
void setOperatingSystem(OperatingSystem operatingSystem); // setter for @Managed type indicates it's a reference
}
@Managed
interface OperatingSystem {
String getName();
void setName(String name);
}
class RulePlugin {
@Model
void createOs(OperatingSystem os) {
os.setName("windows");
}
@Model
void createPlatform(Platform platform, OperatingSystem os) {
platform.setDisplayName("Microsoft Windows")
platform.setOperatingSystem(os)
}
@Mutate
void addPersonTask(CollectionBuilder<Task> tasks, Platform platform) {
tasks.create("echo", t ->
t.doLast(t2 -> System.out.println(platform.getOperatingSystem().getName())); // prints 'windows'
);
}
}
CallingsetOperatingSystem()
with “non managed” impl ofOperatingSystem
is a runtime error (i.e. only managed objects can be used)
- Should be able to path to and through the referenced element.
- Implementation should link to the referenced node, rather than a view of the element.
- When a rule receives a view of an object graph, a particular element that is linked into multiple locations in the graph should be represented using the same object instance
in every location:
- One link is an 'inherent' property, the rest are references
- All links are references
- The links occurs in multiple locations in the view graph
- Link occurs in a collection
- The same element appears in different inputs to a rule
interface Named {
String getName(); void setName(String name);
}
@Managed
interface NamedThing extends Named {
String getValue(); void setValue(String value);
}
- Super types do not need to be annotated with
@Managed
- but are subject to the same constraints as@Managed
types - Specialisation of a generic parent is not supported through this story (i.e. can't do
interface BookList extends List<Book>
)
Can extend more than one interfaceError message produced when super type is not a “manageable” type indicates the original (sub) type (and the rule that caused it to be extracted)Can get/set properties of super type(s)Can depend on super type as input and subjectTwo different types can extend same parentProperty conflicts between super types are detected (different types for the same name)
@Managed
interface Person {
String getName(); void setName(String string)
}
@Managed
interface Group {
String getName(); void setName(String string)
ManagedSet<Person> getMembers();
}
class Rules {
@Model
void group(Group group) {
group.name = "Women in computing"
group.members.create(p -> p.setName("Ada Lovelace"))
group.members.create(p -> p.setName("Grace Hooper"))
}
}
model {
tasks {
create("printGroup") {
it.doLast {
def members = $("group").members*.name.sort().join(", ")
def name = $("group").name
println "$name: $members"
}
}
}
}
Something like the snippet aboveCan set/get a reference to a collection of managed types
- Support for enums of any type
- Enum values are opaque to the model space (i.e. we do not treat enum values as structured objects, e.g. cannot depend on a property of an enum value)
- Enum values are not strictly immutable/threadsafe in Java but almost always are, as such we will consider them to be at this stage
- It doesn't have any impact at this stage, but only the enum value is strictly part of the model (all properties of an enum value are supplied by the runtime)
interface MyModel {
@org.gradle.model.Unmanaged
SomeWeirdThing getThing()
void setThing(SomeWeirdThing thing)
}
Properties of an unmanaged type must be explicitly annotated with @Unmanaged
.
The rationale for this is that the use of unmanaged properties will have a significant impact on tooling and functionality in general, as such it should be very clear to model consumers which properties are unmanaged.
Unmanaged properties must be accompanied by a setter.
Can attach an an unmanaged propertyError when unmanaged property does not have annotationSubtype may declare setter for unmanaged typeUnmanaged property of managed type can be targeted for mutationUnmanaged property of managed type can be used as input
@Managed
interface Person {
String getName(); void setName(String string)
}
class Rules {
@Model
void p1(Person person) {
person.setName("foo");
}
@Mutate void addPeople(CollectionBuilder<Task> tasks, String personName) {
tasks.create("injectedByType", t -> t.doLast(() -> assert personName.equals("foo"))
}
}
model {
tasks {
create("injectedByName") {
it.doLast {
assert $("p1.name") == "foo"
}
}
}
}
Can inject leaf type property (e.g. String, Number)Can inject node type property (i.e. another managed type with properties)Can inject property of property of managed type (i.e. given typeA
has property of managed typeB
, can inject properties ofB
)Can inject by “path”
@Managed
interface Person {
String getName(); void setName(String string)
Person getMother();
Person getFather();
}
class Rules {
@Model
void p1(Person person) {
person.setName("foo");
}
@Mutate void setFather(@Path("p1.father") Person father) {
father.setName("father")
}
}
model {
p1.mother { name = "mother" }
tasks {
create("test") {
it.doLast {
def p1 = $("p1")
assert p1.mother.name == "mother"
assert p1.father.name == "father"
}
}
}
}
(above)
@Managed
interface Person {
Person getPartner();
String getName();
void setName(String string)
}
class Holder {
static Person person
}
class Rules {
@Model
void p1(Person person) {
person.setName("foo");
Holder.person = person
}
@Mutate void setFather(CollectionBuilder<Task> tasks, Person person) {
Holder.person.setName("foo") // ← runtime error
Holder.person.partner.setName("foo") // ← runtime error
person.setName("foo") // ← runtime error
person.partner.setName("foo") // ← runtime error
}
}
Runtime error received when trying to mutate an immutable object should include a reference to the rule that created the immutable object (i.e. not the model element, but that actual object).
User receives runtime error trying to mutate managed set when used as input and outside of mutation method
@Managed
interface Platform {
ManagedSet<OperatingSystem> getOperatingSystems()
}
@Managed
interface OperatingSystem {
String getName()
void setName(String)
}
class Holder {
static Platform platform
}
class Rules {
@Model
void p(Platform platform) {
Holder.platform = platform
platform.operatingSystems.create { name = "foo" }
}
@Mutate void setFather(CollectionBuilder<Task> tasks, Platform platform) {
Holder.platform.create(…) // ← runtime error
platform.create(…) // ← runtime error
}
}
Runtime error received when trying to mutate an immutable object should include a reference to the rule that created the immutable object (i.e. not the model element, but that actual object).
It should not be possible to call any of these methods until the set is realised.
User sees useful type name in stack trace for managed model type and while debugging (i.e. not JDK proxy class names)
Use class generation tool that allows specifying the name of a generated class, e.g. cglib or asm.
When a runtime error is thrown from implementation of a managed element setter/getter the stack trace contains reference to the name of the managed type.
- Types must obey all the same rules as interface based impls
- No constructors are allowed (as best we can detect)
- Can not declare any instance scoped fields
- Subclass should be generated as soon as type is encountered to ensure it can be done
- Should use same class generation techniques as existing decoration (but no necessarily share impl)
- Instance should be created as soon as type is encountered to ensure it can be done
Class based managed type can be used everywhere interface based type canSubclass impl is generated once for each type and reusedSubclass cache does not prevent class from being garbage collectedClass can implement interfaces (with methods conforming to managed type rules)Class can extend other classes (all classes up toObject
must conform to the same rules)Constructor can not call any setter methods (at least a runtime error)Class that cannot be instantiated (e.g. default constructor throws)Class and its ancestors cannot have protected or private abstract and non-abstract methods
- Abstract class should be able to provide a custom
toString()
implementation, should only be able to use inherent properties of the object.
@Managed
abstract class Person {
abstract String getFirstName()
abstract void setFirstName(String firstName)
abstract String getLastName()
abstract void setLastName(String lastName)
String getName() {
return getFirstName() + " " + getLastName()
}
}
- Only “getter” methods are allowed to be non
abstract
Runtime error if provided getter (i.e. non abstract one) calls a setter method
@Managed
interface Person {
String getFirstName();
void setFirstName(String firstName);
String getLastName();
void setLastName(String lastName);
default String getName() {
return getFirstName() + " " + getLastName();
}
}
@RuleSource
class RulePlugin {
@Model
void createPerson(Person person) {
person.setFirstName("Alan");
person.setLastName("Turing");
}
@Mutate
void addPersonTask(CollectionBuilder<Task> tasks, Person person) {
tasks.create("echo", task -> {
task.doLast(unused -> {
System.out.println(String.format("name: %s", person.getName()));
});
});
}
}
- Same semantics as non abstract methods on class based types
like snippet above
interface ManagedSet<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 (likeManagedSet
). 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
ManagedSet
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 toManagedSet
. - 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)