Search

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

October 5, 2025

Groovy Goodness: Interleaving Elements From Collections

Groovy 5 adds the interleave method to the Iterable class. With this method you can interleave elements from two iterables. The result is a new List with elements where the first element is the first element of the first iterable and the second element the first element of the second iterable and so on. The size of the smallest collection is used to keep interleaving elements. Elements from the larger collection are ignored in the result.
If you want to have the items from the largest collection in the resulting list you can use the value true as second argument for the interleave method.

September 26, 2025

Groovy Goodness: Grouping Iterables Using zip And zipAll

Groovy 5 adds the extension methods zip and zipAll for iterables and iterators. Using the method you can combine elements from two collections into a new collection. The new collection contains Tuple2 instances where the values come from the items at the same index from both collections. So the first item of the first collection is grouped with the first item of the second collection. The size of the resulting collection is determined by the size of the smallest collection that is zipped.
With the zipAll method you can combine iterables of different sizes and set default values for missing items. It is possible to set a default value if an item is missing from the first iterable or the second iterable.

September 17, 2025

Groovy Goodness: Getting Extension And BaseName For File And Path

Groovy 5 adds the extension methods getExtension and getBaseName to the File and Path classes. You can invoke them as properties for a File and Path objects. Also the asBoolean method is added. This mean you can use a File or Path instance in a boolean context. If the underlying file exists true is returned and false otherwise.

September 12, 2025

Groovy Goodness: Accessing Regular Expression Named Groups By Name

Groovy (and Java) support using names for groups in regular expressions. The name of the group is defined using the syntax ?<name> where name must be replaced with the actual group name. This is very useful, because you can use the group name to access the value that is captured by the defined regular expression in a java.util.regex.Matcher object. Groovy supports for a long time accessing a group using the index operator. Since Groovy 5 you can use the name of the group to access the value as well. You can specify the name between square brackets ([<name>]) or use the name as property.

September 10, 2025

Groovy Goodness: Using Index Operator For Streams

Groovy already has a lot of extra methods for Stream classes. With Groovy 5 the getAt method is added as a terminal operation. You can now use the [] syntax to get an item from a Stream from a specific position. The argument is the index of the item or a range with the start and end index of the items to get.

September 9, 2025

Groovy Goodness: Get Next And Previous Characters

Since Groovy 1.8.2 the next and previous methods are added to the Character class. When you invoke the method next on a char or Character instance the next character is returned. And when you use the previous method the previous character is returned.
Groovy 5 adds an overloaded version of the next and previous method that accepts an int argument. With this argument you can specify the number of characters to skip before returning the next or previous character. For example 'a'.next(2) return 'c' and 'c'.previous(2) returns 'a'.

September 5, 2025

Groovy Goodness: Map Methods To Operators

Groovy supports operator overloading since the start. Operator overloading is implemented by an actual method signature that maps to an operator. For example an object with a plus method can be used with the + operator. There is a list of methods and operators available on the Groovy website.
As long as an object has a method with a name that Groovy understands the corresponding operator can be used in Groovy code. This is even true for Java objects. Since Groovy 5 you can use the groovy.transform.OperatorRename annotation on classes, methods or constructors to map other method names to the Groovy operator overloading method names. This is very useful for third-party classes that you cannot change, but still want to use simple operators in Groovy code. You can reassign a method name to the following methods so an operator can be used: plus, minus, multiply, div, remainder, power, leftShift, rightShift, rightShiftUnassigned, and, or, xor, compareTo. Suppose you use a class with an add method and want to use the + operator for this method. The following annotation can be used @OperatorRename(plus = 'add') for a method and inside the method you can use the + operator instead of the add method of the class.

September 4, 2025

Groovy Goodness: Logical Implication Operator

Since Groovy 1.8.3 Groovy has an implies() method for Boolean types. Groovy 5 adds an operator ==> for this method so you have a shorter way to express a logical implication. A logical implication is a logical function that can be expressed as P ==> Q. You can read this as P implies Q or if P than Q. This expression is true in every case except when P is true, but Q is false. The following truth table shows the interpretaton of the logical implication operator:

