Search

Dark theme | Light theme
Showing posts with label DSL. Show all posts
Showing posts with label DSL. Show all posts

May 29, 2013

Gradle Goodness: Extending DSL

Gradle already has a powerful DSL, but Gradle wouldn't be Gradle if we couldn't extend the DSL ourselves. Maybe we have our own naming conventions in our company or we have a special problem domain we want to express in a Gradle build script. We can use the ExtensionContainer, available via project.extensions, to add new concepts to our build scripts. In the Standardizing your enterprise build environment webinar by Luke Daley some examples are shown on how to extend the DSL. Also in the samples folder of the Gradle distribution are examples on how to create a custom DSL.

Let's first create a simple DSL extension. We first define a new class CommonDependencies with methods to define dependencies in a Java project. We want to use these methods with descriptive names in our build scripts. To add the class we use the create() method of the ExtensionContainer. The first argument is a name that needs to be unique within the build. The name can be used together with a configuration block in the script to invoke methods on the class we pass as the second argument. Finally we can pass constructor arguments for the class as last arguments of the create() method.

/**
 * Class for DSL extension. A default repository is added
 * to the project. The use<name>() methods add 
 * dependencies to the project.
 */
class CommonDependencies {
    /** Reference to project, so we can set dependencies/repositories */
    final Project project

    CommonDependencies(final Project project) {
        this.project = project

        // Set mavenCentral() repository for project.
        project.repositories {
            mavenCentral()
        }
    }

    /**
     * Define Spock for testCompile dependency 
     * @param version Version of Spock dependency with default 0.7-groovy-2.0
     */
    void useSpock(final String version = '0.7-groovy-2.0') {
        project.dependencies {
            testCompile "org.spockframework:spock-core:$version"
        }
    }

    /**
     * Define Spring for compile dependency 
     * @param version Version of Spring dependency with default 3.2.3.RELEASE
     */
    void useSpring(final String version = '3.2.3.RELEASE') {
        project.dependencies {
            compile "org.springframework:spring-core:$version"
        }
    }

}

// Add DSL extension 'commonDependencies' with class CommonDependencies 
// passing project as constructor argument.
project.extensions.create('commonDependencies', CommonDependencies, project)

apply plugin: 'java'

// Use new DSL extension. Notice we can use configuration closures just
// like we are used to with other Gradle DSL methods.
commonDependencies {
    useSpock()
    useSpring '3.1.4.RELEASE'
}

// We can still use the Java plugin dependencies configuration.
dependencies {
    compile 'joda-time:joda-time:2.1'
}

We can invoke the dependencies task from the command-line and we see all dependencies are resolved correctly:

$ gradle dependencies
...
compile - Compile classpath for source set 'main'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
\--- joda-time:joda-time:2.1
...
testCompile - Compile classpath for source set 'test'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
+--- joda-time:joda-time:2.1
\--- org.spockframework:spock-core:0.7-groovy-2.0
     +--- junit:junit-dep:4.10
     |    \--- org.hamcrest:hamcrest-core:1.1 -> 1.3
     +--- org.codehaus.groovy:groovy-all:2.0.5
     \--- org.hamcrest:hamcrest-core:1.3
...

We can also use a plugin to extend the Gradle DSL. In the plugin code we use the same project.extensions.create() method so it is more transparent for the user. We only have to apply the plugin to a project and we can use the extra DSL methods in the build script. Let's create a simple plugin that will extend the DSL with the concept of a book and chapters. The following build script shows what we can do after we have applied the plugin:

apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

To achieve this we first create the following directory structure with files:

+ sample
  + buildSrc
     + src/main/groovy/com/mrhaki/gradle
       + Book.groovy
       + BookPlugin.groovy
     + src/main/resources/META-INF/gradle-plugins
       + book.properties
  + book
    + build.gradle
  + chapter1/src/html
    + index.html
  + chapter2/src/html
    + index.html
  + settings.gradle

The Book class will be added as DSL extension. The class has a method to set the title property and a method to add chapters which are Gradle project objects.

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/Book.groovy
package com.mrhaki.gradle

import org.gradle.api.*

class Book {
    String title
    List<Project> chapters = []

    void title(final String title) {
        this.title = title
    } 

    void chapter(final Project chapter) {
        chapters << chapter
    }
}

Next we create the BookPlugin class. The plugin will add the Book class as DSL extension. But we also create a task aggregate that will visit each chapter that is defined and then copies the content from the scr/html folder in the chapter project to the aggregate folder in the build folder. Finally we add a dist task that will simply archive the contents of the aggregated files.

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/BookPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.*
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.Zip

class BookPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.configure(project) {
            apply plugin: 'base'

            def book = project.extensions.create 'book', Book

            afterEvaluate {
                // Create task in afterEvaluate, so chapter projects
                // are resolved, otherwise chapters is empty.
                tasks.create(name: 'aggregate') {

                    // Skip task if no chapters are defined.
                    onlyIf { !book.chapters.empty }

                    doFirst {
                        // Copy content in src/html of 'book' directory.
                        copy {
                            from file('src/html')
                            into file("${buildDir}/aggregate")
                        }

                        // Copy content in src/html of chapter directories.
                        book.chapters.each { chapterProject ->
                            copy {
                                from chapterProject.file('src/html')
                                into file("${buildDir}/aggregate/${chapterProject.name}")
                            }
                        }
                    }
                }
            }

