(Greach 2015) Dsl'ing your Groovy

104
DSL’ing YOUR @alotor

Transcript of (Greach 2015) Dsl'ing your Groovy

DSL’ing YOUR

@alotor

@alotor @alotor

Alonso Torres

Domain-specificLanguages

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 athousand 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 '[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?

▸ 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!