P Q P ==> Q

false

true

true

false

false

true

true

true

true

true

false

false

September 2, 2025

Groovy Goodness: For Loops With Index Variable

Groovy 5 adds support for using an index variable in a for-in loop. You must define an extra variable as first argument of the for loop. This variable will be used as index variable. For each iteration of the code in the for-in loop the value of this index variable is incremented by 1. You can use this value in the for loop.

September 1, 2025

Groovy Goodness: Create Ascii Bar Charts

Groovy 5 adds a new utility method to create an ascii bar chart. You can use the bar method in the org.codehaus.groovy.util.StringUtil class. You can pass a value, a minimum and maximum value and optinally specify the width of the bar chart. The result is a String value consisting of a number of "blocks". A block could be whole, but also 1/8 eights of the block are used to get a nice looking bar chart. How many of these values are needed is based on the input arguments. With this method you have a nice way to format number values on a command-line.

August 30, 2025

Groovy Goodness: Use Range With subList Method

Groovy is known for exending standard Java classes with extra methods or extra arguments for existing methods. Since Groovy 5 you can use a range as argument for the List.subList method. The range is used to determine the begin and end index of the original List instance to return.

August 29, 2025

Groovy Goodness: Transform Iterable Into A Map

In a previous blog post you can see how to use the collectEntries method to transform an iterable object into a Map. Groovy 5 introduced some extra overloaded methods for collectEntries. You can now pass a closure or function as arguments to transform the original iterable element into the key and value for the resulting Map. It is now also possible to pass a so-called collector Map that will be used to extend with new key/value pairs.

Besides extra overloaded method signatures for collectEntries Groovy 5 also adds the new methods withCollectedKeys and withCollectedValues. With the method withCollectedKeys a closure or function can passed to create the keys for the new Map based on the elements from the iterable. The value of the key/value pair is the unchanged element. You use the method withCollectedValues to pass a closure or function to create the value for the new key/value pair in the resulting Map. The key will be the original element from the iterable.

October 9, 2023

Groovy Goodness: Using NullCheck Annotation To Prevent NullPointerException

In Groovy we can apply the @NullCheck annotation to a class, constructor or method. The annotation is an AST (Abstract Syntax Tree) transformation and will insert code that checks for null values in methods or constructors. If a null value is passed to an annotated method or constructor, it will throw an IllegalArgumentException. Without the annotation we could have a NullPointerException if we try to invoke a method on the value we pass as argument. The annotation has an optional property includeGenerated which by default is false. If we set it to true then the null checks are also applied to generated methods and constructors. This is very useful if we apply other AST transformations to our class that generates additional code.

In the following example we use the @NullCheck annotation for a method and at class level:

import groovy.transform.NullCheck

@NullCheck
String upper(String value) {
    "Upper:" + value.toUpperCase()
}

assert upper("groovy") == "Upper:GROOVY"

try {
    upper(null)
} catch (IllegalArgumentException e) {
    assert e.message == "value cannot be null"
}
import groovy.transform.NullCheck

// Apply null check for all constructors and methods.
@NullCheck
class Language {
    private String name

    Language(String name) {
       this.name = name;
    }

    String upper(String prefix) {
        return prefix + name.toUpperCase();
    }
}


def groovy = new Language("groovy")

assert groovy.upper("Upper:") == "Upper:GROOVY"

// Method arguments are checked.
try {
    groovy.upper(null)
} catch (IllegalArgumentException e) {
    assert e.message == "prefix cannot be null"
}

// Constructor argument is also checked.
try {
    def lang = new Language(null)
} catch (IllegalArgumentException e) {
    assert e.message == "name cannot be null"
}

In the following example we set the includeGenerated property to true to also generate null checks for generated code like the constructor generated by @TupleConstructor:

import groovy.transform.NullCheck
import groovy.transform.TupleConstructor

@NullCheck(includeGenerated = true)
@TupleConstructor
class Language {
    final String name

