DSL’ing YOUR
@alotor
@alotor @alotor
Alonso Torres
Domain-specific
Languages
a Domain Specific Language
is a programming language that offers,
through appropriate notations and
abstractions, expressive power focused on a
particular problem domain.
a Domain Specific Language
is a programming language that offers,
through appropriate notations and
abstractions, expressive power focused on a
particular problem domain.
Expressive abstractions and notations
for a particular problem
A code snippet is worth a
thousand images
Configuration
log4j.main = {
error 'org.codehaus.groovy.grails.web.servlet',
'org.codehaus.groovy.grails.web.pages',
'org.codehaus.groovy.grails.web.sitemesh',
'org.codehaus.groovy.grails.web.mapping.filter',
'org.codehaus.groovy.grails.web.mapping',
'org.codehaus.groovy.grails.commons',
'org.codehaus.groovy.grails.plugins',
'org.codehaus.groovy.grails.orm.hibernate',
'org.springframework',
'org.hibernate',
'net.sf.ehcache.hibernate'
debug 'myapp.core',
}
class User {
...
static constraints = {
login size: 5..15, blank: false, unique: true
password size: 5..15, blank: false
email email: true, blank: false
age min: 18
}
}
Expressive API
def results = Account.createCriteria() {
between "balance", 500, 1000
eq "branch", "London"
or {
like "holderFirstName", "Fred%"
like "holderFirstName", "Barney%"
}
maxResults 10
order "holderLastName", "desc"
}
Specific notations
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
User’s input
apply plugin: 'groovy'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.1'
testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
testCompile 'junit:junit:4.11'
}
How cool is that?
But only “them”
can do those things
1. Closures
2. Builders
3. Open Classes
4. AST
5. Script
TOC
0. Groovy “sugar”
▸ Optional parentheses
GROOVY NICETIES
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.1'
testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
testCompile 'junit:junit:4.11'
}
dependencies({
compile('org.codehaus.groovy:groovy-all:2.4.1')
testCompile('org.spockframework:spock-core:0.7-groovy-2.0')
testCompile('junit:junit:4.11')
})
▸ Optional parentheses
▸ Getter / setters
GROOVY NICETIES
sourceCompatibility = 1.8
targetCompatibility = 1.8
void setSourceCompatibility(version) {
...
}
void setTargetCompatibility(version) {
...
}
def sourceVersion = script.sourceCompatibility
def targetVersion = script.targetCompatibility
def getSourceCompatibility() {
...
}
def getTargetCompatibility() {
...
}
▸ Optional parentheses
▸ Getter / setters
▸ Operator overloading
GROOVY NICETIES
Operator Method
+ a.plus(b)
- a.minus(b)
* a.multiply(b)
/ a.div(b)
% a.mod(b)
** a.power(b)
| a.or(b)
& a.and(b)
^ a.xor(b)
Operator Method
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
<< a.leftShift(b)
>> a.rightShift(b)
++ a.next()
-- a.previous()
+a a.positive()
-a a.negative()
~a a.bitwiseNegative()
▸ Optional parentheses
▸ Getter / setters
▸ Operator overloading
▸ Keyword arguments
GROOVY NICETIES
def myKeyArgs(Map keyargs=[:], String value1, String value2) {
...
}
myKeyArgs("value1", "value2")
myKeyArgs("value1", "value2", cache: true)
myKeyArgs("value1", "value2", drop: 20, take: 50)
▸ Optional parentheses
▸ Getter / setters
▸ Operator overloading
▸ Keyword arguments
▸ Closure arguments
GROOVY NICETIES
def myClosureArg(String value1, String value2, Closure cls=null) {
...
}
myClosureArg("value1", "value2")
myClosureArg("value1", "value2") {
println ">> Calling inside closure"
}
▸ Optional parentheses
▸ Getter / setters
▸ Operator overloading
▸ Keyword arguments
▸ Closure arguments
▸ Command chaining
GROOVY NICETIES
take 2.pills of chloroquinine after 6.hours
take(2.pills).of(chloroquinine).after(6.hours)
paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
given({}).when({}).then({})
given { } when { } then { }
Now, let’s talk business
1. Closure DSL’s
▸ DSL inside a closure
CLOSURE DSL’s
emailService.send {
from 'grumpy@cat.com'
to 'keyboard@cat.com'
subject 'Check this video out!'
body {
p 'Really awesome!'
}
}
▸ DSL inside a closure
CLOSURE DSL’s
emailService.send {
from 'grumpy@cat.com'
to 'keyboard@cat.com'
subject 'Check this video out!'
body {
p 'Really awesome!'
}
}
Method invocation.
Where are these
methods?
▸ this
▸ owner
▸ delegate
GROOVY CLOSURES CONTEXT
Three objects handle
the closure context
▸ this
▸ owner
▸ delegate
GROOVY CLOSURES CONTEXT
Normaly handles the
context (default)
▸ this
▸ owner
▸ delegate
GROOVY CLOSURES CONTEXT
Only changes for
nested closures
▸ this
▸ owner
▸ delegate
GROOVY CLOSURES CONTEXT
Can be changed!
▸ The handler will be called
CLOSURE DSL’s
class EmailHandler {
void from(String value) { }
void to(String value) { }
void subject(String value) { }
void body(Closure body) { }
Map buildData() { }
}
▸ Set the handler as delegate
CLOSURE DSL’s
def send(Closure dsl) {
def handler = new EmailHandler()
def code = cls.rehydrate(handler, null, null)
code.resolveStrategy = Closure.DELEGATE_ONLY
code.call()
def emailData = handler.buildData()
}
▸ Set the handler as delegate
CLOSURE DSL’s
def send(Closure dsl) {
def handler = new EmailHandler()
def code = cls.rehydrate(handler, null, null)
code.resolveStrategy = Closure.DELEGATE_ONLY
code.call()
def emailData = handler.buildData()
}
delegate owner this
▸ Set the handler as delegate
CLOSURE DSL’s
def send(Closure dsl) {
def handler = new EmailHandler()
def code = cls.rehydrate(handler, null, null)
code.resolveStrategy = Closure.DELEGATE_ONLY
code.call()
def emailData = handler.buildData()
}
Disable unexpected
interactions
▸ Set the handler as delegate
CLOSURE DSL’s
def send(Closure dsl) {
def handler = new EmailHandler()
def code = cls.rehydrate(handler, null, null)
code.resolveStrategy = Closure.DELEGATE_ONLY
code.call()
def emailData = handler.buildData()
}
Call the NEW closure
▸ Set the handler as delegate
CLOSURE DSL’s
def send(Closure dsl) {
def handler = new EmailHandler()
def code = cls.rehydrate(handler, null, null)
code.resolveStrategy = Closure.DELEGATE_ONLY
code.call()
def emailData = handler.buildData()
}
The handler now
contains the data
▸ All closure’s method/properties calls will
call a delegate
▸ Build around the delegate and then
retrieve the data
CLOSURE DSL’s
2. Groovy Builders
▸ Problem: Complex nested structures
BUILDER DSL’s
def bookshelf = builder.bookshelf {
author("George R. R. Martin") {
books {
"A Game Of Thrones" {
pages 1000
characters 57
houses {
stark {
motto "Winter is comming"
}
}
}
}
}
}
▸ Problem: Complex nested structures
BUILDER DSL’s
def bookshelf = builder.bookshelf {
author("George R. R. Martin") {
books {
"A Game Of Thrones" {
pages 1000
characters 57
houses {
stark {
motto "Winter is comming"
}
}
}
}
}
}
Delegate HELL
▸ Groovy provides support for this type of
DSL
▸ groovy.util.BuilderSupport
BUILDER DSL’s
▸ Defines a tree-like structure
BUILDER DSL’s
class BinaryTreeBuilderSupport extends BuilderSupport {
def createNode(def name, Map attributes, def value) {
new Container(name: name,
attributes: attributes,
value: value)
}
void setParent(def parent, def child) {
parent.items.push(child)
}
...
}
▸ Defines a tree-like structure
BUILDER DSL’s
class BinaryTreeBuilderSupport extends BuilderSupport {
def createNode(def name, Map attributes, def value) {
new Container(name: name,
attributes: attributes,
value: value)
}
void setParent(def parent, def child) {
parent.items.push(child)
}
...
}
Create Nodes
▸ Defines a tree-like structure
BUILDER DSL’s
class BinaryTreeBuilderSupport extends BuilderSupport {
def createNode(def name, Map attributes, def value) {
new Container(name: name,
attributes: attributes,
value: value)
}
void setParent(def parent, def child) {
parent.items.push(child)
}
...
}
Define parent-children
relationship
▸ Profit
BUILDER DSL’s
def bookshelf = builder.bookshelf {
author("George R. R. Martin") {
books {
"A Game Of Thrones" {
...
}
...
}
}
}
println bookshelf.items[0].items[0].items.name
>>> [“A Game of Thrones”, ...]
▸ You can use the BuilderSupport when you
have complex tree-like structures
▸ Only have to create nodes and
relationships between them
BUILDER DSL’s
3. Open Classes
▸ Groovy “standard” types can be extended
OPEN CLASSES DSL’s
Integer.metaClass.randomTimes = { Closure cls->
def randomValue = (new Random().nextInt(delegate)) +1
randomValue.times(cls)
}
Adding the method
“randomTimes” to ALL
the Integers
▸ Groovy “standard” types can be extended
OPEN CLASSES DSL’s
Integer.metaClass.randomTimes = { Closure cls->
def randomValue = (new Random().nextInt(delegate)) +1
randomValue.times(cls)
}
delegate has the
Integer’s value
▸ Groovy “standard” types can be extended
OPEN CLASSES DSL’s
Integer.metaClass.randomTimes = { Closure cls->
def randomValue = (new Random().nextInt(delegate)) +1
randomValue.times(cls)
}
Repeat a random
number of times the
closure
▸ Groovy “standard” types can be extended
OPEN CLASSES DSL’s
Integer.metaClass.randomTimes = { Closure cls->
def randomValue = (new Random().nextInt(delegate)) +1
randomValue.times(cls)
}
10.randomTimes {
println "x"
}
▸ Allows us to create nice DSL’s
OPEN CLASSES DSL’s
def order = buy 10.bottles of "milk"
▸ Allows us to create nice DSL’s
OPEN CLASSES DSL’s
def order = buy 10.bottles of "milk"
Integer.metaClass.getBottles = {
return new Quantity(quantity: delegate, ontainer: "bottle")
}
4. AST Transformations
▸ Problem: The language isn’t flexible
enough for your taste
AST DSL’s
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
▸ Problem: The language isn’t flexible
enough for your taste
AST DSL’s
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
What???!!!!
▸ With AST’s you can modify the language
on compile time
▸ BUT you have to respect the syntax
AST DSL’s
AST DSL’s
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
Bit-level OR Logical OR
▸ We can do the same
AST DSL’s
class Main {
@SpockTable
def getTable() {
value1 | value2 | value3 || max
1 | 2 | 3 || 3
2 | 1 | 0 || 2
2 | 2 | 1 || 2
}
public static void main(def args) {
def tableData = new Main().getTable()
assert tableData['value1'] == [1, 2, 2]
}
}
▸ We can do the same
OPEN CLASSES DSL’s
class Main {
@SpockTable
def getTable() {
value1 | value2 | value3 || max
1 | 2 | 3 || 3
2 | 1 | 0 || 2
2 | 2 | 1 || 2
}
public static void main(def args) {
def tableData = new Main().getTable()
assert tableData['value1'] == [1, 2, 2]
}
}
Local AST
▸ What kind of transformation we want?
AST DSL’s
def getTable() {
value1 | value2 | value3 || max
1 | 2 | 3 || 3
2 | 1 | 0 || 2
2 | 2 | 1 || 2
}
def getTablePostAST() {
[
value1 : [1, 2, 2],
value2 : [2, 1, 2],
value3 : [3, 0, 1],
max : [3, 2, 2]
]
}
AST DSL’s
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
Retrieves all the
method statements
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
The first will be the
header of our table
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
The rest will be the
different values for
the table body
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
With this values we
create new code for
this method body
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
Delete all the old
one
▸ Have to convert from one AST to the other
AST DSL’s
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def existingStatements = ((BlockStatement)method.code).statements
def headers = processTableHeaders(existingStatements[0])
def mapToSet = processTableBody(headers, existingStatements[1..-1])
def mapExpression = createMapStatement(mapToSet)
existingStatements.clear()
existingStatements.add(mapExpression)
}
Replace with the
new code
▸ Try your DSL syntax on groovyConsole
▸ Check the “source” AST and the “target”
AST
▸ Think about how to convert from one to
another
AST DSL’s
No magic involved ;-)
5. Scripting
▸ All these techniques with external scripts
SCRIPTING DSL’s
apply plugin: 'groovy'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.1'
testCompile 'junit:junit:4.11'
}
▸ All these techniques with external scripts
SCRIPTING DSL’s
apply plugin: 'groovy'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.1'
testCompile 'junit:junit:4.11'
}
Properties
Method calls
▸ Script binding to a map
SCRIPTING DSL’s
def binding = new Binding(
apply: { Map args -> println args},
repositories: { Closure dsl -> println "repositories"},
dependencies: { Closure dsl -> println "dependencies" }
)
def shell = new GroovyShell(binding)
shell.evaluate(new File("build.gradle"))
▸ We want a state for these methods
SCRIPTING DSL’s
class MyGradle {
void apply(Map toApply) {
...
}
void repositories(Closure dslRepositories) {
...
}
void dependencies(Closure dslDependencies) {
...
}
}
▸ Script binding to an object
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
configuration.setScriptBaseClass(DelegatingScript.class.getName())
def shell = new GroovyShell(new Binding(),configuration)
def script = shell.parse(new File("build.gradle"))
script.setDelegate(new MyGradle())
script.run()
▸ Script binding to an object
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
configuration.setScriptBaseClass(DelegatingScript.class.getName())
def shell = new GroovyShell(new Binding(),configuration)
def script = shell.parse(new File("build.gradle"))
script.setDelegate(new MyGradle())
script.run()
Type of Script
▸ Script binding to an object
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
configuration.setScriptBaseClass(DelegatingScript.class.getName())
def shell = new GroovyShell(new Binding(),configuration)
def script = shell.parse(new File("build.gradle"))
script.setDelegate(new MyGradle())
script.run()
Set our delegate
▸ Default imports
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def imports = new ImportCustomizer()
imports.addStaticStar('java.util.Calendar')
configuration.addCompilationCustomizers(imports)
▸ Default imports
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def imports = new ImportCustomizer()
imports.addStaticStar('java.util.Calendar')
configuration.addCompilationCustomizers(imports)
import static from java.util.Calendar.*
▸ Apply AST Transformations
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def ast = new ASTTransformationCustomizer(Log)
configuration.addCompilationCustomizers(ast)
▸ Apply AST Transformations
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def ast = new ASTTransformationCustomizer(Log)
configuration.addCompilationCustomizers(ast)
AST to apply inside the script
▸ Sanitize user input
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def secure = new SecureASTCustomizer()
secure.methodDefinitionAllowed = false
configuration.addCompilationCustomizers(secure)
▸ Sanitize user input
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def secure = new SecureASTCustomizer()
secure.methodDefinitionAllowed = false
configuration.addCompilationCustomizers(secure)
We don’t allow method
definitions in the script
1. Closures
2. Builders
3. Open Classes
4. AST
5. Script
Go ahead!
DSL your Groovy
@alotor @alotor
THANKS!

