DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Why "Polyglot Programming" or "Do It Yourself Programming Languages" or "Language Oriented Programming" sucks?
  • Apache Doris vs Elasticsearch: An In-Depth Comparative Analysis
  • Doris vs Elasticsearch: A Comparison and Practical Cost Case Study
  • Finally, an ORM That Matches Modern Architectural Patterns!

Trending

  • Integrating Security as Code: A Necessity for DevSecOps
  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • Event-Driven Architectures: Designing Scalable and Resilient Cloud Solutions
  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  1. DZone
  2. Coding
  3. Languages
  4. Creating a Groovy DSL for Structurizr

Creating a Groovy DSL for Structurizr

Groovy DSLs are easy to make, and in this example, we'll create one for the Structurizr documentation generator, showing off Groovy's Closure class along the way.

By 
Grzegorz Ziemoński user avatar
Grzegorz Ziemoński
·
May. 24, 17 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
8.1K Views

Join the DZone community and get the full member experience.

Join For Free

In the previous post, we took a quick look into generating documentation with Structurizr. I really enjoyed playing with the tool, but I wasn’t aesthetically pleased with the code necessary to create a simple diagram. Well, seems like a perfect chance to introduce you to creating Groovy DSLs and produce something useful at the same time.

What Are We Going to Do?

We’re going to start from the very end so that you know what we’re aiming for and better understand the things that I’m going to explain. One of the code samples in the previous post looked like this:

public class HelloWorld {
    private static final String API_KEY = "your-api-key";
    private static final String API_SECRET = "your-api-secret";
    private static final int WORKSPACE_ID = 0; // your workspace ID

    public static void main(String[] args) throws Exception {
        Workspace workspace = new Workspace("My First Workspace", "I'm using this to learn about Structurizr");
        Model model = workspace.getModel();
        ViewSet viewSet = workspace.getViews();

        Person me = model.addPerson("Me", "Myself.");
        SoftwareSystem world = model.addSoftwareSystem("World", "Earth, to be precise.");

        me.uses(world, "Hello, World!");

        viewSet.createSystemContextView(world, "My First View", "Just me and the world.").addAllElements();

        StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET);
        structurizrClient.putWorkspace(WORKSPACE_ID, workspace);
    }
}


As you can see, there are quite a lot of objects and non-fluent method calls involved. With a little help from Groovy, we will be able to replace the code above with something like this:

class HelloWorld {
    private static final String API_KEY = "your-api-key"
    private static final String API_SECRET = "your-api-secret"
    private static final int WORKSPACE_ID = 0 // your workspace ID

    static void main(String[] args) {
        def ws = Structurizr.workspace {
            name "My First Workspace"
            description "I'm using this to learn about Structurizr"

            softwareSystem {
                name "World"
                description "Earth, to be precise."
            }

            person {
                name "Me"
                description "Myself."
                uses {
                    softwareSystem "World"
                    description "Hello, World!"
                }
            }

            systemContextView {
                softwareSystem "World"
                key "My First View"
                description "Just me and the world."
            }
        }

        StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET);
        structurizrClient.putWorkspace(WORKSPACE_ID, ws);
    }
}


This version is a bit longer, but that’s the price I’m willing to pay for more expressiveness and readability. Let’s do this!

Introduction to Groovy DSLs

If you don’t know Groovy well enough, the code above might seem like some sort of dark magic. But don’t worry, it should seem super easy in just a moment.

Optional Parenthesis

The first thing that you need to know in order to understand the code above is that Groovy allows you to skip parenthesis in method calls. It means that the lines...

name "My First Workspace"
description "I'm using this to learn about Structurizr"


...are equivalent in Groovy to:

name("My First Workspace")
description("I'm using this to learn about Structurizr")

Closures

The second thing that you should know about Groovy is the Closure class and the related syntax. A closure in Groovy is like a pimped out version of the Java 8 lambda expression. Similarly to Java’s lambda expressions, to define a closure, we need to use the curly braces. The difference between the two is that closure’s parameters are specified inside the braces, instead of outside. When there are no arguments, you just skip the argument name and the “arrow”.

// Java 8:
arg -> {
    // do stuff
    return "some value"
}

() -> {
    // do stuff
    return "some value"
}

// Groovy:
{ arg ->
    // do stuff
    return "some value"
}

{
    // do stuff
    return "some value"
}

Optional Parenthesis + Closures

When you combine the previous two sections together, you have the answer how the softwareSystem, person, and systemContextView methods work. These methods simply take a single Closure parameter and, by omitting the parenthesis, we get a nice Config-ish syntax:

void take(Closure c) {
    c()
}

take {
    println "I'm a closure!"
}

// prints: I'm a closure!

Closure Delegation

The last piece required to create a Groovy DSL like the one above is closure delegation. By default, you are allowed to use in a closure all the methods that are available in its outer scope (AKA the owner).

void take(Closure c) {
    c()
}

void talk() {
    println("I'm a closure!")
}

take {
    talk()
}

// prints: I'm a closure!


For DSL purposes, the Closure class has a delegate parameter that allows us to extend the pool of available methods with methods of an arbitrary object. An example might do a better job explaining this than words:

class Talker {
    void talk() {
        println("I'm a closure!")
    }
}

void take(Closure c) {
    c.delegate = new Talker()
    c()
}

take {
    talk()
}

// prints: I'm a closure!


If we were to convert the take method to an English conversation, it basically tells the closure: “Hey, if you can’t find a required method, try delegating to this Talker object.” And so it does delegate in our example.