    String upper(String prefix) {
        return prefix + name.toUpperCase();
    }
}

// Constructor is generated by @TupleConstructor and
// @NullCheck is applied to the generated constructor.
try {
    def lang = new Language(null)
} catch (IllegalArgumentException e) {
    assert e.message == "name cannot be null"
}

Written with Groovy 4.0.13

April 21, 2023

Groovy Goodness: Sorting Data With GINQ

GINQ (Groovy-INtegerate Query) is part of Groovy since version 4. With GINQ we can use SQL-like queries to work with in-memory data collections. If we want to sort the data we can use orderby followed by the property of the data we want to sort just like in SQL we can use order by. By default the sort ordering is ascending and null values are put last. We can change the sort ordering by specifying in desc with the orderby clause. Or to make the ascending order explicitly we use the statement in asc. Each of asc and desc also can take an argument to specify how we want null values to be sorted. The default way is to keep null values last in the ordering. If we want to make this explicit we use nullslast as argument to asc or desc. To have null values in the sorted result first we use the argument nullsfirst.

The following example shows all use cases for using orderby when using GINQ:

import groovy.json.JsonSlurper

// Parse sample JSON with a list of users.
def json = new JsonSlurper().parseText('''[
{ "username": "mrhaki", "email": "mrhaki@localhost" },
{ "username": "mrhaki", "email": "user@localhost" },
{ "username": "hubert", "email": "user@localhost" },
{ "username": "hubert", "email": "hubert@localhost" },
{ "username": "hubert", "email": null }
]''')

// Helper method to return a String
// representation of the user row.
def formatUser(row) {
    row.username + "," + row.email
}

// Default ordering is ascending.
// We specify the field name we want to order on.
assert GQ {
    from user in json
    orderby user.username
    select formatUser(user)
}.toList() == [
    'hubert,user@localhost',
    'hubert,hubert@localhost',
    'hubert,null',
    'mrhaki,mrhaki@localhost',
    'mrhaki,user@localhost'
]

// We can explicitly set ordering to ascending.
assert GQ {
    from user in json
    orderby user.email in asc
    select formatUser(user)
}.toList() == [
    'hubert,hubert@localhost',
    'mrhaki,mrhaki@localhost',
    'mrhaki,user@localhost',
    'hubert,user@localhost',
    'hubert,null'
]

// By default null values are last.
// We can also make this explicit as
// option to in asc() or in desc().
assert GQ {
    from user in json
    orderby user.email in asc(nullslast)
    select formatUser(user)
}.toList() == [
    'hubert,hubert@localhost',
    'mrhaki,mrhaki@localhost',
    'mrhaki,user@localhost',
    'hubert,user@localhost',
    'hubert,null'
]

// We can combine multiple properties to sort on.
assert GQ {
    from user in json
    orderby user.username, user.email
    select formatUser(user)
}.toList() == [
    'hubert,hubert@localhost',
    'hubert,user@localhost',
    'hubert,null',
    'mrhaki,mrhaki@localhost',
    'mrhaki,user@localhost'
]

// To order descending we must specify it
// as in desc.
assert GQ {
    from user in json
    orderby user.username in desc
    select formatUser(user)
}.toList() == [
    'mrhaki,mrhaki@localhost',
    'mrhaki,user@localhost',
    'hubert,user@localhost',
    'hubert,hubert@localhost',
    'hubert,null'
]

// We can mix the ordering and set it
// differently for each property.
assert GQ {
    from user in json
    orderby user.username in asc, user.email in desc
    select formatUser(user)
}.toList() == [
    'hubert,user@localhost',
    'hubert,hubert@localhost',
    'hubert,null',
    'mrhaki,user@localhost',
    'mrhaki,mrhaki@localhost'
]

// By default all null values are last,
// but we can use nullsfirst to have null
// values as first value in the ordering.
assert GQ {
    from user in json
    orderby user.username in asc, user.email in desc(nullsfirst)
    select formatUser(user)
}.toList() == [
    'hubert,null',
    'hubert,user@localhost',
    'hubert,hubert@localhost',
    'mrhaki,user@localhost',
    'mrhaki,mrhaki@localhost'
]