            tasks.create(name: 'dist', dependsOn: 'aggregate', type: Zip) {
                from file("${buildDir}/aggregate")
            }
        }        
    }
}

We create the file book.properties to tell Gradle about our new plugin:

# File: buildSrc/src/main/resources/META-INF/gradle-plugins/book.properties
implementation-class=com.mrhaki.gradle.BookPlugin

Our plugin is finished, so we can add a book project and some chapter projects. In the settings.gradle file we define an inclusion for these directories:

// File: settings.gradle
include 'chapter1'
include 'chapter2'
include 'book'

In the chapter directories we can add some sample content in the src/html directories. And in the book folder we create the following build.gradle file:

// File: book/build.gradle
apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

Now from the book folder we can run the aggregate and dist tasks. The end result is that all files from the chapter src/html folder are in the build/aggregate folder. And in the build/distributions folder we have the file book.zip containing the files.

Code written with Gradle 1.6.

July 11, 2010

Book Review: Groovy for Domain-Specific Languages

Title: Groovy for Domain-Specific Languages
Paperback : 312 pages (235mm x 191mm)
ISBN : 184719690X
ISBN 13 : 978-1-847196-90-3
Author(s) : Fergal Dearle
Publisher: Packt Publishing
Website
Sample chapter: Building a builder (chapter 7)

Groovy for Domain-Specific Languages has the subtitle 'Extend and enhance your Java applications with Domain-Specific Language in Groovy'. The book is aimed at Java developers who want to write a Domain-Specific Language (DSL) and integrate it with their applications.

Because the book targets a Java audience the first chapters are mostly an introduction to Groovy. The first chapter is an introduction to Groovy and Domain-Specific Languages (DSLs). The author explains some common concepts for DSLs and common examples of DSLs. In the second chapter we learn how to install Groovy and how to write and run scripts. Next we get a brief introduction into the Groovy language features, so as a Java developer we get enough knowledge to get started with Groovy. Features like metaprogramming are covered in a later chapter.

In chapter three the author covers closures. Because Groovy supports closures and they are important for writing DSLs a complete chapter is devoted to closures. This is a good thing, because as Java developers we get a good understanding of the power of closures, we don't have in Java. We learn how to write closures, pass parameters, curry parameters and invoke them. The book doesn't cover the latests Groovy 1.7.2 curry methods, but it is enough to get started.

After the first three chapters we learned enough to write our first DSL. The author explains how we can write our own DSL for a Twitter client. First we learn about the Twitter API and how to use it with the Twitter4J library. Next we are going to make the API calls more Groovy by using closures and Groovy's collection support. This results already in more readable code that doesn't read like normal Java code. We even learn how we can write a command-line script to use the Twitter DSL. The author explains that we can extend from the Script class to provide a very clean DSL that just read like English.

So after the fourth chapter we already can write our own DSL in Groovy with jsut the basic Groovy features. In chapter five the author introduces more powerful Groovy features. We learn about named parameters, builders and the builder design pattern, metaprogramming with Groovy's Meta-Object Protocol (MOP) and the ExpandoMetaClass to enhance classes.

In chapter six the author shows some existing DSLs from the Groovy eco-system. This is a bit theoretical but we get to see Grails' GORM, Gant, and test frameworks like Spock and EasyB. For a Java developer this might be new and shows at least the power of Groovy and what type of DSLs can be achieved.

Builders are a powerful way to work with hierarchical data and constructs. So chapter seven is completely dedicated to builders. This chapter is also available as free chapter for download. The author first explains how we can use closures and the invokeMethod and methodMissing methods to construct a builder. Next we learn how to use the BuilderSupport classes to create our own builders.

In chapter eight we learn how to write our own rules DSL and that is where it really gets interesting. We learn how we can use the bindings support in Groovy for a business rules DSL. We create a DSL to define rewards and promotion rules for an imaginery online media provider. Step by step we build the DSL and write supporting classes to load the DSL rules and execute them with concrete domain object classes.

In the last chapter we learn different ways to integrate the rules DSL from the previous chapter in a Java application. First we see how Groovy and Java integrate. And next we learn how we can integrate the DSL script with GroovyClassLoader, GroovyShell and GroovyScriptEngine.

Groovy for Domain-Specific Languages is a good book to learn how to write a DSL in Groovy. As a Java developer we get enough introduction to Groovy to get started, but if we already know Groovy we can skip the chapters about Groovy's features. It is good that in chapter four we already learned enough to write a DSL for the Twitter API. This shows that writing a DSL doesn't have to be difficult. The rest of the book introduces some more powerful features so in the end we can write a builder and a extensive rules DSL that can be integrated in our Java (or Groovy) applications. The book is a good starting point and provides useful code to get stared with writing our own Domain-Specific Language in Groovy. The author doesn't cover AST transformations as a another way to write DSLs, but with the provided information we can already achieve a lot.