Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

(Greach 2015) Dsl'ing your Groovy

3,365 views

Published on

Video: https://www.youtube.com/watch?v=SfrEThI_m7g
Source code: https://github.com/Alotor/2015-greach-groovy-dsls

Behind each good Groovy library or framework there is a good DSL (Domain Specific Language). And this is not by chance, one of the most exciting features of Groovy is its amazing syntax flexibility and metaprogramming capabilities that allow us do things in a highly expressive manner through DSLs.

In this talk I’ll explain the basics of doing DLS’s with Groovy. What you’ll need to start and what to investigate deeper. Also, we’ll check some of the most well known ones libraries like Spock, Gradle or Grails so you can use their techniques in your own Groovy projects.

Published in: Technology
  • DOWNLOAD FULL eBOOK INTO AVAILABLE FORMAT ......................................................................................................................... ......................................................................................................................... 1.DOWNLOAD FULL. PDF eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... 1.DOWNLOAD FULL. doc eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... 1.DOWNLOAD FULL. PDF eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... 1.DOWNLOAD FULL. doc eBook here { https://tinyurl.com/y3nhqquc } ......................................................................................................................... ......................................................................................................................... ......................................................................................................................... .............. Browse by Genre Available eBooks ......................................................................................................................... Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, CookeBOOK Crime, eeBOOK Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult,
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Source: https://github.com/Alotor/2015-greach-groovy-dsls
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

(Greach 2015) Dsl'ing your Groovy

  1. 1. DSL’ing YOUR @alotor
  2. 2. @alotor @alotor Alonso Torres
  3. 3. Domain-specific Languages
  4. 4. a Domain Specific Language is a programming language that offers, through appropriate notations and abstractions, expressive power focused on a particular problem domain.
  5. 5. a Domain Specific Language is a programming language that offers, through appropriate notations and abstractions, expressive power focused on a particular problem domain.
  6. 6. Expressive abstractions and notations for a particular problem
  7. 7. A code snippet is worth a thousand images
  8. 8. Configuration
  9. 9. 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', }
  10. 10. 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 } }
  11. 11. Expressive API
  12. 12. def results = Account.createCriteria() { between "balance", 500, 1000 eq "branch", "London" or { like "holderFirstName", "Fred%" like "holderFirstName", "Barney%" } maxResults 10 order "holderLastName", "desc" }
  13. 13. Specific notations
  14. 14. 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 } }
  15. 15. User’s input
  16. 16. 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' }
  17. 17. How cool is that?
  18. 18. But only “them” can do those things
  19. 19. 1. Closures 2. Builders 3. Open Classes 4. AST 5. Script TOC
  20. 20. 0. Groovy “sugar”
  21. 21. ▸ Optional parentheses GROOVY NICETIES
  22. 22. 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') })
  23. 23. ▸ Optional parentheses ▸ Getter / setters GROOVY NICETIES
  24. 24. sourceCompatibility = 1.8 targetCompatibility = 1.8 void setSourceCompatibility(version) { ... } void setTargetCompatibility(version) { ... }
  25. 25. def sourceVersion = script.sourceCompatibility def targetVersion = script.targetCompatibility def getSourceCompatibility() { ... } def getTargetCompatibility() { ... }
  26. 26. ▸ Optional parentheses ▸ Getter / setters ▸ Operator overloading GROOVY NICETIES
  27. 27. 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()
  28. 28. ▸ Optional parentheses ▸ Getter / setters ▸ Operator overloading ▸ Keyword arguments GROOVY NICETIES
  29. 29. def myKeyArgs(Map keyargs=[:], String value1, String value2) { ... } myKeyArgs("value1", "value2") myKeyArgs("value1", "value2", cache: true) myKeyArgs("value1", "value2", drop: 20, take: 50)
  30. 30. ▸ Optional parentheses ▸ Getter / setters ▸ Operator overloading ▸ Keyword arguments ▸ Closure arguments GROOVY NICETIES
  31. 31. def myClosureArg(String value1, String value2, Closure cls=null) { ... } myClosureArg("value1", "value2") myClosureArg("value1", "value2") { println ">> Calling inside closure" }
  32. 32. ▸ Optional parentheses ▸ Getter / setters ▸ Operator overloading ▸ Keyword arguments ▸ Closure arguments ▸ Command chaining GROOVY NICETIES
  33. 33. 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 { }
  34. 34. Now, let’s talk business
  35. 35. 1. Closure DSL’s
  36. 36. ▸ 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!' } }
  37. 37. ▸ 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?
  38. 38. ▸ this ▸ owner ▸ delegate GROOVY CLOSURES CONTEXT Three objects handle the closure context
  39. 39. ▸ this ▸ owner ▸ delegate GROOVY CLOSURES CONTEXT Normaly handles the context (default)
  40. 40. ▸ this ▸ owner ▸ delegate GROOVY CLOSURES CONTEXT Only changes for nested closures
  41. 41. ▸ this ▸ owner ▸ delegate GROOVY CLOSURES CONTEXT Can be changed!
  42. 42. ▸ 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() { } }
  43. 43. ▸ 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() }
  44. 44. ▸ 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
  45. 45. ▸ 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
  46. 46. ▸ 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
  47. 47. ▸ 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
  48. 48. ▸ All closure’s method/properties calls will call a delegate ▸ Build around the delegate and then retrieve the data CLOSURE DSL’s
  49. 49. 2. Groovy Builders
  50. 50. ▸ 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" } } } } } }
  51. 51. ▸ 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
  52. 52. ▸ Groovy provides support for this type of DSL ▸ groovy.util.BuilderSupport BUILDER DSL’s
  53. 53. ▸ 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) } ... }
  54. 54. ▸ 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
  55. 55. ▸ 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
  56. 56. ▸ 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”, ...]
  57. 57. ▸ You can use the BuilderSupport when you have complex tree-like structures ▸ Only have to create nodes and relationships between them BUILDER DSL’s
  58. 58. 3. Open Classes
  59. 59. ▸ 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
  60. 60. ▸ 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
  61. 61. ▸ 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
  62. 62. ▸ 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" }
  63. 63. ▸ Allows us to create nice DSL’s OPEN CLASSES DSL’s def order = buy 10.bottles of "milk"
  64. 64. ▸ 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") }
  65. 65. 4. AST Transformations
  66. 66. ▸ 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 } }
  67. 67. ▸ 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???!!!!
  68. 68. ▸ With AST’s you can modify the language on compile time ▸ BUT you have to respect the syntax AST DSL’s
  69. 69. AST DSL’s where: a | b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 Bit-level OR Logical OR
  70. 70. ▸ 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] } }
  71. 71. ▸ 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
  72. 72. ▸ 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] ] }
  73. 73. AST DSL’s
  74. 74. ▸ 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) }
  75. 75. ▸ 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
  76. 76. ▸ 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
  77. 77. ▸ 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
  78. 78. ▸ 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
  79. 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) } Delete all the old one
  80. 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) } Replace with the new code
  81. 81. ▸ 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
  82. 82. No magic involved ;-)
  83. 83. 5. Scripting
  84. 84. ▸ 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' }
  85. 85. ▸ 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
  86. 86. ▸ 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"))
  87. 87. ▸ We want a state for these methods SCRIPTING DSL’s class MyGradle { void apply(Map toApply) { ... } void repositories(Closure dslRepositories) { ... } void dependencies(Closure dslDependencies) { ... } }
  88. 88. ▸ 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()
  89. 89. ▸ 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
  90. 90. ▸ 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
  91. 91. ▸ Default imports SCRIPTING DSL’s def configuration = new CompilerConfiguration() def imports = new ImportCustomizer() imports.addStaticStar('java.util.Calendar') configuration.addCompilationCustomizers(imports)
  92. 92. ▸ 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.*
  93. 93. ▸ Apply AST Transformations SCRIPTING DSL’s def configuration = new CompilerConfiguration() def ast = new ASTTransformationCustomizer(Log) configuration.addCompilationCustomizers(ast)
  94. 94. ▸ Apply AST Transformations SCRIPTING DSL’s def configuration = new CompilerConfiguration() def ast = new ASTTransformationCustomizer(Log) configuration.addCompilationCustomizers(ast) AST to apply inside the script
  95. 95. ▸ Sanitize user input SCRIPTING DSL’s def configuration = new CompilerConfiguration() def secure = new SecureASTCustomizer() secure.methodDefinitionAllowed = false configuration.addCompilationCustomizers(secure)
  96. 96. ▸ 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
  97. 97. 1. Closures 2. Builders 3. Open Classes 4. AST 5. Script
  98. 98. Go ahead! DSL your Groovy
  99. 99. @alotor @alotor THANKS!

×