Written with Groovy 4.0.11.

Groovy Goodness: Calculate The Median Of A Collection

Since Groovy 4 we can use SQL like queries on in-memory collections with GINQ (Groovy-Integrated Query). GINQ provides some built-in aggregate functions like min, max, sum and others. One of these functions is median. With median we can get the value that is in the middle of the sorted list of values we want to calculate the median for. If the list has an uneven number of elements the element in the middle is returned, but if the list has an even number of elements the average of the two numbers in the middle is returned.

In the following example we see the use of the median function with GINQ:

// List of uneven number of response times.
def responseTimes = [201, 200, 179, 211, 350]

// Get the median from the list of response times.
// As the list has an uneven number of items
// the median is in the middle of the list after
// it has been sorted.
assert GQ {
    from time in responseTimes
    select median(time)
}.toList() == [201]

// List of even number of response times.
responseTimes = [201, 200, 179, 211, 350, 192]

// 2 numbers are the median so the result
// is the average of the 2 numbers.
assert GQ {
    from time in responseTimes
    select median(time)
}.findResult() == 200.5

// Use the GQ annotation and return a List from the method.
@groovy.ginq.transform.GQ(List)
def medianSize(List<String> values) {
    from s in values
    // We can also use an expression to get the median.
    // Here we take the size of the string values to
    // calculage the median.
    select median(s.size())
}

assert medianSize(["Java", "Clojure", "Groovy", "Kotlin", "Scala"]) == [6]

// Sample data structure where each record
// is structured data (map in this case).
// Could also come from JSON for example.
def data = [
    [test: "test1", time: 200],
    [test: "test1", time: 161],
    [test: "test2", time: 427],
    [test: "test2", time: 411],
    [test: "test1", time: 213]
]

// We want to get each record, but also
// the median for all times belonging to a single test.
// We can use the windowing functions provided by GINQ
// together with median.
def query = GQ {
    from result in data
    orderby result.test
    select result.test as test_name,
           result.time as response_time,
           (median(result.time) over(partitionby result.test)) as median_per_test
}

assert query
        .collect { row -> [name: row.test_name,
                           response: row.response_time,
                           median: row.median_per_test] } ==
[
    [name: "test1", response: 200, median: 200],
    [name: "test1", response: 161, median: 200],
    [name: "test1", response: 213, median: 200],
    [name: "test2", response: 427, median: 419],
    [name: "test2", response: 411, median: 419]
]

Written with Groovy 4.0.11.

April 13, 2023

Groovy Goodness: Using Tuples

Groovy supports a tuple type. A tuple is an immutable object to store elements of potentially different types. In Groovy there is a separate Tuple class based on how many elements we want to store in the tuple. The range starts at Tuple0 and ends with Tuple16. So we can store a maximum of 16 elements in a Groovy tuple.
Each of the classes has a constructor that takes all elements we want to store. But the Tuple class also has static factory methods to create those classes. We can use the tuple method and based on how many elements we provide to this method we get the corresponding Tuple object.

To get the elements from a Tuple instance we can use specific properties for each element. The first element can be fetched using the v1 property, the second element is v2 and so on for each element. Alternatively we use the subscript operator ([]) where the first element is at index 0.

Each Tuple instance also has a subList and subTuple method where we can provide the from and to index values of the elements we want to be returned. The methods return a new Tuple with only the elements we requested.
A Groovy tuple is also a List and that means we can use all collection methods for a List also on a Tuple instance.

In the following example we create some tuples and use different methods:

// Using the constructor to create a Tuple.
def tuple2 = new Tuple2("Groovy", "Goodness")

// We can also use the static tuple method.
// Maximum number of elements is 16.
def tuple3 = Tuple.tuple("Groovy", "is", "great")

assert tuple3 instanceof Tuple3


