The main goal of this project is to explore basic features of groovy to produce specific DSL.
Reference: Groovy specification - core DSLs
Reference: Learning Groovy - Adam L. Davis
Reference: Groovy in Action
Reference: DSL - Martin Fowler
Domain-Specific Languages are small languages, focused on a particular
aspect of a software system. They allow business experts to read or write
code without having to be programming experts.
DSLs come in two main forms:
- external - language that's parsed independently of the host general purpose
language, examples:
regular expressions
andCSS
. - internal - particular form of
API
in a host general purpose language, often referred to as a fluent interface, examples:Spock
andMockito
.
Groovy
has many features that make it great for writing DSLs
:
- Closures with delegates.
- Parentheses and dots
(.)
are optional. - Ability to add methods to standard classes using Category, Mixins and Traits.
- The ability to overload operators.
- Metaprogramming:
methodMissing
andpropertyMissing
features.
Within Groovy
you can take a closure as a parameter and then call it using a
local variable as a delegate.
@ToString
class X {
String value
}
class Y {
static def handler(closure) {
X x = new X()
closure.delegate = x
closure()
x
}
}
println Y.handler {setValue 'test'} // X(test)
In Groovy
it's possible to omit parentheses and dots
X.resolve {take 10 plus 30 minus 15} // it's same as: new X().take(10).plus(30).minus(15)
where:
class X {
@Delegate
Integer value
Integer take(Integer x) {
x
}
static def resolve(Closure closure) {
closure.delegate = new X()
closure()
}
}
Groovy
categories are the mechanism to augment classes with new methods.
@Category(Integer)
class X {
def reverse() {
this.toString().reverse().replaceFirst(/^0+/,'').toInteger()
}
}
use(X) {
println 123000020.reverse() // 20000321
}
- Remarks:
- During compilation, all methods are transformed to static ones with
an additional self parameter of the type you supply as the annotation
parameter (the default type for the self parameters is
Object
which might be more broad reaching than you like so it is usually wise to specify a type). - Properties invoked using
'this'
references are transformed so that they are instead invoked on the additional self parameter and not on theCategory
instance. - Remember that once the category is applied, the reverse will occur and
we will be back to conceptually having methods on the
this
references again.
- During compilation, all methods are transformed to static ones with
an additional self parameter of the type you supply as the annotation
parameter (the default type for the self parameters is
Groovy
mixin is a mechanism to augment classes with new methods at runtime.
class X {
static def test(String x) {
println "test ${x}"
}
}
String.mixin X
'mixin'.test() // test mixin
- Remarks:
- Static mixins (
@Mixin
) have been deprecated in favour oftraits
. - Methods are only visible at runtime.
- Static mixins (
Traits
can be seen as interfaces carrying both default implementations
and state.
class Y implements X {
@Override
def name() {
return name
}
}
trait X {
def name = "X"
abstract def name()
def printName() {
println "test ${name()}"
}
}
new Y().printName() // X
- Remarks:
- Methods defined in a
trait
are visible in bytecode. - Internally, the
trait
is represented as an interface (without default methods) and several helper classes this means that an object implementing a trait effectively implements an interface. - Methods are visible from
Java
and they are compatible with type checking and static compilation.
- Methods defined in a
class ComplexNumber {
double x
double y
ComplexNumber plus(ComplexNumber other) {
new ComplexNumber(x: this.x + other.x, y: this.y + other.y)
}
}
ComplexNumber cn1 = new ComplexNumber(x: 1, y: 1)
ComplexNumber cn2 = new ComplexNumber(x: 2, y: 2)
ComplexNumber result = cn1 + cn2 // (3, 3)
}
Groovy
provides a way to implement functionality at runtime via the methods:
methodMissing(String name, args)
- invoked only in the case of a failed method dispatch when no method can be found for the given name and/or the given arguments.propertyMissing(String name)
- called only when no getter method for the given property can be found at runtime.propertyMissing(String name, Object value)
- called only when no setter method for the given property can be found at runtime.
class X {
def methodMissing(String name, args) {
println "methodMissing: $name $args"
}
def propertyMissing(String name, Object value) {
println "propertyMissing: $name $value"
}
def propertyMissing(String name) {
println "propertyMissing: $name"
}
}
def x = new X()
x.nonExsistingMethod "1", "2", "3" // methodMissing: nonExsistingMethod [1, 2, 3]
x.nonExsistingProperty // propertyMissing: nonExsistingProperty
x.settingNonExsistingProperty = 5 // "propertyMissing: settingNonExsistingProperty 5"
Elaborated above mechanisms used:
- closures with delegation:
def static make(@DelegatesTo(strategy = Closure.DELEGATE_ONLY, value = DeadlineMemo) Closure closure) { def code = closure.rehydrate(new DeadlineMemo(), this, this) code.resolveStrategy = Closure.DELEGATE_ONLY code() }
@DelegatesTo
- for type checkingclosure.rehydrate(new DeadlineMemo(), this, this)
- returns a copy of this closure for which thedelegate
,owner
andthisObject
are replaced with the supplied parameters.
- metaprogramming:
we dynamically add sections with custom names and bodiesdef methodMissing(String methodName, args) { def section = new ToDo(title: methodName, body: args[0]) toDo << section }
- optional parentheses:
DeadlineMemo.make { title 'IMPORTANT' deadline '2020-01-01' idea 'Be a better programmer!' plan 'Commit to github everyday!' xml }
We provide DSL to create memos and print them in specified format.
- Memos have structure:
- title
- deadline-date
- any number of arbitrary named sections that have title and body
- Supported formats:
- json
- text
- xml
- Exemplary memo looks like:
shopping-list 2018-06-16 food: butter, bread, meat cleaning supplies: washing powder
DeadlineMemo
,ToDo
- entities.json
/text
/xml
packages containing appropriate . convertersmemo -> specified format
.- Full tests.
To print memo in xml format:
println DeadlineMemo.make {
title 'any title you like'
deadline '2018-06-16'
subsection-title1 'any body you like'
subsection-title2 'any body you like'
xml
}
other formats: json
, xml
.
We provide tests for every format converter:
DeadlineMemoJsonConverterTest
DeadlineMemoTextConverterTest
DeadlineMemoXmlConverterTest
And we test DSL itself as well:
DslTest