Introduction

Target audience: Intermediate Gradle developers wishing to manage complex configurations.

In Gradle plugin development, managing complex configurations is a common task. Rather than overloading the DSL with hundreds of properties, it is often cleaner and more maintainable to define the configuration in an external file, such as YAML. This article guides you through the integration of the powerful Jackson library to parse YAML files into Kotlin objects, adopting a rigorous TDD approach to ensure the robustness of our plugin.site-baker.

1. The Context: Our site-baker Plugin

Our plugin`site-baker`is designed to automate the generation and deployment of a static site. It needs to read a YAML configuration file (managed-jbake-context.yml) to obtain information such as source paths, deployment destinations, and Git identifiers.

The YAML configuration we want to parse looks like this:

bake:
  srcPath: "./site/jbake"
  destDirPath: "bake"
  cname: "cheroliv.com"
pushPage:
  from: "bake"
  to: "cvs"
  repo:
    name: "trainings"
    repository: "https://github.com/pages-content/pages-content.github.io.git"
    credentials:
      username: "USERNAME"
      password: "SECRET_TOKEN"
  branch: "main"
  message: "cheroliv.com"
# ... autres configurations (pushMaquette, supabase, etc.)

2. Modeling the Configuration in Kotlin

Before parsing, we must define the structure of our data in Kotlin. Jackson will use these classes to automatically map the YAML content.

// plugin/src/main/kotlin/com/cheroliv/site/baker/data/SiteConfiguration.kt
package com.cheroliv.site.baker.data

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule

fun parseSiteConfiguration(yaml: String): SiteConfiguration {
    val mapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
    return mapper.readValue(yaml)
}

data class GitPushConfiguration(
    val from: String = "",
    val to: String = "",
    val repo: RepositoryConfiguration = RepositoryConfiguration(),
    val branch: String = "",
    val message: String = "",
)

data class RepositoryConfiguration(
    val name: String = "",
    val repository: String = "",
    val credentials: RepositoryCredentials = RepositoryCredentials(),
) {
    companion object {
        const val ORIGIN = "origin"
        const val CNAME = "CNAME"
        const val REMOTE = "remote"
    }
}

data class RepositoryCredentials(val username: String = "", val password: String = "")

data class SiteConfiguration(
    val bake: BakeConfiguration = BakeConfiguration(),
    val pushPage: GitPushConfiguration = GitPushConfiguration(),
    val pushMaquette: GitPushConfiguration = GitPushConfiguration(),
    val pushSource: GitPushConfiguration? = null,
    val pushTemplate: GitPushConfiguration? = null,
    val supabase: SupabaseContactFormConfig? = null
)

data class BakeConfiguration(
    val srcPath: String = "",
    val destDirPath: String = "",
    val cname: String? = null,
)

// ... (autres data classes pour Supabase, si nécessaire)

The function`parseSiteConfiguration`is our entry point for deserialization. It uses`ObjectMapper`from Jackson, configured with`YAMLFactory`for the YAML format and`registerKotlinModule()`for support of Kotlin specifics (such as property default values).

3. Integrating Jackson into build.gradle.kts

To use Jackson, we must add the necessary dependencies in the`build.gradle.kts`of our`plugin`module:

// plugin/build.gradle.kts
dependencies {
    // Jackson for YAML parsing
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.3")
    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.3")

    // ... autres dépendances
}

We use`jackson-module-kotlin`for Kotlin support and`jackson-dataformat-yaml`for managing the YAML format.

4. The TDD Approach: Testing YAML Parsing

Now, let’s apply TDD to validate our parsing logic.

4.1. Unit Test: Mapping YAML to a Kotlin Object

We start with a unit test in`SiteBakerPluginTest.kt`to verify that the function`parseSiteConfiguration`can correctly transform a YAML string into a`SiteConfiguration`object.

// plugin/src/test/kotlin/com/cheroliv/site/baker/SiteBakerPluginTest.kt
@Test
fun `can map configuration text to SiteConfiguration object`() {
    val yamlString = "../../managed-jbake-context.yml"
        .run(::File)
        .readText()
        .trimIndent()

    val config: SiteConfiguration = parseSiteConfiguration(yamlString)

    assertEquals("cheroliv.com", config.bake.cname)
    assertEquals("main", config.pushPage.branch)
    assertEquals("https://github.com/pages-content/pages-content.github.io.git", config.pushPage.repo.repository)
}

This test reads the content of the`managed-jbake-context.yml`file (which must exist for the test to be valid) and performs assertions on the resulting`SiteConfiguration`object. It validates that the key YAML values are correctly mapped.

4.2. Functional Test: Validating Configuration File Reading

To ensure that the plugin can read the configuration file via its DSL and that parsing works in a real Gradle environment, we add a functional test in`SiteBakerPluginFunctionalTest.kt`.

This test is crucial because it simulates the execution of the plugin in a real project. It must behermetic, meaning it must create the`managed-jbake-context.yml`file itself with controlled content, to guarantee the reproducibility and isolation of the test.

// plugin/src/functionalTest/kotlin/com/cheroliv/site/baker/SiteBakerPluginFunctionalTest.kt
@Test
fun `config file contains good data`(){
    val configContent = configFile.readText(UTF_8)
    // Vérifications comme dans vos commentaires
    assertTrue(configContent.contains("bake"))
    assertTrue(configContent.contains("pushPage"))
    assertTrue(configContent.contains("repository"))
}

This functional test verifies that the configuration file copied into the test temporary directory contains the expected data. Although this test does not directly parse the YAML into a Kotlin object, it validates the presence of the file and its content, which is an essential prerequisite for parsing by the plugin.

5. Visualizing the Parsing Flow

The following diagram illustrates the data flow and interactions during the parsing of the YAML configuration:

Diagram

Conclusion

By following a TDD approach, we have successfully integrated the Jackson library to parse YAML configuration files into Kotlin objects within our Gradle plugin. This method allowed us to:

  • Clearly modelour configuration with Kotlin data classes.

  • Validate the parsing logicwith targeted unit tests.

  • Ensure integrationin a real Gradle environment through hermetic functional tests.

This solid foundation gives us great confidence in extending our plugin with features that rely on this configuration, while ensuring the maintainability and robustness of the code. YAML parsing is now a reliable and tested feature of our`site-baker`plugin.

Related articles