A Domain Specific Language (DSL) is a programming language focused on a particular problem domain. DSLs provide expressive notations and abstractions for their domain through appropriate syntax and semantics. Groovy enables the creation of DSLs through features like closures, builders, open classes, and AST transformations that allow customizing and extending the language.
8. a Domain Specific Language
is a programming language that offers,
through appropriate notations and
abstractions, expressive power focused on a
particular problem domain.
9. a Domain Specific Language
is a programming language that offers,
through appropriate notations and
abstractions, expressive power focused on a
particular problem domain.
18. 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
}
}
38. 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 { }
41. ▸ 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!'
}
}
42. ▸ 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?
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 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() { }
}
48. ▸ 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()
}
49. ▸ 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
50. ▸ 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
51. ▸ 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
52. ▸ 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
53. ▸ All closure’s method/properties calls will
call a delegate
▸ Build around the delegate and then
retrieve the data
CLOSURE DSL’s
61. ▸ 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”, ...]
62. ▸ You can use the BuilderSupport when you
have complex tree-like structures
▸ Only have to create nodes and
relationships between them
BUILDER DSL’s
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 us to create nice DSL’s
OPEN CLASSES DSL’s
def order = buy 10.bottles of "milk"
69. ▸ 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")
}
71. ▸ 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
}
}
72. ▸ 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???!!!!
73. ▸ With AST’s you 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 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]
}
}
76. ▸ 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
79. ▸ 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)
}
80. ▸ 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
81. ▸ 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
82. ▸ 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
83. ▸ 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
84. ▸ 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
85. ▸ 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
86. ▸ 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
92. ▸ We want a state for these methods
SCRIPTING DSL’s
class MyGradle {
void apply(Map toApply) {
...
}
void repositories(Closure dslRepositories) {
...
}
void dependencies(Closure dslDependencies) {
...
}
}
93. ▸ 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()
94. ▸ 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
95. ▸ 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
96. ▸ Default imports
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def imports = new ImportCustomizer()
imports.addStaticStar('java.util.Calendar')
configuration.addCompilationCustomizers(imports)
97. ▸ 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.*
98. ▸ Apply AST Transformations
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def ast = new ASTTransformationCustomizer(Log)
configuration.addCompilationCustomizers(ast)
99. ▸ Apply AST Transformations
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def ast = new ASTTransformationCustomizer(Log)
configuration.addCompilationCustomizers(ast)
AST to apply inside the script
100. ▸ Sanitize user input
SCRIPTING DSL’s
def configuration = new CompilerConfiguration()
def secure = new SecureASTCustomizer()
secure.methodDefinitionAllowed = false
configuration.addCompilationCustomizers(secure)
101. ▸ 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