by Cédric Champeau (@CedricChampeau)
speaker { name 'Cédric Champeau' company 'Gradle Inc' oss 'Apache Groovy committer', successes 'Static type checker', 'Static compilation', 'Traits', 'Markup template engine', 'DSLs' failures Stream.of(bugs), twitter '@CedricChampeau', github 'melix', extraDescription '''Groovy in Action 2 co-author Misc OSS contribs (Gradle plugins, deck2pdf, jlangdetect, ...)''' }
Focused
Readable
Practical
(usually) embeddable
Examples: SQL, HTML, XSLT, Ant, …
This is an opiniated talk about how a DSL designed with Apache Groovy should look like.
Concise, clean syntax
Supports scripting
Supports metaprogramming
Embeddable
Mature tooling: Eclipse, IntelliJ, Netbeans…
sql.execute """insert into PROJECT (id, name, url) values ($map.id, $map.name, $map.url)"""
def persons = Person.findByLastName('Stark') assert persons.findAll { it.alive }.isEmpty()
task(hello) << { println "hello" }
vs
task(hello) { println "hello" }
removing semicolons is not designing a DSL
removing parenthesis is not designing a DSL
user experience is important
consistency is important
Try to be idiomatic
setup: def map = new HashMap() when: map.put(null, "elem") then: notThrown(NullPointerException)
assert Person.findAll { lastName == 'Stark' && alive }.isEmpty()
model { components { shared(CustomLibrary) { javaVersions 6, 7 } main(JvmLibrarySpec) { targetPlatform 'java6' targetPlatform 'java7' sources { java { dependencies { library 'shared' } } } } } }
ratpack { handlers { get { render "Hello World!" } get(":name") { render "Hello $pathTokens.name!" } } }
job { using 'TMPL-test' name 'PROJ-integ-tests' scm { git(gitUrl) } triggers { cron('15 1,13 * * *') } steps { maven('-e clean integTest') } }
modelTypes = { List<String> persons } html { body { ul { persons.each { p -> li p.name } } } }
Closures with support annotations (@DelegatesTo, …)
Compilation customizers
AST transformations
Type checking extensions
Groovy Shell / Groovy Console
Still at the core of most DSLs
delegate is very important:
['Paris', 'Washington', 'Berlin'].collect { it.length() == 5 }
do we really need it?
class HelperExtension { public static <T,U> List<U> myCollect(List<T> items, Closure<U> action) { def clone = action.clone() clone.resolveStrategy = Closure.DELEGATE_FIRST def result = [] items.each { clone.delegate = it result << clone() } result } } HelperExtension.myCollect(['Paris', 'Washington', 'Berlin']) { length() == 5 }
META-INF
services
org.codehaus.groovy.runtime.ExtensionModule
moduleName=My extension module moduleVersion=1.0 extensionClasses=path.to.HelperExtension
Consume it as if it was a regular Groovy method
['Paris', 'Washington', 'Berlin'].myCollect { length() == 5 }
Best IDE support
Only way to have static type checking
public static <T,U> List<U> myCollect( List<T> items, @DelegatesTo(FirstParam.FirstGenericType) Closure<U> action) { ... }
Is your DSL self-contained?
If so
Try to remove explicit imports
Avoid usage of the new keyword
Avoid usage of annotations
Embrace SAM types
This is ugly:
handle(new Handler() { @Override void handle(String message) { println message } })
This is cool:
handle { println message }
SAM type coercion works for both interfaces and abstract classes.
class WebServer { static void serve(@DelegatesTo(ServerSpec) Closure cl) { // ... } }
def importCustomizer = new ImportCustomizer() importCustomizer.addStaticStars 'com.acme.WebServer' def configuration = new CompilerConfiguration() configuration.addCompilationCustomizers(importCustomizer) def shell = new GroovyShell(configuration) shell.evaluate ''' serve { port 80 get('/foo') { ... } } '''
ImportCustomizer: automatically add imports to your scripts
ASTTransformationCustomizer: automatically apply AST transformations to your scripts
SecureASTCustomizer: restrict the grammar of the language
SourceAwareCustomizer: apply customizers based on the source file
class WebServer { static void serve(@DelegatesTo(ServerSpec) Closure cl) { def spec = new ServerSpec() cl.delegate = spec cl.resolveStrategy = 'DELEGATE_FIRST' cl() def runner = new Runner() runner.execute(spec) } }
class ServerSpec { int port void port(int port) { this.port = port } void get(String path, @DelegatesTo(HandlerSpec) Closure spec) { ... } }
Use the ServerSpec style above
The closure should configure the model
Execution can be deferred
Provide early feedback to the user
Type safety
Help the compiler understand your DSL
Event-based API
React to events such as undefined variable or method not found
Developer instructs the type checker what to do
methodNotFound { receiver, name, argList, argTypes, call -> if (receiver==classNodeFor(String) && name=='longueur' && argList.size()==0) { handled = true return newMethod('longueur', classNodeFor(String)) } }
Given the following template
pages.each { page -> p("Page title: $page.title") p(page.text) }
How do you make sure that pages is a valid model type?
How do you notify the user that page doesn’t have a text property?
How to make it fast?
Declare the model types
modelTypes = { List<Page> pages } pages.each { page -> p("Page title: $page.title") p(page.text) }
Implement a type checking extension
Recognizes unresolved method calls
converts them into direct methodMissing calls
Recognizes unresolved variables
checks if they are defined in the binding
if yes, instructs the type checker what the type is
Applies @CompileStatic transparently
Performs post-type checking transformations
Don’t do this at home!
For type checking/static compilation
['a','b','c'].eachWithIndex { str, idx -> ... }
public static <T> Collection<T> eachWithIndex( Collection<T> self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) { ... }
Check out the documentation for more details.
Leverage the lean syntax of Groovy
Scoping improves readability
Use the delegate
Use @DelegatesTo and @ClosureParams for IDE/type checker support
Use imperative style as last resort
Help yourself (builders, immutable datastructures, …)
Slides and code : https://github.com/melix/javaone-groovy-dsls
Groovy documentation : http://groovy-lang.org/documentation.html
Follow me: @CedricChampeau