// We can mix types as each elements can
// have it's own type.
def mixed = Tuple.tuple(30, "minutes")

// We can use the subscript operator ([])
// to get a value.
assert mixed[0] == 30
assert mixed[1] == "minutes"

// Or use the get() method.
assert mixed.get(0) instanceof Integer
assert mixed.get(1) instanceof String

// Or use the getter/property V1/V2.
// For each element in a Tuple we can use that.
// Notice that the first element starts with v1.
assert mixed.v1 == 30
assert mixed.getV2() == "minutes"

// Or use multiple assignments.
def (int minutes, String period) = mixed
assert minutes == 30
assert period == "minutes"


// We can get the size.
assert mixed.size() == 2


// Or transform the elements to an array
// and type information is saved.
assert mixed.toArray() == [30, "minutes"]

assert mixed.toArray()[0].class.name == "java.lang.Integer"
assert mixed.toArray()[1].class.name == "java.lang.String"


// Sample tuple with 4 elements.
Tuple4 tuple4 = Tuple.tuple("Groovy", "rocks", "as", "always")

// We can use subList or subTuple to create a new Tuple
// with elements from the original Tuple.
// We need to specify the "from" and "to" index.
// The "to" index is exclusive.
assert tuple4.subList(0, 2) == Tuple.tuple("Groovy", "rocks")
assert tuple4.subTuple(0, 2) == Tuple.tuple("Groovy", "rocks")

// As Tuple extends from List we can use all
// Groovy collection extensions.
assert tuple4.findAll { e -> e.startsWith("a") } == ["as", "always"]
assert tuple4.collect { e -> e.toUpperCase() } == ["GROOVY", "ROCKS", "AS", "ALWAYS"]


// We can even create an empty Tuple.
assert Tuple.tuple() instanceof Tuple0

Written with Groovy 4.0.11.

April 4, 2023

Groovy Goodness: Using Subscript Operator With Multiple Fields On Date Related Objects

Since Groovy 4.0.5 we can use a subscript operator that accepts multiple fields on a java.util.Date and java.util.Calendar objects. And Groovy 4.0.6 extended this subscript operator to any java.time.TemporalAccessor instance. Before these Groovy version we could already use the subscript operator, but we could provide only one field we wanted to access. In a previous post we already have seen this. But now we can use multiple fields to get their values with one statement. We simply define the fields we want as arguments to the subscript operator. Under the hood the subscript operator is implemented by a getAt method that is added as an extension to the Date, Calendar and TemporalAccess classes. The return type is java.util.List and we can combine this with the multiple assignment support in Groovy. In other languages it is also called destructurizing. With multiple assignments we can assign the values from a java.util.List directly to variables.

In the following example we see several usages of the subscript operator with multiple fields on Date, Calendar and LocalDateTime objects:

import java.time.LocalDateTime
import java.time.Month
import static java.time.temporal.ChronoField.*
import static java.time.temporal.IsoFields.*

// Create a Date instance.
def date = new Date().parse('yyyy/MM/dd', '1973/07/09')

// Groovy adds the subscript operator for multiple
// fields to the Date class.
def output = date[Calendar.DATE, Calendar.MONTH, Calendar.YEAR]
assert output == [9, 6, 1973]

// The result is a list and we can destructurize it
// to assign values to variables (also called multiple assignments).
def (day, month, year) = date[Calendar.DATE, Calendar.MONTH, Calendar.YEAR]

assert "$day-${month + 1}-$year" == "9-7-1973"


// Create a Calendar instance.
def calendar = date.toCalendar()

// The subscript operator supporting multiple fields
// is also added to the Calendar class.
def (calDay, calMonth, calYear) = calendar[Calendar.DATE, Calendar.MONTH, Calendar.YEAR]

assert "Time to celebrate on $calDay-${calMonth + 1}" == "Time to celebrate on 9-7"


// Create a LocalDateTime instance
def birthDateTime = LocalDateTime.of(1973, Month.JULY, 9, 6, 30, 0);

