On 17/02/2011, at 10:07 AM, Peter Niederwieser wrote:
>
> I've been thinking about ways to improve Gradle's "configuration DSL" - the
> way domain objects (projects, tasks, etc.) get configured with closures. The
> current approach is similar to Groovy's Object.with {} method. It maps
> method/property calls inside the closure directly to method/property calls
> on the underlying object. What I like about this approach is that it is very
> simple and predictable. As soon as you know the API, you know the DSL. On
> the other hand, the approach also has some drawbacks:
>
> - Using the DSL doesn't give much over using the underlying object's API
> directly.
> - In order to provide a rich DSL, one has to inflate the API with
> getters/setters, methods accepting single arguments and/or varargs, methods
> with varying parameter types, etc.
> - As a result of inflating the API, code duplication and inconsistencies
> will creep in (property vs. method syntax, different coercions supported in
> different places, etc.). And the larger the API grows, the more difficult it
> will be to use directly, for example when implementing a plugin.
>
> I'm proposing a "smarter" (excuse the word) configuration DSL. Such a DSL
> could allow this domain object:
>
> class Foo {
> String bar
>
> List<Integer> moos
>
> Map vars
>
> Dependency dep
> }
>
> ...to be configured as follows:
>
> foo {
> bar "some string"
>
> moo 5
> moo "6"
> moos 7, 8, 9
>
> vars one: 1, two: 2
>
> dep "junit:junit:4.8"
> dep new Dependency("junit", "junit", "4.8")
> }
>
> This example tries to give an idea of how:
> - a small and focused convention-based (bean-style) API could "generate" a
> rich DSL
> - a method-based DSL is less noisy and feels more declarative than a
> property-based DSL (no '=')
> - a method-based DSL can leverage syntactic features such as varargs and
> named parameters
> - a smart DSL could accept arguments not accepted by the underlying API
>
> The last point is particularly important. The idea is that the DSL leverages
> a central service to automatically coerce arguments to the types expected by
> the API. This keeps the API small and guarantees that the same coercions are
> available everywhere, with zero effort on behalf of the API author. Plugins
> could potentially extend the set of known coercions.
This approach also allows us to have caller specific coercions. For example, we
might convert a String to a File relative to the script (ie caller) location.
At the moment, coercions are hard-coded into the target object, and so must be
caller independent.
>
> Of course the story doesn't end here. A smart configuration DSL could also:
> - allow arguments to be lazy by accepting a closure
> - free APIs from Groovy-specific types and conventions (for example by
> coercing closures to Gradle's Actions)
> - automatically issue a deprecation warning if a method marked with
> @Deprecated is called
- We could do some interesting things with forward references to domain objects:
task a { dependsOn b }
task b
When the configuration closure for task a executes, 'b' does not refer to
anything. We could instead treat 'b' as an object of type UnknownReference,
which might, when passed to dependsOn(), could be coerced to a Callable<Task>
or some other placeholder.
- Improve error messages. Information about where a value came from can travel
with the value, which we can use in error messages. For example, when we
validate the value of a property later in the build, we can inform the user
where the invalid value came from.
- Improve reporting. For example the report generated by 'gradle properties'
might include information about where a property was defined, and where the
effective value came from.
- Apply a trust model. For example, untrusted code will be prevented from doing
certain things to a domain object (eg querying userName and password
properties, say, or changing the destination for an upload).
>
> I've already implemented DSLs similar to the one outlined here with great
> success. The only clear downside that I can see is that taking Gradle into
> this direction would likely constitute a major breaking change. However, the
> potential benefits convinced me to bring up the topic anyway (and before
> 1.0). What do you think? Looking forward to hearing your opinions.
I think this is an excellent idea, and something we should do before 1.0.
Given that the above mapping is pretty much what we already use in the DSL
(albeit on an ad hoc basis), it's not actually a breaking change, if we were to
continue to allow property assignment. Perhaps we could tackle it by:
1. Introduce a mapping layer, which maps:
* Property access in the DSL to a property access on the target object
* Method call in the DSL to a method call on the target object. On unknown
method, attempts to map a single parameter method call to a set property call
on the target object.
2. Clean up our domain objects to remove unnecessary methods.
3. Deprecate the use of property assignment in the DSL (and spit out a warning
when you do).
3. Start adding additional mappings, in particular handling multi-values
properties, cleaning up our domain objects as we go.
4. Start introducing coercions, in particular to File, Iterable<File> and
FileCollection.
5. Remove support for using property assignment in the DSL.
Some open questions:
* Do we actually want to prevent the use of property assignment?
* How should the DSL syntax look for multi-valued properties? We want to be
able to add values, remove values, and replace the contents.
* Currently, plugins drive the API, rather than the DSL. It would be nice to
reuse as much of the DSL as possible when implementing plugins.
--
Adam Murdoch
Gradle Developer
http://www.gradle.org
CTO, Gradle Inc. - Gradle Training, Support, Consulting
http://www.gradle.biz