Search

Dark theme | Light theme

November 16, 2016

Gradle Goodness: Validate Model In Rule Based Model Configuration

Rule based model configuration gives Gradle more knowledge about the objects and their dependencies. This information can be used by Gradle to optimise the build process. We define rules on how we want Gradle to create objects and how we want to mutate objects in a class that extends RuleSource. We can also add rules to validate objects available in the Gradle model space. We use the @Validate annotation on methods that have validation logic. The first argument of the method is of the type of the object we want to validate. This type must be managed by Gradle.

In the following example we use the sample from a previous post. In this sample we have a VersionFile class that is managed by Gradle. The class has a version and outputFile property. The version must be set and must start with a v. The outputFile property is also required.

// File: buildSrc/src/main/groovy/mrhaki/gradle/VersionFileTaskRules.groovy
package mrhaki.gradle

import org.gradle.api.GradleException
import org.gradle.api.Task
import org.gradle.model.Model
import org.gradle.model.ModelMap
import org.gradle.model.Mutate
import org.gradle.model.RuleSource
import org.gradle.model.Validate

class VersionFileTaskRules extends RuleSource {

    @Model
    void versionFile(final VersionFile versionFile) {}

    /**
     * Version property of {@link VersionFile} must have a value and the value
     * must start with a 'v'.
     * 
     * @param versionFile Gradle managed {@link VersionFile} object we want to validate
     */
    @Validate
    void validateVersionFileVersion(final VersionFile versionFile) {
        def message = """\
            Property VersionFile.version is not set. Set a value in the model configuration.
            
            Example:
            -------
            model {
                versionFile {
                    version = 'v1.0.0'
                }
            }
            """.stripIndent()
        checkAssert(message) {
            assert versionFile.version
        }
        
        message = """\
            Property VersionFile.version should start with 'v'. Set a value starting with 'v' in the model configuration.
            
            Example:
            -------
            model {
                versionFile {
                    version = 'v${versionFile.version}'
                }
            }
            """.stripIndent()
        checkAssert(message) {
            assert versionFile.version.startsWith('v')
        }
    }

    /**
     * Outputfile property of {@link VersionFile} must have a value.
     *
     * @param versionFile Gradle managed {@link VersionFile} object we want to validate
     */
    @Validate
    void validateVersionFileOutputFile(final VersionFile versionFile) {
        def message = """\
            Property VersionFile.outputFile is not set. Set a value in the model configuration.

            Example:
            -------
            model {
                versionFile {
                    outputFile = project.file("\${buildDir}/version.txt")
                }
            }
            """.stripIndent()
        checkAssert(message) {
            assert versionFile.outputFile
        }
    }

    /**
     * Run assert statement in assertion Closure. If the assertion fails
     * we catch the exception. We use the message with the error appended with an user message
     * and throw a {@link GradleException}. 
     * 
     * @param message User message to be appended to assertion error message
     * @param assertion Assert statement(s) to run
     */
    private void checkAssert(final String message, final Closure assertion) {
        try {
            // Run Closure with assert statement(s).
            assertion()
        } catch (AssertionError assertionError) {
            // Use Groovy power assert output from the assertionError
            // exception and append user message.
            final exceptionMessage = new StringBuilder(assertionError.message)
            exceptionMessage << System.properties['line.separator'] << System.properties['line.separator']
            exceptionMessage << message
            
            // Throw exception so Gradle knows the validation fails.
            throw new GradleException(exceptionMessage, assertionError)
        }
    }
    
    @Mutate
    void createVersionFileTask(final ModelMap<Task> tasks, final VersionFile versionFile) {
        tasks.create('generateVersionFile', VersionFileTask) { task ->
            task.version = versionFile.version
            task.outputFile = versionFile.outputFile
        }
    }

}

Let's use the following build file and apply the rules to the project:

// File: build.gradle
apply plugin: mrhaki.gradle.VersionFileTaskRules

model {
}

From the command line we run the model task to check the Gradle model space:

$ gradle -q model

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'versionrule'.
> Exception thrown while executing model rule: VersionFileTaskRules#validateVersionFileOutputFile(VersionFile)
   >                 assert versionFile.outputFile
            |           |
            |           null
            VersionFile 'versionFile'

     Property VersionFile.outputFile is not set. Set a value in the model configuration.

     Example:
     -------
     model {
         versionFile {
             outputFile = project.file("${buildDir}/version.txt")
         }
     }



* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
$

Notice the validation rules are evaluated in alphabetical order of the methods names that have the @Validate annotation.

Let's fix this and set also the version property in our build file:

// File: build.gradle
apply plugin: mrhaki.gradle.VersionFileTaskRules

model {
    versionFile {
        version = '1.0.3.RELEASE'
        outputFile = project.file("${buildDir}/version.txt")
    }
}

We rerun the model task and in the output we see the version is invalid, because it doesn't start with a v:

$ gradle -q model

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'versionrule'.
> Exception thrown while executing model rule: VersionFileTaskRules#validateVersionFileVersion(VersionFile)
   > assert versionFile.version.startsWith('v')
            |           |       |
            |           |       false
            |           1.0.3.RELEASE
            VersionFile 'versionFile'

     Property VersionFile.version should start with 'v'. Set a value starting with 'v' in the model configuration.

     Example:
     -------
     model {
         versionFile {
             version = 'v1.0.3.RELEASE'
         }
     }


* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
$

Let's make our validation pass with the following build script:

// File: build.gradle
apply plugin: mrhaki.gradle.VersionFileTaskRules

model {
    versionFile {
        version = 'v1.0.3.RELEASE'
        outputFile = project.file("${buildDir}/version.txt")
    }
}

And in the output of the model we see the properties are set with our values:

$ gradle -q model
...
+ versionFile
      | Type:           mrhaki.gradle.VersionFile
      | Creator:        VersionFileTaskRules#versionFile(VersionFile)
      | Rules:
         ⤷ versionFile { ... } @ build.gradle line 10, column 5
         ⤷ VersionFileTaskRules#validateVersionFileOutputFile(VersionFile)
         ⤷ VersionFileTaskRules#validateVersionFileVersion(VersionFile)
    + outputFile
          | Type:       java.io.File
          | Value:      /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build/version.txt
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
    + version
          | Type:       java.lang.String
          | Value:      v1.0.3.RELEASE
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
...
$

Written with Gradle 3.2.