Add Editor Support to Your Groovy Builder with a DSLD
In this article, the author will give an example on how to use the DSLD effectively and get great editor support.
Join the DZone community and get the full member experience.
Join For FreeIn a previous article, A Basic Builder with Groovy, I showed how to make a DSL with a Groovy Builder. The next thing you probably want to have is editor support. Groovy offers support for DSL's with a DSL Definition, which makes Groovy DSL's in Eclipse feel like they're really part of the language.
A DSLD consists of a single file and must be on the class path. I like to store it in a package called 'dsld'. A full overview of the DSLD syntax and semantics can be found at: DSL Descriptors for Groovy and Eclipse.
A DSLD contains contributions to the Groovy editor in Eclipse. Each contribution has a point cut that tells the editor where to apply the contribution. The point cuts operate on the abstract syntax tree of Groovy. This is an important point to note when figuring out why the point cut is applied to the wrong location.
Here I will give an example on how to use the DSLD effectively and get great editor support.
A DSLD for the CompanyBuilder
First, create a file named basic_builder.dsld in a package called dsld. I usually place that package in a source folder called resources, or src/main/resources. The Groovy editor will automatically pick up this file and will try to apply it. If you go to Windows and then Preferences in the Eclipse menu, and type DSLD, you will get an overview of all DSLD's loaded by the editor. You can select to edit, recompile, disable a specific DSLD or disable DSLD support entirely.
I like to divide the DSLD in four blocks:
- Root
- Root Objects
- Child Objects
- Attributes
The root is the first call on the builder, in our example it is 'company'. Root objects are anything directly below the root. Child objects are anything belonging to root objects. Attributes are calls tht don't create an object, but change attributes on an object. In our example we have two: role and name.
This way I keep a clear view of the hierarchy of the syntax. The first contribution is for the root method, which is called ëcompanyí and it must match on the builder. The root constructor in a builder is not enclosed in a closure, unlike every other constructor:
final String ROOT = 'company'
final String PROVIDER = 'CompanyBuilder'
final Map TYPE_PARAMS = [id:String]
final String BUILDER_TYPE = "builder.basic.CompanyBuilder"
final String COMPANY_TYPE = "builder.basic.model.Company"
// ---------------- ROOT
contribute (currentType(BUILDER_TYPE) & (~enclosingClosure())) {
method name: ROOT,
type:COMPANY_TYPE,
params:TYPE_PARAMS,
doc: '''Create a new company with departments, for example:
<pre>
builder.company('ABC') {
department('DEP1') {
department('DEP1.1') {
}
}
}
</pre>
''',
provider:PROVIDER
}
The keywords that make up the syntax are placed in the top of the file and are immutable. The method name tells the editor that 'company' should appear in the content assist of Eclipse when pressing CTRL+SPACE. The 'type' specifies the return type, the 'params' attribute specifies the list of parameter with their respective types and finally, the ëdocí section, is where you can place a HTML snippet with text and examples. The 'provider' attribute shows the name and type of the builder and the Javadoc that is added to the class definition of the builder. It will look like this:
In the builder in the previous section, all methods were declared public by default, as is usual with Groovy. I recommend to now declare them private ,so they show up differently in the content assist:
The company contribution we just created is now the only public method, marked with a green dot. This should be a clear message to developers to only use the company method at this point. It is possible in Groovy to call private methods, but when a method is declared private, this is a signal from the designer of that class to not use it outside the declaring class scope. If you go back to the example, then you'll find that the company constructor is not underlined anymore.
Second, root objects are added. These are constructors directly beneath the root. In the example, there is only one, namely the department. The department is declared in a closure, its declaring type is the CompanyBuilder. The department is enclosed in the scope of the company, but not in the scope of the employee node. This last step is necessary otherwise if a department is added to the employee, the content assist would also show it. You can still do it, but the builder will (should!) throw an error. The content assist and javadocs are merely a reference as to what is allowable and what not. The department contribution looks like follows:
//---------------- ROOT OBJECTS
contribute (enclosingClosure()
& enclosingCallDeclaringType(BUILDER_TYPE)
& enclosingCallName(ROOT)
& (~enclosingCallName(EMPLOYEE))) {
method name: DEPARTMENT,
type:DEPARTMENT_TYPE,
params:TYPE_PARAMS,
doc: '''Create a department with employees or other departments, for example:
<pre>
department('XYZ') {
employee('emp12345') {
}
}
</pre>
or:
<pre>
department('DEP1') {
department('DEP1.1') {
}
}
</pre>
''',
provider:PROVIDER
}
//---------------- /ROOT OBJECTS
Third, any child objects are added. The join points can become quite complex if the content assist must reflect the hierarchy of the builder. Groovy looks up the abstract syntax tree so if any match is made, content assist will show it. Therefore, I tell Groovy that the employee content assist must be shown if the parent is a department block, but not an employee block:
//---------------- CHILD OBJECTS
contribute (enclosingClosure()
& (enclosingCallName(DEPARTMENT)
& (~enclosingCallName(EMPLOYEE)) )) {
method name: EMPLOYEE,
type:EMPLOYEE_TYPE,
params:TYPE_PARAMS,
doc: '''Add an employee to a department, for example:
<pre>
employee('emp12345') {
name('John')
role('Administrator')
}
</pre>
''',
provider:PROVIDER
}
//---------------- /CHILD OBJECTS
Fourth, on the employee, we can set attributes. These are added last. Attributes may not be nested and may only be visible in an employee block. The technique is the same:
//---------------- ATTRIBUTES
contribute (enclosingClosure()
& enclosingCallName(EMPLOYEE)
& (~enclosingCallName(NAME))
& (~enclosingCallName(ROLE)) ) {
method name: NAME,
type:void,
params:TYPE_PARAMS,
doc: '''Set the name in the following format:
<pre>
name('John')
</pre>
''',
provider:PROVIDER
}
contribute (enclosingClosure()
& enclosingCallName(EMPLOYEE)
& (~enclosingCallName(NAME))
& (~enclosingCallName(ROLE)) ) {
method name: ROLE,
type:void,
params:TYPE_PARAMS,
doc: '''Set the role in the following format:
<pre>
role('Developer')
</pre>
''',
provider:PROVIDER
}
//---------------- /ATTRIBUTES
Now, you have editor support for your Groovy Builder based DSL. The code can be downloaded from GitHub: GroovyBuilder.
Opinions expressed by DZone contributors are their own.
Trending
-
Demystifying SPF Record Limitations
-
DevOps Midwest: A Community Event Full of DevSecOps Best Practices
-
Constructing Real-Time Analytics: Fundamental Components and Architectural Framework — Part 2
-
Low Code vs. Traditional Development: A Comprehensive Comparison
Comments