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.

DSL101

320 views

Published on

My slides of my talk DSL 101 on GR8conf EU 2018 in Copenhagen giving an insight in the options to write DSLs using Groovy.

Published in: Software
  • Be the first to comment

  • Be the first to like this

DSL101

  1. 1. DSL 101_ Alexander (Sascha) Klein 1
  2. 2. About me Alexander (Sascha) Klein Branchmanager Stuttgart codecentric AG Heßbrühlstr. 7 70565 Stuttgart +49 172 529 40 20 alexander.klein@codecentric.de www.codecentric.de blog.codecentric.de @saschaklein 2
  3. 3. What are DSLs? 3
  4. 4. Definition Domain Specific Language programming language to target a specific problem area at the same level of abstraction as the problem domain using a tongue related to the problem domain 4
  5. 5. Examples SQL - to create, edit and query relational database systems Ant-File - to define a build-process SELECT * FROM USERS; <project name="MyProject" default="dist" basedir="."> <description> simple example build file </description> <target name="init"> <tstamp/> <mkdir dir="${build}"/> </target> <target name="compile" depends="init" description="compile the source"> <javac srcdir="${src}" destdir="${build}"/> </target> </project> 5
  6. 6. Examples 2 CSS - to specify the look and feel of a webpage SwingBuilder - to define Swing-UI’s in a declarative way p.info { font-family: arial, sans-serif; line-height: 150%; } p.info span { font-weight: bold; } p.info span::after { content: ": "; } sb = new groovy.swing.SwingBuilder() frame = sb.frame(title: "Click", size: [200, 100], defaultCloseOperation: WindowConstants.EXIT_ON_CLOSE) { panel() { button ("Red", foreground: Color.RED, actionPerformed: {println "Red clicked"}) button ("Blue", foreground: Color.BLUE, actionPerformed: {println "Blue clicked"}) } } frame.visible = true 6
  7. 7. Types of DSL’s internal external non-textual eg. Scratch CC BY-SA 3.0 http://commons.wikimedia.org/wiki/File:Scratch_Hello_World.png 7
  8. 8. Why DSL’s expressive and concise syntax increases readability through higher abstraction level simplify development with different skill levels integration of business domain members into development 8
  9. 9. Creating DSLs 9
  10. 10. Scopes of DSLs Command Closure @groovy.transform.builder.Builder class User { String name int age } println User.builder().name('Sascha Klein').age(43).build() def user = new User() Closure dsl = { name = 'Sascha Klein' age = 43 } dsl.resolveStrategy = Closure.DELEGATE_FIRST dsl.delegate = user dsl() println user 10
  11. 11. Scopes of DSLs 2 Script Script myscript.groovy Custom ScriptBase Usage @groovy.transform.BaseScript UserBaseScript script name 'Sascha Klein' age 43 abstract class UserBaseScript extends Script { @Delegate User user = new User() } def cl = new GroovyClassLoader() Script script = cl.parseClass(new File('myscript.groovy')).newInstance() script.run() println script.user 11
  12. 12. Scopes of DSLs 3 alternative way: Script myscript.groovy Usage name 'Sascha Klein' age 43 import org.codehaus.groovy.control.CompilerConfiguration def config = new CompilerConfiguration(scriptBaseClass: 'UserBaseScript') def cl = new GroovyClassLoader(getClass().classLoader, config) Script script = cl.parseClass(new File('myscript.groovy')).newInstance() script.run() println script.user 12
  13. 13. Script binding undefined variables access the Script’s binding 'injecting' variables and functions myscript.groovy myVar = 100 println myVar // 100 Binding binding = new Binding() binding.setVariable('myVar', 50) binding.setVariable('myFunc', { it.toUpperCase() }) Script script = cl.parseClass(new File('myscript.groovy')).newInstance() script.setBinding(binding) script.run() name myFunc('Sascha Klein') age myVar 13
  14. 14. Command chains turn left then right turn(left).then(right) take 2.pills of aspirin after 6.hours take(2.pills).of(aspirin).after(6.hours) check that: margarita tastes good check(that: margarita).tastes(good) given {} when {} then {} given({}).when({}).then({}) 14
  15. 15. Command chains 2 please show the square_root of 100 please(show).the(square_root).of(100) show = { println it } square_root = { Math.sqrt(it) } def please(action) { new Object() { Map<String, Closure> the(Closure what) { } [of: { n -> action(what(n)) }] } } 15
  16. 16. Command chains 3 split "a ,_b ,c__" on ',' trimming '_' split("a ,_b ,c__").on(',').trimming('_') @Grab('com.google.guava:guava:r09') import com.google.common.base.* Splitter.on(',') .trimResults(CharMatcher.is('_' as char)) .split("_a ,_b_ ,c__") .iterator() .toList() @Grab('com.google.guava:guava:r09') import com.google.common.base.* def split(string) { [on: { separator -> [trimming: { trimChar -> Splitter.on(separator) .trimResults(CharMatcher.is(trimChar as char)) .split(string) .iterator() .toList() }] }] } 16
  17. 17. Method and Property access Collect all key, value pairs in a Map: Intercept all method calls Caution: methodMissing will be called from invokeMethod name 'Sascha' age 43 myProperty 1.2345 abstract class MyBaseScript extends Script { Map data = [:] def methodMissing(String name, args) { data[name] = args } } class Interception implements GroovyInterceptable { def invokeMethod(String name, Object args) { ... } } 17
  18. 18. Method and Property access 2 Collect all key, value pairs in a Map: Intercept all property access Caution: propertyMissing will be called from get/setProperty name = 'Sascha' age = 43 myProperty = 1.2345 abstract class MyBaseScript extends Script { Map data = [:] def propertyMissing(String name, value) { data[name] = value } def propertyMissing(String name) { return data[name] } } def getProperty(String name) { ... } void setProperty(String name, value) { ... } 18
  19. 19. MetaClasses 6.pills calls 6.getPills() You have to add getPills to the class Integer All classes have a MetaClass It manages all properties and methods of a class static and dynamic All calls will be redirected through the MetaClass 19
  20. 20. Usage of MetaClasses adding a method to an existing class adding a property Integer.metaClass.getPills = {-> [new Pill()] * delegate } assert 3.getPills().every{ it instanceof Pill } def storage = Collections.synchronizedMap([:]) String.metaClass.getName = {-> return storage[System.identityHashCode(delegate) + "name"] } String.metaClass.setName = { String name -> storage[System.identityHashCode(delegate) + "name"] = name } def test = "Test" test.name = "Sascha" assert test.name == "Sascha" assert ''.name == null 20
  21. 21. Operator overloading order icecream & suncream when temperature > 20 order(icecream & suncream).when(temperature > 20) def suncream = "suncream" def icecream = "icecream" def temperature = 30 String.metaClass.and = {a -> delegate + " and "+ a} def order = { what -> [ when: { condition -> println condition ? what : "nothing to order" } ] } 21
  22. 22. Operator overloading 2 Operator Method Operator Method a + b a.plus(b) -a a.negative() a - b a.minus(b) +a a.positive() a * b a.multiply(b) a++, ++a a.next(b) a / b a.div(b) a--, --a a.previous(b) a % b a.mod(b) a == b a.equals(b) a | b a.or(b) a != b !a.equals(b) a < b a.compareTo(b)<0 a > b a.compareTo(b)>0 a <= b a.compareTo(b)<=0 a >= b a.compareTo(b)>=0 a[b] a.getAt(b) a[b] = c a.setAt(b, c) ~a a.bitwiseNegate(b) a**b a.power(b) a in b a.isCase(b) a as b a.asType() a <=> b a.compareTo(b) 22
  23. 23. Extension Modules StringHelper.groovy META-INF/services/org.codehaus.groovy.runtime.ExtensionModule To use multiple extensionClasses, separate them with commas class StringHelper { static String low(String string) { return string.toLowerCase() } } moduleName = my-module moduleVersion = 1.0 extensionClasses = StringHelper assert "test" == "TeSt".low() 23
  24. 24. AST transformations AST = abstract syntax tree AST transformation manipulating AST to add or modify classes at compiletime local: triggered by an annotation global: always active when registered most flexible instrument enables syntaxes otherwise not possible can be used to change the result from ground up changes get 'melted' into byte code grants best IDE support 24
  25. 25. AST transformations 2 local AST-transformations Annotation astNodes contains AnnotationNode triggeringAnnotation Node annotatedNode @Retention (RetentionPolicy.SOURCE) @Target ([ElementType.METHOD]) @GroovyASTTransformationClass ("MyTransformation") public @interface MyAnnotation { String value() default "this is the default value" } package my.pkg @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) class MyTransformation implements ASTTransformation { void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { ... } } class MyClass { @MyAnnotation void doit() { ... } } 25
  26. 26. AST transformations 3 global AST-transformations ignore astNodes parameter they have to be part of the classpath and have to be registered META-INF/services/org.codehaus.groovy.transform.ASTTransformation Groovy 2.5+: Macros simplify AST transformation creation my.pkg.MyTransformation my.pkg.MySecondTransformation 26
  27. 27. Using DSLs 27
  28. 28. Embedding DSLs GroovyShell using CompilationConfiguration Binding binding = new Binding() binding.setVariable("foo", 2) GroovyShell shell = new GroovyShell(binding) Object value = shell.evaluate(new File('my.dsl')) assert value == 20 assert binding.getVariable("x") == 123 ... import org.codehaus.groovy.control.CompilerConfiguration def config = new CompilerConfiguration(scriptBaseClass: 'MyBaseScript') GroovyShell shell = new GroovyShell(config, binding) ... 28
  29. 29. Embedding DSLs 2 GroovyClassLoader using CompilationConfiguration def cl = new GroovyClassLoader() Script script = cl.parseClass(new File('my.dsl')).newInstance() script.run() println script.user import org.codehaus.groovy.control.CompilerConfiguration def config = new CompilerConfiguration(scriptBaseClass: 'MyBaseScript') def cl = new GroovyClassLoader(getClass().classLoader, config) ... 29
  30. 30. Embedding DSLs 3 GroovyScriptEngine using CompilationConfiguration import groovy.lang.Binding import groovy.util.GroovyScriptEngine String[] roots = [ "/my/groovy/script/path", "/my/other/script/path" ] GroovyScriptEngine gse = new GroovyScriptEngine(roots) Binding binding = new Binding() binding.setVariable("foo", 2) gse.run("mydsl.groovy", binding) ... import org.codehaus.groovy.control.CompilerConfiguration def config = new CompilerConfiguration(scriptBaseClass: 'MyBaseScript') GroovyScriptEngine gse = new GroovyScriptEngine(roots) gs.config = config ... 30
  31. 31. CompilerConfiguration ScriptBaseClass ASTTransformationCustomizer Default imports import org.codehaus.groovy.control.CompilerConfiguration import groovy.util.logging.Log def config = new CompilerConfiguration() config.addCompilationCustomizers(new ASTTransformationCustomizer(Log)) def config = new CompilerConfiguration() def imports = new ImportCustomizer() imports.addImport('my.pkg.MyClass') imports.addStarImports('java.time') config.addCompilationCustomizers(imports) 31
  32. 32. CompilerConfiguration 2 SecureASTCustomizer SourceAwareCustomizer def secureAstCust = new SecureASTCustomizer() secureAstCust.methodDefinitionAllowed = false secureAstCust.statementsBlacklist = [SwitchStatement, AssertStatement] def variableNames = { expr -> if (expr instanceof VariableExpression) { ! expr.variable[0] in ['_', '$'] } else { return true } } as ExpressionChecker secureAstCust.addExpressionCheckers(variableNames) config.addCompilationCustomizers(secureAstCust) def imports = new ImportCustomizer() imports.addImport('my.pkg.MyClass') def sac = new SourceAwareCustomizer(imports) sac.extensionValidator = { ext -> ext == 'dsl' } config.addCompilationCustomizers(sac) 32
  33. 33. CompilerConfiguration 3 CompilerCustomizationBuilder CompilerCustomizationBuilder.withConfig(configuration) { ast(groovy.util.logging.Log) ast(includeNames: true, groovy.transform.ToString) imports { normal 'my.pkg.MyClass' star 'java.time' } def variableNames = { expr -> if (expr instanceof VariableExpression) { ! expr.variable[0] in ['_', '$'] } else { return true } } as ExpressionChecker secureAst { addExpressionCheckers variableNames methodDefinitionAllowed false statementsBlacklist = [SwitchStatement, AssertStatement] } source(extensions: ['dsl']) { imports { normal 'my.pkg.MyClass' } } } 33
  34. 34. Compiler Configuration Script config.groovy build.gradle import org.codehaus.groovy.control.customizers.ImportCustomizer def imports = new ImportCustomizer() imports.addImports('org.some.*') configuration.addCompilationCustomizers(imports) >> groovyc --configscript pathto/config.groovy compileGroovy.groovyOptions.configurationScript=file('pathto/config.groovy') 34
  35. 35. Best practices 35
  36. 36. How to design a DSL minimalistic expose only the behaviour needed distilled remove all nonessential details extensibel designed in a way extensions have minimal impact on users 36
  37. 37. Syntax checking ExpressionCheckers can be mighty, but complex @TypeChecked / @CompileStatic most dynamic features will fail you have to help the compiler 37
  38. 38. @DelegatesTo email { from 'dsl-guru@mycompany.com' to 'john.doe@waitaminute.com' subject 'The pope has resigned!' body 'Really, the pope has resigned!' } class EmailSpec { void from(String from) { println "From: $from"} void to(String... to) { println "To: $to"} void subject(String subject) { println "Subject: $subject"} void body(Strig body) { println body } } def email(Closure cl) { def email = new EmailSpec() def code = cl.clone() code.delegate = email code.resolveStrategy = Closure.DELEGATE_ONLY return code() } 38
  39. 39. @DelegatesTo 2 compilation fails specification of email-Closure unknown to parser Now the compilation is successful @groovy.transform.TypeChecked // or @groovy.transform.CompileStatic void sendEmail() { email { from 'dsl-guru@mycompany.com' to 'john.doe@waitaminute.com' subject 'The pope has resigned!' body 'Really, the pope has resigned!' } } def email(@DelegatesTo(EmailSpec) Closure cl) { ... } def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) { ... } 39
  40. 40. @DelegatesTo 3 Delegate to a parameter def exec(Object target, Closure code) { def clone = code.clone() clone.delegate = target return clone() } class Greeter { void sayHello() { println 'Hello' } } def greeter = new Greeter() exec(greeter) { sayHello() } def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) { def clone = code.rehydrate(target, this, this) clone() } 40
  41. 41. @DelegatesTo 4 Delegate to a generic type For other cases write a @TypeChecked extension public <T> void configure(List<T> elements, Closure configuration) { elements.each { T element -> def clone = configuration.rehydrate(element, this, this) clone.resolveStrategy = Closure.DELEGATE_FIRST clone.call() } } @groovy.transform.ToString class Realm { String name } List<Realm> list = [new Realm()] * 3 configure(list) { name = 'My Realm' } assert list.every { it.name == 'My Realm' } public <T> void configure( @DelegatesTo.Target List<T> elements, @DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) { ... } http://docs.groovy-lang.org/next/html/documentation/type-checking- 41
  42. 42. Execution control @groovy.transform.ThreadInterrupt adds thread interruption checks after loops (for, while) first instruction of a method first instruction of a closure body @groovy.transform.TimedInterrupt interrupts after the given time @groovy.transform.ConditionalInterrupt interrupts after the given closure returns true 42
  43. 43. Dependency management Grape GRoovy Adaptable Packaging Engine Addable via ASTTransformationCustomizer @Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate @Grab('org.springframework:spring-orm:3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate @Grapes([ @Grab(group='commons-primitives', module='commons-primitives', version='1.0'), @Grab(group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7') ]) 43
  44. 44. Errorhandling Give detailed information clear message file, line GroovyShell shell = new GroovyShell() try { Object value = shell.evaluate ''' prinln 123 ''' } catch(ex) { def ste = ex.stackTrace.find { !(it.declaringClass.startsWith("org.codehaus.groovy.") || it.declaringClass.startsWith("groovy.") || it.declaringClass.startsWith("java.")) } println "Error at $ste.fileName(line $ste.lineNumber): $ex.message" } 44
  45. 45. IDE Support IntelliJ IDEA supports @DelegatesTo @BaseScript GroovyDSL Script: .gdsl abstract class UserBaseScript extends Script { @Lazy User user = { new User() }() def methodMissing(String name, args) { user."$name" = args } } @groovy.transform.BaseScript UserBaseScript script name 'Sascha Klein' age 43 http://www.tothenew.com/blog/gdsl-awesomeness-introduction-to-gdsl-in- intellij-idea/ 45
  46. 46. Questions? Alexander (Sascha) Klein Branchmanager Stuttgart codecentric AG Heßbrühlstr. 7 70565 Stuttgart +49 172 529 40 20 alexander.klein@codecentric.de www.codecentric.de blog.codecentric.de @saschaklein 46

×