(Greach 2015) Dsl'ing your Groovy
-
Upload
alonso-torres -
Category
Technology
-
view
757 -
download
0
Transcript of (Greach 2015) Dsl'ing your Groovy
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.
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
}
}
def results = Account.createCriteria() {
between "balance", 500, 1000
eq "branch", "London"
or {
like "holderFirstName", "Fred%"
like "holderFirstName", "Barney%"
}
maxResults 10
order "holderLastName", "desc"
}
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 }}
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'
}
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')
})
sourceCompatibility = 1.8
targetCompatibility = 1.8
void setSourceCompatibility(version) {
...
}
void setTargetCompatibility(version) {
...
}
def sourceVersion = script.sourceCompatibility
def targetVersion = script.targetCompatibility
def getSourceCompatibility() {
...
}
def getTargetCompatibility() {
...
}
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 { }
▸ DSL inside a closure
CLOSURE DSL’s
emailService.send {
from '[email protected]'
to '[email protected]'
subject 'Check this video out!'
body {
p 'Really awesome!'
}
}
▸ DSL inside a closure
CLOSURE DSL’s
emailService.send {
from '[email protected]'
to '[email protected]'
subject 'Check this video out!'
body {
p 'Really awesome!'
}
}
Method invocation. Where are these methods?
▸ 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
▸ 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
▸ 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
▸ 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"
Integer.metaClass.getBottles = {
return new Quantity(quantity: delegate, ontainer: "bottle")
}
▸ 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
▸ 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]
]
}
▸ 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
▸ 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