// Groovy adds the subscript operator with multiple fields
// on any TemporalAccessor instance.
def (dayOfWeek, dayOfYear, quarter, week) = birthDateTime[DAY_OF_WEEK, DAY_OF_YEAR, QUARTER_OF_YEAR, WEEK_OF_WEEK_BASED_YEAR]

assert "Born in week $week on day $dayOfWeek" == "Born in week 28 on day 1"
assert quarter == 3
assert dayOfYear == 190

def (hour, minute) = birthDateTime[HOUR_OF_DAY, MINUTE_OF_HOUR]

assert "Live started at $hour:$minute" == "Live started at 6:30"

Written with Groovy 4.0.11.

March 24, 2023

Spocklight: Assert Elements In Collections In Any Order

Since Spock 2.1 we have 2 new operators we can use for assertions to check collections: =~ and ==~. We can use these operators with implementations of the Iterable interface when we want to check that a given collection has the same elements as an expected collection and we don’t care about the order of the elements. Without the new operators we would have to cast our collections to a Set first and than use the == operator.

The difference between the operators =~ and ==~ is that =~ is lenient and ==~ is strict. With the lenient match operator we expect that each element in our expected collection appears at least once in the collection we want to assert. The strict match operator expects that each element in our expected collection appears exactly once in the collection we want to assert.

In the following example we see different uses of the new operators and some other idiomatic Groovy ways to check elements in a collection in any order:

package mrhaki

import spock.lang.Specification;

class CollectionConditions extends Specification {

    void "check items in list are present in the same order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        // List result is ordered so the items
        // are not the same as the expected List.
        result != [20, 10, 2, 1]
        result == [1, 10, 2, 20]

        // We can cast the result and expected list to Set
        // and now the contents and order is the same.
        result as Set == [20, 10, 2, 1] as Set
    }

    void "check all items in list are present in any order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        result ==~ [20, 10, 2, 1]

        /* The following assert would fail:
           result ==~ [20, 10]

           Condition not satisfied:

           result ==~ [20, 10]
           |      |
           |      false
           [1, 10, 2, 20]

           Expected: iterable with items [<20>, <10>] in any order
                but: not matched: <1>*/

        // Negating also works
        result !==~ [20, 10]
    }

    void "lenient check all items in list are present in any order"() {
        when:
        List<Integer> result = [1, 1, 10, 2, 2, 20]

        then:
        // result has extra values 1, 2 but with lenient
        // check the assert is still true.
        result =~ [20, 10, 2, 1]

        /* The following assert would fail:

        result =~ [20, 10]

        Condition not satisfied:

        result =~ [20, 10]
        |      |
        |      false
        |      2 differences (50% similarity, 0 missing, 2 extra)
        |      missing: []
        |      extra: [1, 2]
        [1, 10, 2, 20] */

        // Negating works
        result !=~ [20, 10]
    }

    void "check at least one item in list is part of expected list in any order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        result.any { i -> i in [20, 10]}
    }

    void "check every item in list is part of expected list in any order"() {
        when:
        List<Integer> result = [1, 1, 10, 2, 2, 20]

        then:
        result.every { i -> i in [20, 10, 2, 1]}
    }
}

Written with Spock 2.3-groovy-4.0.

July 4, 2022

Groovy Goodness: Closed And Open Ranges

Groovy supports ranges for a long time. But Groovy 4 adds a new feature for ranges and that is the support for open (exclusive) ranges at the beginning of a range. Open means the number that defines the range is not part of the actual range result and we must use the less-than character (<). This is also referred to as exclusive, where the value is excluded from the range. When a range is closed the value is included, also called inclusive. Before Groovy 4 we could already define the end of the range to be exclusive or inclusive, but now we can also define the beginning of the range to be exclusive.

In the following example we use closed and open range definitions from the start or end:

def inclRange = 0..5

assert inclRange == [0, 1, 2, 3, 4, 5]
assert inclRange.from == 0
assert inclRange.to == 5


def exclEndRange = 0..<5