Now, this is enough to get some basic delegation working, but the IDEs will get lost without a bit of help. How is the IDE supposed to know what kind of object are you going to delegate to, huh? Well, there’s an annotation for that.

class Talker {
    void talk() {
        println("I'm a closure!")
    }
}

void take(@DelegatesTo(Talker) Closure c) {
    c.delegate = new Talker()
    c()
}

take {
    talk()
}

// prints: I'm a closure!


With this annotation in place, the IDE correctly suggests using Talker’s methods inside the closure. At this point, it should be clear where the methods like person, name, description etc. are taken in subsequent closures of our HelloWorld example.

Now, I’d love to say that it’s enough to start writing the DSL itself, but there’s one more little thing. By default, the closure first looks for methods inside its outer scope and then asks the delegation object. This default behavior would be bad for our softwareSystem method, which has two different meanings in two different scopes.

void softwareSystem() {
    println "BUHAHAHA!"
}

class ViewConfiguration {
    void softwareSystem() {
        println "Expected behavior."
    }
}

void take(@DelegatesTo(ViewConfiguration) Closure c) {
    c.delegate = new ViewConfiguration()
    c()
}

take {
    softwareSystem()
}

// prints: BUHAHAHA!


Luckily, Groovy allows us to change this default behavior. We simply need to change the Closure’s resolveStrategy.

void softwareSystem() {
    println "BUHAHAHA!"
}

class ViewConfiguration {
    void softwareSystem() {
        println "Expected behavior."
    }
}

void take(@DelegatesTo(value = ViewConfiguration, strategy = DELEGATE_FIRST) Closure c) {
    c.delegate = new ViewConfiguration()
    c.resolveStrategy = DELEGATE_FIRST
    c()
}

take {
    softwareSystem()
}

// prints: Expected behavior.


Huh, that’s it! We’re ready to create an actual, useful DSL.

Groovy DSL for Structurizr

I trust that you’re a smart person and that I did a good job explaining in the previous section, so we’ll rush through the DSL implementation, rather than walking it step by step.

The entry point to our DSL is the Structurizr class. It has only one method, which initializes configuring a workspace. It is the same method that we used in the first line of our HelloWorld example’s main.

class Structurizr {

    static Workspace workspace(
            @DelegatesTo(value = WorkspaceConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
        def wc = new WorkspaceConfigurer()
        configurer.delegate = wc
        configurer.resolveStrategy = DELEGATE_FIRST
        configurer()
        return wc.apply()
    }
}


As you can see, we’re just delegating to a WorkspaceConfigurer here and expecting it to return a complete Workspace object afterward.

This “configurer” is just one of many similar classes, each responsible for configuring a single thing in the workspace. Each of them keeps necessary data and references to other related “configurers.” Our WorkspaceConfigurer currently looks like this:

class WorkspaceConfigurer {
    String name
    String description
    List<SoftwareSystemConfigurer> softwareSystems = []
    List<PersonConfigurer> people = []
    List<SystemContextViewConfigurer> systemContextViews = []

    Workspace apply() {
        def workspace = new Workspace(name, description)
        applyConfigurers(workspace)
        return workspace
    }

    void applyConfigurers(Workspace workspace) {
        softwareSystems.forEach { it.apply(workspace) }
        people.forEach { it.apply(workspace) }
        systemContextViews.each { it.apply(workspace) }
    }

    void name(String name) {
        this.name = name
    }

    void description(String description) {
        this.description = description
    }

    void softwareSystem(@DelegatesTo(value = SoftwareSystemConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
        def ssc = new SoftwareSystemConfigurer()
        configurer.delegate = ssc
        configurer.resolveStrategy = DELEGATE_FIRST
        configurer()
        softwareSystems.add(ssc)
    }

    void person(@DelegatesTo(value = PersonConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
        def pc = new PersonConfigurer()
        configurer.delegate = pc
        configurer.resolveStrategy = DELEGATE_FIRST
        configurer()
        people.add(pc)
    }

    void systemContextView(
            @DelegatesTo(value = SystemContextViewConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
        def scvc = new SystemContextViewConfigurer()
        configurer.delegate = scvc
        configurer.resolveStrategy = DELEGATE_FIRST
        configurer()
        systemContextViews.add(scvc)
    }
}


This piece of code might seem complicated at first, but actually, it’s really simple. We’ve got two “real” fields in the class: name and description. For these fields, we expose methods with the same names. For each of the elements that will be configured in a separate closure, we need to expose a delegating method and remember the configurer that we’re delegating towards. After all the configuration is done and apply is called, we simply set the name and description, and let the other configurers do their job.

Now, we could go through each of the configurers one by one and explain what they do, but there’s no point. They are really similar and we’d be wasting your precious time. If you want to see a more complete version of the DSL’s codebase, you can find it here.

Summary

Creating a configuration DSL in Groovy is a piece of cake. Once you grasp the concepts of Closures and delegation, all you need to do is simply bash the necessary methods through some helper (configurer) objects. As you can see, although there is some typing involved, you can easily create a nice DSL for the tools you like if they lack a friendly API. Mine was Structurizr. What’s yours?

Domain-Specific Language Groovy (programming language)

Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Why "Polyglot Programming" or "Do It Yourself Programming Languages" or "Language Oriented Programming" sucks?
  • Apache Doris vs Elasticsearch: An In-Depth Comparative Analysis
  • Doris vs Elasticsearch: A Comparison and Practical Cost Case Study
  • Finally, an ORM That Matches Modern Architectural Patterns!

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!