(Greach 2015) Dsl'ing your Groovy

  • 1.
  • 4.
  • 7.
  • 8.
    a Domain SpecificLanguage is a programming language that offers, through appropriate notations and abstractions, expressive power focused on a particular problem domain.
  • 9.
    a Domain SpecificLanguage is a programming language that offers, through appropriate notations and abstractions, expressive power focused on a particular problem domain.
  • 10.
    Expressive abstractions andnotations for a particular problem
  • 11.
    A code snippetis worth a thousand images
  • 12.
  • 13.
    log4j.main = { error'org.codehaus.groovy.grails.web.servlet', 'org.codehaus.groovy.grails.web.pages', 'org.codehaus.groovy.grails.web.sitemesh', 'org.codehaus.groovy.grails.web.mapping.filter', 'org.codehaus.groovy.grails.web.mapping', 'org.codehaus.groovy.grails.commons', 'org.codehaus.groovy.grails.plugins', 'org.codehaus.groovy.grails.orm.hibernate', 'org.springframework', 'org.hibernate', 'net.sf.ehcache.hibernate' debug 'myapp.core', }
  • 14.
    class User { ... staticconstraints = { login size: 5..15, blank: false, unique: true password size: 5..15, blank: false email email: true, blank: false age min: 18 } }
  • 15.
  • 16.
    def results =Account.createCriteria() { between "balance", 500, 1000 eq "branch", "London" or { like "holderFirstName", "Fred%" like "holderFirstName", "Barney%" } maxResults 10 order "holderLastName", "desc" }
  • 17.
  • 18.
    class MathSpec extendsSpecification { def "maximum of two numbers"() { expect: Math.max(a, b) == c where: a | b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 } }
  • 19.
  • 20.
    apply plugin: 'groovy' sourceCompatibility= 1.8 targetCompatibility = 1.8 repositories { mavenLocal() jcenter() } dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.1' testCompile 'org.spockframework:spock-core:0.7-groovy-2.0' testCompile 'junit:junit:4.11' }
  • 21.
  • 22.
    But only “them” cando those things
  • 24.
    1. Closures 2. Builders 3.Open Classes 4. AST 5. Script TOC
  • 25.
  • 26.
  • 27.
    dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.1' testCompile'org.spockframework:spock-core:0.7-groovy-2.0' testCompile 'junit:junit:4.11' } dependencies({ compile('org.codehaus.groovy:groovy-all:2.4.1') testCompile('org.spockframework:spock-core:0.7-groovy-2.0') testCompile('junit:junit:4.11') })
  • 28.
    ▸ Optional parentheses ▸Getter / setters GROOVY NICETIES
  • 29.
    sourceCompatibility = 1.8 targetCompatibility= 1.8 void setSourceCompatibility(version) { ... } void setTargetCompatibility(version) { ... }
  • 30.
    def sourceVersion =script.sourceCompatibility def targetVersion = script.targetCompatibility def getSourceCompatibility() { ... } def getTargetCompatibility() { ... }
  • 31.
    ▸ Optional parentheses ▸Getter / setters ▸ Operator overloading GROOVY NICETIES
  • 32.
    Operator Method + a.plus(b) -a.minus(b) * a.multiply(b) / a.div(b) % a.mod(b) ** a.power(b) | a.or(b) & a.and(b) ^ a.xor(b) Operator Method a[b] a.getAt(b) a[b] = c a.putAt(b, c) << a.leftShift(b) >> a.rightShift(b) ++ a.next() -- a.previous() +a a.positive() -a a.negative() ~a a.bitwiseNegative()
  • 33.
    ▸ Optional parentheses ▸Getter / setters ▸ Operator overloading ▸ Keyword arguments GROOVY NICETIES
  • 34.
    def myKeyArgs(Map keyargs=[:],String value1, String value2) { ... } myKeyArgs("value1", "value2") myKeyArgs("value1", "value2", cache: true) myKeyArgs("value1", "value2", drop: 20, take: 50)
  • 35.
    ▸ Optional parentheses ▸Getter / setters ▸ Operator overloading ▸ Keyword arguments ▸ Closure arguments GROOVY NICETIES
  • 36.
    def myClosureArg(String value1,String value2, Closure cls=null) { ... } myClosureArg("value1", "value2") myClosureArg("value1", "value2") { println ">> Calling inside closure" }
  • 37.
    ▸ Optional parentheses ▸Getter / setters ▸ Operator overloading ▸ Keyword arguments ▸ Closure arguments ▸ Command chaining GROOVY NICETIES
  • 38.
    take 2.pills ofchloroquinine after 6.hours take(2.pills).of(chloroquinine).after(6.hours) paint(wall).with(red, green).and(yellow) paint wall with red, green and yellow given({}).when({}).then({}) given { } when { } then { }
  • 39.
  • 40.
  • 41.
    ▸ DSL insidea closure CLOSURE DSL’s emailService.send { from 'grumpy@cat.com' to 'keyboard@cat.com' subject 'Check this video out!' body { p 'Really awesome!' } }
  • 42.
    ▸ DSL insidea closure CLOSURE DSL’s emailService.send { from 'grumpy@cat.com' to 'keyboard@cat.com' subject 'Check this video out!' body { p 'Really awesome!' } } Method invocation. Where are these methods?
  • 43.
    ▸ this ▸ owner ▸delegate GROOVY CLOSURES CONTEXT Three objects handle the closure context
  • 44.
    ▸ this ▸ owner ▸delegate GROOVY CLOSURES CONTEXT Normaly handles the context (default)
  • 45.
    ▸ this ▸ owner ▸delegate GROOVY CLOSURES CONTEXT Only changes for nested closures
  • 46.
    ▸ this ▸ owner ▸delegate GROOVY CLOSURES CONTEXT Can be changed!
  • 47.
    ▸ The handlerwill be called CLOSURE DSL’s class EmailHandler { void from(String value) { } void to(String value) { } void subject(String value) { } void body(Closure body) { } Map buildData() { } }
  • 48.
    ▸ Set thehandler as delegate CLOSURE DSL’s def send(Closure dsl) { def handler = new EmailHandler() def code = cls.rehydrate(handler, null, null) code.resolveStrategy = Closure.DELEGATE_ONLY code.call() def emailData = handler.buildData() }
  • 49.
    ▸ Set thehandler as delegate CLOSURE DSL’s def send(Closure dsl) { def handler = new EmailHandler() def code = cls.rehydrate(handler, null, null) code.resolveStrategy = Closure.DELEGATE_ONLY code.call() def emailData = handler.buildData() } delegate owner this
  • 50.
    ▸ Set thehandler as delegate CLOSURE DSL’s def send(Closure dsl) { def handler = new EmailHandler() def code = cls.rehydrate(handler, null, null) code.resolveStrategy = Closure.DELEGATE_ONLY code.call() def emailData = handler.buildData() } Disable unexpected interactions
  • 51.
    ▸ Set thehandler as delegate CLOSURE DSL’s def send(Closure dsl) { def handler = new EmailHandler() def code = cls.rehydrate(handler, null, null) code.resolveStrategy = Closure.DELEGATE_ONLY code.call() def emailData = handler.buildData() } Call the NEW closure
  • 52.
    ▸ Set thehandler as delegate CLOSURE DSL’s def send(Closure dsl) { def handler = new EmailHandler() def code = cls.rehydrate(handler, null, null) code.resolveStrategy = Closure.DELEGATE_ONLY code.call() def emailData = handler.buildData() } The handler now contains the data
  • 53.
    ▸ All closure’smethod/properties calls will call a delegate ▸ Build around the delegate and then retrieve the data CLOSURE DSL’s
  • 54.
  • 55.
    ▸ Problem: Complexnested structures BUILDER DSL’s def bookshelf = builder.bookshelf { author("George R. R. Martin") { books { "A Game Of Thrones" { pages 1000 characters 57 houses { stark { motto "Winter is comming" } } } } } }
  • 56.
    ▸ Problem: Complexnested structures BUILDER DSL’s def bookshelf = builder.bookshelf { author("George R. R. Martin") { books { "A Game Of Thrones" { pages 1000 characters 57 houses { stark { motto "Winter is comming" } } } } } } Delegate HELL
  • 57.
    ▸ Groovy providessupport for this type of DSL ▸ groovy.util.BuilderSupport BUILDER DSL’s
  • 58.
    ▸ Defines atree-like structure BUILDER DSL’s class BinaryTreeBuilderSupport extends BuilderSupport { def createNode(def name, Map attributes, def value) { new Container(name: name, attributes: attributes, value: value) } void setParent(def parent, def child) { parent.items.push(child) } ... }
  • 59.
    ▸ Defines atree-like structure BUILDER DSL’s class BinaryTreeBuilderSupport extends BuilderSupport { def createNode(def name, Map attributes, def value) { new Container(name: name, attributes: attributes, value: value) } void setParent(def parent, def child) { parent.items.push(child) } ... } Create Nodes
  • 60.
    ▸ Defines atree-like structure BUILDER DSL’s class BinaryTreeBuilderSupport extends BuilderSupport { def createNode(def name, Map attributes, def value) { new Container(name: name, attributes: attributes, value: value) } void setParent(def parent, def child) { parent.items.push(child) } ... } Define parent-children relationship
  • 61.
    ▸ Profit BUILDER DSL’s defbookshelf = builder.bookshelf { author("George R. R. Martin") { books { "A Game Of Thrones" { ... } ... } } } println bookshelf.items[0].items[0].items.name >>> [“A Game of Thrones”, ...]
  • 62.
    ▸ You canuse the BuilderSupport when you have complex tree-like structures ▸ Only have to create nodes and relationships between them BUILDER DSL’s
  • 63.
  • 64.
    ▸ Groovy “standard”types can be extended OPEN CLASSES DSL’s Integer.metaClass.randomTimes = { Closure cls-> def randomValue = (new Random().nextInt(delegate)) +1 randomValue.times(cls) } Adding the method “randomTimes” to ALL the Integers
  • 65.
    ▸ Groovy “standard”types can be extended OPEN CLASSES DSL’s Integer.metaClass.randomTimes = { Closure cls-> def randomValue = (new Random().nextInt(delegate)) +1 randomValue.times(cls) } delegate has the Integer’s value
  • 66.
    ▸ Groovy “standard”types can be extended OPEN CLASSES DSL’s Integer.metaClass.randomTimes = { Closure cls-> def randomValue = (new Random().nextInt(delegate)) +1 randomValue.times(cls) } Repeat a random number of times the closure
  • 67.
    ▸ Groovy “standard”types can be extended OPEN CLASSES DSL’s Integer.metaClass.randomTimes = { Closure cls-> def randomValue = (new Random().nextInt(delegate)) +1 randomValue.times(cls) } 10.randomTimes { println "x" }
  • 68.
    ▸ Allows usto create nice DSL’s OPEN CLASSES DSL’s def order = buy 10.bottles of "milk"
  • 69.
    ▸ Allows usto create nice DSL’s OPEN CLASSES DSL’s def order = buy 10.bottles of "milk" Integer.metaClass.getBottles = { return new Quantity(quantity: delegate, ontainer: "bottle") }
  • 70.
  • 71.
    ▸ Problem: Thelanguage isn’t flexible enough for your taste AST DSL’s class MathSpec extends Specification { def "maximum of two numbers"() { expect: Math.max(a, b) == c where: a | b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 } }
  • 72.
    ▸ Problem: Thelanguage isn’t flexible enough for your taste AST DSL’s class MathSpec extends Specification { def "maximum of two numbers"() { expect: Math.max(a, b) == c where: a | b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 } } What???!!!!
  • 73.
    ▸ With AST’syou can modify the language on compile time ▸ BUT you have to respect the syntax AST DSL’s
  • 74.
    AST DSL’s where: a |b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 Bit-level OR Logical OR
  • 75.
    ▸ We cando the same AST DSL’s class Main { @SpockTable def getTable() { value1 | value2 | value3 || max 1 | 2 | 3 || 3 2 | 1 | 0 || 2 2 | 2 | 1 || 2 } public static void main(def args) { def tableData = new Main().getTable() assert tableData['value1'] == [1, 2, 2] } }
  • 76.
    ▸ We cando the same OPEN CLASSES DSL’s class Main { @SpockTable def getTable() { value1 | value2 | value3 || max 1 | 2 | 3 || 3 2 | 1 | 0 || 2 2 | 2 | 1 || 2 } public static void main(def args) { def tableData = new Main().getTable() assert tableData['value1'] == [1, 2, 2] } } Local AST
  • 77.
    ▸ What kindof transformation we want? AST DSL’s def getTable() { value1 | value2 | value3 || max 1 | 2 | 3 || 3 2 | 1 | 0 || 2 2 | 2 | 1 || 2 } def getTablePostAST() { [ value1 : [1, 2, 2], value2 : [2, 1, 2], value3 : [3, 0, 1], max : [3, 2, 2] ] }
  • 78.
  • 79.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) }
  • 80.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } Retrieves all the method statements
  • 81.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } The first will be the header of our table
  • 82.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } The rest will be the different values for the table body
  • 83.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } With this values we create new code for this method body
  • 84.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } Delete all the old one
  • 85.
    ▸ Have toconvert from one AST to the other AST DSL’s void visit(ASTNode[] nodes, SourceUnit sourceUnit) { MethodNode method = (MethodNode) nodes[1] def existingStatements = ((BlockStatement)method.code).statements def headers = processTableHeaders(existingStatements[0]) def mapToSet = processTableBody(headers, existingStatements[1..-1]) def mapExpression = createMapStatement(mapToSet) existingStatements.clear() existingStatements.add(mapExpression) } Replace with the new code
  • 86.
    ▸ Try yourDSL syntax on groovyConsole ▸ Check the “source” AST and the “target” AST ▸ Think about how to convert from one to another AST DSL’s
  • 87.
  • 88.
  • 89.
    ▸ All thesetechniques with external scripts SCRIPTING DSL’s apply plugin: 'groovy' sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenLocal() jcenter() } dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.1' testCompile 'junit:junit:4.11' }
  • 90.
    ▸ All thesetechniques with external scripts SCRIPTING DSL’s apply plugin: 'groovy' sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenLocal() jcenter() } dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.1' testCompile 'junit:junit:4.11' } Properties Method calls
  • 91.
    ▸ Script bindingto a map SCRIPTING DSL’s def binding = new Binding( apply: { Map args -> println args}, repositories: { Closure dsl -> println "repositories"}, dependencies: { Closure dsl -> println "dependencies" } ) def shell = new GroovyShell(binding) shell.evaluate(new File("build.gradle"))
  • 92.
    ▸ We wanta state for these methods SCRIPTING DSL’s class MyGradle { void apply(Map toApply) { ... } void repositories(Closure dslRepositories) { ... } void dependencies(Closure dslDependencies) { ... } }
  • 93.
    ▸ Script bindingto an object SCRIPTING DSL’s def configuration = new CompilerConfiguration() configuration.setScriptBaseClass(DelegatingScript.class.getName()) def shell = new GroovyShell(new Binding(),configuration) def script = shell.parse(new File("build.gradle")) script.setDelegate(new MyGradle()) script.run()
  • 94.
    ▸ Script bindingto an object SCRIPTING DSL’s def configuration = new CompilerConfiguration() configuration.setScriptBaseClass(DelegatingScript.class.getName()) def shell = new GroovyShell(new Binding(),configuration) def script = shell.parse(new File("build.gradle")) script.setDelegate(new MyGradle()) script.run() Type of Script
  • 95.
    ▸ Script bindingto an object SCRIPTING DSL’s def configuration = new CompilerConfiguration() configuration.setScriptBaseClass(DelegatingScript.class.getName()) def shell = new GroovyShell(new Binding(),configuration) def script = shell.parse(new File("build.gradle")) script.setDelegate(new MyGradle()) script.run() Set our delegate
  • 96.
    ▸ Default imports SCRIPTINGDSL’s def configuration = new CompilerConfiguration() def imports = new ImportCustomizer() imports.addStaticStar('java.util.Calendar') configuration.addCompilationCustomizers(imports)
  • 97.
    ▸ Default imports SCRIPTINGDSL’s def configuration = new CompilerConfiguration() def imports = new ImportCustomizer() imports.addStaticStar('java.util.Calendar') configuration.addCompilationCustomizers(imports) import static from java.util.Calendar.*
  • 98.
    ▸ Apply ASTTransformations SCRIPTING DSL’s def configuration = new CompilerConfiguration() def ast = new ASTTransformationCustomizer(Log) configuration.addCompilationCustomizers(ast)
  • 99.
    ▸ Apply ASTTransformations SCRIPTING DSL’s def configuration = new CompilerConfiguration() def ast = new ASTTransformationCustomizer(Log) configuration.addCompilationCustomizers(ast) AST to apply inside the script
  • 100.
    ▸ Sanitize userinput SCRIPTING DSL’s def configuration = new CompilerConfiguration() def secure = new SecureASTCustomizer() secure.methodDefinitionAllowed = false configuration.addCompilationCustomizers(secure)
  • 101.
    ▸ Sanitize userinput SCRIPTING DSL’s def configuration = new CompilerConfiguration() def secure = new SecureASTCustomizer() secure.methodDefinitionAllowed = false configuration.addCompilationCustomizers(secure) We don’t allow method definitions in the script
  • 102.
    1. Closures 2. Builders 3.Open Classes 4. AST 5. Script
  • 103.
  • 104.