assert exclEndRange == [0, 1, 2, 3, 4]
assert exclEndRange.from == 0
assert exclEndRange.to == 4


// Support for exclusive begin added in Groovy 4.
def exclBeginRange = 0<..5

assert exclBeginRange == [1, 2, 3, 4, 5]
assert exclBeginRange.from == 1
assert exclBeginRange.to == 5


// Support for exclusive begin added in Groovy 4.
def exclRange = 0<..<5

assert exclRange == [1, 2, 3, 4]
assert exclRange.from == 1
assert exclRange.to == 4

Written with Groovy 4.0.3.

July 1, 2022

Groovy Goodness: Creating TOML Configuration With TomlBuilder

Groovy 4 introduced support for TOML configuration file. In a previous post we already learned how we can parse TOML content. In this post we will see we can use a builder syntax to create TOML content. We need the class TomlBuilder and then define our structure using a nice builder DSL. The DSL is comparable to create JSON using the JsonBuilder. The names of the nodes in the DSL structure will be the names of the properties. Nodes within nodes will result in concatenated property names with the name of each node separated by a dot (.). We can also use collections as arguments and those will translated to TOML arrays. A collection can optionally be followed by a closure that processes each item in the collection to generate the content for the TOML array.

In the following example we use the builder syntax to create what we want in our TOML content. Using the toString method we get the TOML data as string:

import groovy.toml.TomlBuilder

// Helper record class to store "server" properties.
record Server(String env, String host, int port) {}

// Create Tomlbuilder.
def toml = new TomlBuilder()

// Define structure.
toml {
    // Use closure to group.
    application {
        name "Groovy TOML"
        version "1.0.0"
    }

    // Use closures to define levels.
    users {
        env {
           enabled true
        }
        acc {
            enabled false
        }
    }
         
    // Use maps
    debug(enabled: true)  
    
    // We can use collections
    ports([80, 443]) 
    
    // Convert data with closure applied for each item in collection.
    servers([new Server("dev", "localhost", 8080), 
             new Server("uat", "cloud-acc", 80)], { server -> 
        env server.env 
        host server.host 
        port server.port 
    }) 
}

assert toml.toString() == """\
application.name = 'Groovy TOML'
application.version = '1.0.0'
users.env.enabled = true
users.acc.enabled = false
debug.enabled = true
ports = [80, 443]
servers = [{env = 'dev', host = 'localhost', port = 8080}, {env = 'uat', host = 'cloud-acc', port = 80}]
"""

// In order to write to a writer we could use:
// def sw = new StringWriter()
// toml.writeTo(sw)
// def content = sw.toString()

Instead of using the builder DSL syntax we can also use a Map with all data we want to transform to TOML data:

import groovy.toml.TomlBuilder

def instant = Instant.ofEpochSecond(1656487920)
def clock = Clock.fixed(instant, ZoneOffset.UTC)

def config = [
    application: [
      name: "Groovy TOML",
      version: "1.0.0"
    ],
    users: [
        dev: [enabled: true],
        uat: [enabled: false]
    ],
    ports: [80, 443],
    debug: [
        enabled: false
    ],
    build: [
        jdk: 'openjdk version "17.0.3" 2022-04-19',
        time: ZonedDateTime.now(clock).dateTimeString
   ],
   servers: [
       [env: "dev", host: "localhost", port: 8080],
       [env: "uat", host: "cloud-acc", port: 80]
   ]
]

// Create TomlBuilder
def toml = new TomlBuilder()

// Use data defined in the Map.
toml config

assert toml.toString() == """\
application.name = 'Groovy TOML'
application.version = '1.0.0'
users.dev.enabled = true
users.uat.enabled = false
ports = [80, 443]
debug.enabled = false
build.jdk = 'openjdk version "17.0.3" 2022-04-19'
build.time = '2022-06-29T07:32:00Z'
servers = [{env = 'dev', host = 'localhost', port = 8080}, {env = 'uat', host = 'cloud-acc', port = 80}]
"""

Written with Groovy 4.0.3.