This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 1a34c22ff4 GROOVY-11923: Provide a groovy-csv optional module
1a34c22ff4 is described below

commit 1a34c22ff46e860936f07645bfc6832a1a4e64b3
Author: Paul King <[email protected]>
AuthorDate: Sun Apr 12 06:30:22 2026 +1000

    GROOVY-11923: Provide a groovy-csv optional module
---
 settings.gradle                                    |   1 +
 subprojects/groovy-csv/build.gradle                |  35 +++
 .../src/main/java/groovy/csv/CsvBuilder.java       | 160 ++++++++++++++
 .../main/java/groovy/csv/CsvRuntimeException.java  |  44 ++++
 .../src/main/java/groovy/csv/CsvSlurper.java       | 245 +++++++++++++++++++++
 .../src/main/java/groovy/csv/package-info.java     |  23 ++
 .../groovy-csv/src/spec/doc/csv-userguide.adoc     | 112 ++++++++++
 .../src/spec/test/groovy/csv/CsvBuilderTest.groovy | 101 +++++++++
 .../src/spec/test/groovy/csv/CsvSlurperTest.groovy | 106 +++++++++
 9 files changed, 827 insertions(+)

diff --git a/settings.gradle b/settings.gradle
index 52d38fb580..51a38c2ea5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -78,6 +78,7 @@ def subprojects = [
         'groovy-test-junit5',
         'groovy-test-junit6',
         'groovy-testng',
+        'groovy-csv',
         'groovy-toml',
         'groovy-typecheckers',
         'groovy-xml',
diff --git a/subprojects/groovy-csv/build.gradle 
b/subprojects/groovy-csv/build.gradle
new file mode 100644
index 0000000000..57fdccd28a
--- /dev/null
+++ b/subprojects/groovy-csv/build.gradle
@@ -0,0 +1,35 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+plugins {
+    id 'org.apache.groovy-library'
+}
+
+dependencies {
+    api rootProject
+    implementation 
"com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${versions.jackson}"
+    implementation 
"com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
+    testImplementation projects.groovyTest
+    testRuntimeOnly 
"com.fasterxml.jackson.core:jackson-annotations:${versions.jacksonAnnotations}"
+    testRuntimeOnly projects.groovyAnt // for JavadocAssertionTests
+}
+
+groovyLibrary {
+    optionalModule()
+    withoutBinaryCompatibilityChecks()
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java 
b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java
new file mode 100644
index 0000000000..f11e67f2d5
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java
@@ -0,0 +1,160 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.csv;
+
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import groovy.lang.Writable;
+import org.apache.groovy.lang.annotation.Incubating;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Builds CSV output from collections of maps or typed objects.
+ * <p>
+ * Example with maps:
+ * <pre><code class="groovyTestCase">
+ * def data = [[name: 'Alice', age: 30], [name: 'Bob', age: 25]]
+ * def csv = groovy.csv.CsvBuilder.toCsv(data)
+ * assert csv.contains('name,age')
+ * assert csv.contains('Alice,30')
+ * </code></pre>
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvBuilder implements Writable {
+    private final CsvMapper mapper;
+    private char separator = ',';
+    private char quoteChar = '"';
+    private String content;
+
+    public CsvBuilder() {
+        this.mapper = new CsvMapper();
+    }
+
+    /**
+     * Set the column separator character (default: comma).
+     *
+     * @param separator the separator character
+     * @return this builder for chaining
+     */
+    public CsvBuilder setSeparator(char separator) {
+        this.separator = separator;
+        return this;
+    }
+
+    /**
+     * Set the quote character (default: double-quote).
+     *
+     * @param quoteChar the quote character
+     * @return this builder for chaining
+     */
+    public CsvBuilder setQuoteChar(char quoteChar) {
+        this.quoteChar = quoteChar;
+        return this;
+    }
+
+    /**
+     * Convert a collection of maps to CSV.
+     * The keys of the first map are used as column headers.
+     *
+     * @param data the collection of maps
+     * @return the CSV string
+     */
+    public static String toCsv(Collection<? extends Map<String, ?>> data) {
+        if (data == null || data.isEmpty()) {
+            return "";
+        }
+        CsvMapper csvMapper = new CsvMapper();
+        Map<String, ?> first = data.iterator().next();
+        CsvSchema.Builder schemaBuilder = CsvSchema.builder();
+        for (String key : first.keySet()) {
+            schemaBuilder.addColumn(key);
+        }
+        CsvSchema schema = schemaBuilder.build().withHeader();
+        try {
+            return csvMapper.writer(schema).writeValueAsString(data);
+        } catch (IOException e) {
+            throw new CsvRuntimeException(e);
+        }
+    }
+
+    /**
+     * Convert a collection of typed objects to CSV using Jackson databinding.
+     * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations.
+     *
+     * @param data the collection of objects
+     * @param type the object type (used to derive the schema)
+     * @param <T> the object type
+     * @return the CSV string
+     */
+    public static <T> String toCsv(Collection<T> data, Class<T> type) {
+        if (data == null || data.isEmpty()) {
+            return "";
+        }
+        CsvMapper csvMapper = new CsvMapper();
+        CsvSchema schema = csvMapper.schemaFor(type).withHeader();
+        try {
+            return csvMapper.writer(schema).writeValueAsString(data);
+        } catch (IOException e) {
+            throw new CsvRuntimeException(e);
+        }
+    }
+
+    /**
+     * Build CSV from a collection of maps.
+     *
+     * @param data the collection of maps
+     * @return this builder
+     */
+    public CsvBuilder call(Collection<? extends Map<String, ?>> data) {
+        if (data == null || data.isEmpty()) {
+            this.content = "";
+            return this;
+        }
+        Map<String, ?> first = data.iterator().next();
+        CsvSchema.Builder schemaBuilder = CsvSchema.builder()
+                .setColumnSeparator(separator)
+                .setQuoteChar(quoteChar);
+        for (String key : first.keySet()) {
+            schemaBuilder.addColumn(key);
+        }
+        CsvSchema schema = schemaBuilder.build().withHeader();
+        try {
+            this.content = mapper.writer(schema).writeValueAsString(data);
+        } catch (IOException e) {
+            throw new CsvRuntimeException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return content != null ? content : "";
+    }
+
+    @Override
+    public Writer writeTo(Writer out) throws IOException {
+        return out.append(toString());
+    }
+}
diff --git 
a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java 
b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java
new file mode 100644
index 0000000000..c105074c71
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java
@@ -0,0 +1,44 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.csv;
+
+import groovy.lang.GroovyRuntimeException;
+import org.apache.groovy.lang.annotation.Incubating;
+
+/**
+ * Represents runtime exception occurred when parsing or building CSV
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvRuntimeException extends GroovyRuntimeException {
+    private static final long serialVersionUID = 2809672072790437945L;
+
+    public CsvRuntimeException(String msg) {
+        super(msg);
+    }
+
+    public CsvRuntimeException(Throwable cause) {
+        super(cause);
+    }
+
+    public CsvRuntimeException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java 
b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java
new file mode 100644
index 0000000000..d85536289a
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java
@@ -0,0 +1,245 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.csv;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import org.apache.groovy.lang.annotation.Incubating;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a CSV parser.
+ * <p>
+ * Usage:
+ * <pre><code class="groovyTestCase">
+ * def csv = new 
groovy.csv.CsvSlurper().parseText('name,age\nAlice,30\nBob,25')
+ * assert csv[0].name == 'Alice'
+ * assert csv[1].age == '25'
+ * </code></pre>
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvSlurper {
+    private final CsvMapper mapper;
+    private char separator = ',';
+    private char quoteChar = '"';
+    private boolean useHeader = true;
+
+    public CsvSlurper() {
+        this.mapper = new CsvMapper();
+    }
+
+    /**
+     * Set the column separator character (default: comma).
+     *
+     * @param separator the separator character
+     * @return this slurper for chaining
+     */
+    public CsvSlurper setSeparator(char separator) {
+        this.separator = separator;
+        return this;
+    }
+
+    /**
+     * Set the quote character (default: double-quote).
+     *
+     * @param quoteChar the quote character
+     * @return this slurper for chaining
+     */
+    public CsvSlurper setQuoteChar(char quoteChar) {
+        this.quoteChar = quoteChar;
+        return this;
+    }
+
+    /**
+     * Set whether the first row is a header row (default: true).
+     *
+     * @param useHeader true to treat the first row as headers
+     * @return this slurper for chaining
+     */
+    public CsvSlurper setUseHeader(boolean useHeader) {
+        this.useHeader = useHeader;
+        return this;
+    }
+
+    /**
+     * Parse the content of the specified CSV text.
+     *
+     * @param csv the CSV text
+     * @return a list of maps (one per row), keyed by column headers
+     */
+    public List<Map<String, String>> parseText(String csv) {
+        if (csv == null || csv.isBlank()) {
+            return List.of();
+        }
+        return parse(new StringReader(csv));
+    }
+
+    /**
+     * Parse CSV from a reader.
+     * When {@code useHeader} is true (the default), each row is returned as a 
map keyed
+     * by column headers from the first row. When {@code useHeader} is false, 
maps are
+     * keyed by auto-generated column names.
+     *
+     * @param reader the reader of CSV
+     * @return a list of maps (one per row)
+     */
+    public List<Map<String, String>> parse(Reader reader) {
+        try {
+            CsvSchema schema = buildSchema();
+            MappingIterator<Map<String, String>> it = mapper
+                    .readerFor(Map.class)
+                    .with(schema)
+                    .readValues(reader);
+            List<Map<String, String>> result = it.readAll();
+            // Jackson may return a single empty map for header-only input
+            if (result.size() == 1 && result.get(0).isEmpty()) {
+                result.clear();
+            }
+            return result;
+        } catch (IOException e) {
+            throw new CsvRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse CSV from an input stream. The caller is responsible for closing 
the stream.
+     *
+     * @param stream the input stream of CSV
+     * @return a list of maps (one per row)
+     */
+    public List<Map<String, String>> parse(InputStream stream) {
+        return parse(new InputStreamReader(stream));
+    }
+
+    /**
+     * Parse CSV from a file.
+     *
+     * @param file the CSV file
+     * @return a list of maps (one per row), keyed by column headers
+     */
+    public List<Map<String, String>> parse(File file) throws IOException {
+        return parse(file.toPath());
+    }
+
+    /**
+     * Parse CSV from a path.
+     *
+     * @param path the path to the CSV file
+     * @return a list of maps (one per row), keyed by column headers
+     */
+    public List<Map<String, String>> parse(Path path) throws IOException {
+        try (InputStream stream = Files.newInputStream(path)) {
+            return parse(new InputStreamReader(stream));
+        }
+    }
+
+    /**
+     * Parse CSV into typed objects using Jackson databinding.
+     * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations for
+     * column mapping and type conversion.
+     *
+     * @param type the target type
+     * @param csv the CSV text
+     * @param <T> the target type
+     * @return a list of typed objects
+     */
+    public <T> List<T> parseAs(Class<T> type, String csv) {
+        return parseAs(type, new StringReader(csv));
+    }
+
+    /**
+     * Parse CSV from a reader into typed objects.
+     *
+     * @param type the target type
+     * @param reader the reader of CSV
+     * @param <T> the target type
+     * @return a list of typed objects
+     */
+    public <T> List<T> parseAs(Class<T> type, Reader reader) {
+        try {
+            // Use empty schema with header — Jackson matches columns by name
+            // rather than by position, allowing CSV column order to differ 
from field order
+            CsvSchema schema = CsvSchema.emptySchema();
+            if (useHeader) {
+                schema = schema.withHeader();
+            }
+            schema = schema.rebuild()
+                    .setColumnSeparator(separator)
+                    .setQuoteChar(quoteChar)
+                    .build();
+            MappingIterator<T> it = mapper
+                    .readerFor(type)
+                    .with(schema)
+                    .readValues(reader);
+            return it.readAll();
+        } catch (IOException e) {
+            throw new CsvRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse CSV from a file into typed objects.
+     *
+     * @param type the target type
+     * @param file the CSV file
+     * @param <T> the target type
+     * @return a list of typed objects
+     */
+    public <T> List<T> parseAs(Class<T> type, File file) throws IOException {
+        return parseAs(type, file.toPath());
+    }
+
+    /**
+     * Parse CSV from a path into typed objects.
+     *
+     * @param type the target type
+     * @param path the path to the CSV file
+     * @param <T> the target type
+     * @return a list of typed objects
+     */
+    public <T> List<T> parseAs(Class<T> type, Path path) throws IOException {
+        try (InputStream stream = Files.newInputStream(path)) {
+            return parseAs(type, new InputStreamReader(stream));
+        }
+    }
+
+    private CsvSchema buildSchema() {
+        CsvSchema.Builder builder = CsvSchema.builder()
+                .setColumnSeparator(separator)
+                .setQuoteChar(quoteChar);
+        CsvSchema schema = builder.build();
+        if (useHeader) {
+            schema = schema.withHeader();
+        }
+        return schema;
+    }
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/package-info.java 
b/subprojects/groovy-csv/src/main/java/groovy/csv/package-info.java
new file mode 100644
index 0000000000..af071c8fe6
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/package-info.java
@@ -0,0 +1,23 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/**
+ * Classes for parsing and building <a 
href="https://datatracker.ietf.org/doc/html/rfc4180";>CSV</a>.
+ */
+package groovy.csv;
diff --git a/subprojects/groovy-csv/src/spec/doc/csv-userguide.adoc 
b/subprojects/groovy-csv/src/spec/doc/csv-userguide.adoc
new file mode 100644
index 0000000000..853970c701
--- /dev/null
+++ b/subprojects/groovy-csv/src/spec/doc/csv-userguide.adoc
@@ -0,0 +1,112 @@
+//////////////////////////////////////////
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+
+//////////////////////////////////////////
+
+= Processing CSV
+
+Groovy has an optional `groovy-csv` module which provides support for reading 
and writing
+https://datatracker.ietf.org/doc/html/rfc4180[CSV (RFC 4180)] data. The 
classes are found
+in the `groovy.csv` package.
+
+[[csv_csvslurper]]
+== CsvSlurper
+
+`CsvSlurper` parses CSV text into a list of maps, where each row becomes a map 
keyed
+by the column headers from the first row. Values are returned as strings.
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=parse_text,indent=0]
+----
+
+Rows support dynamic property access using the header names:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=property_access,indent=0]
+----
+
+=== Configuration
+
+The separator character and quote character can be customised:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=custom_separator,indent=0]
+----
+
+Quoted fields follow RFC 4180 — fields containing the separator, newlines, or 
the quote
+character are enclosed in quotes, with embedded quotes doubled:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=quoted_fields,indent=0]
+----
+
+=== Typed parsing
+
+`CsvSlurper` can parse CSV directly into typed objects using Jackson 
databinding.
+Standard Jackson annotations such as `@JsonProperty` and `@JsonFormat` are 
supported
+for column name mapping and type conversion:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=typed_parsing,indent=0]
+----
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvSlurperTest.groovy[tags=typed_parsing_usage,indent=0]
+----
+
+[[csv_csvbuilder]]
+== CsvBuilder
+
+`CsvBuilder` converts collections of maps or typed objects to CSV. The keys of 
the first
+map are used as column headers.
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvBuilderTest.groovy[tags=to_csv_maps,indent=0]
+----
+
+=== Typed writing
+
+`CsvBuilder` can also write typed objects. Jackson annotations are supported 
for
+column naming and formatting:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvBuilderTest.groovy[tags=typed_writing,indent=0]
+----
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvBuilderTest.groovy[tags=typed_writing_usage,indent=0]
+----
+
+=== Round-trip
+
+CSV written by `CsvBuilder` can be read back with `CsvSlurper`:
+
+[source,groovy]
+----
+include::../test/groovy/csv/CsvBuilderTest.groovy[tags=round_trip,indent=0]
+----
diff --git 
a/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvBuilderTest.groovy 
b/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvBuilderTest.groovy
new file mode 100644
index 0000000000..989bd6afcf
--- /dev/null
+++ b/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvBuilderTest.groovy
@@ -0,0 +1,101 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.csv
+
+import groovy.test.GroovyTestCase
+
+class CsvBuilderTest extends GroovyTestCase {
+
+    void testToCsvFromMaps() {
+        // tag::to_csv_maps[]
+        def data = [
+            [name: 'Alice', age: 30],
+            [name: 'Bob', age: 25]
+        ]
+        def csv = CsvBuilder.toCsv(data)
+        assert csv.contains('name,age')
+        assert csv.contains('Alice,30')
+        assert csv.contains('Bob,25')
+        // end::to_csv_maps[]
+    }
+
+    void testToCsvEmpty() {
+        assert CsvBuilder.toCsv([]) == ''
+        assert CsvBuilder.toCsv(null) == ''
+    }
+
+    void testToCsvQuotesSpecialChars() {
+        def data = [[name: 'Alice, Jr.', note: 'said "hi"']]
+        def csv = CsvBuilder.toCsv(data)
+        assert csv.contains('"Alice, Jr."')
+        assert csv.contains('"said ""hi"""')
+    }
+
+    void testBuilderInstance() {
+        def builder = new CsvBuilder()
+        builder.call([[name: 'Alice', age: 30], [name: 'Bob', age: 25]])
+        def csv = builder.toString()
+        assert csv.contains('name,age')
+        assert csv.contains('Alice,30')
+    }
+
+    void testWritable() {
+        def builder = new CsvBuilder()
+        builder.call([[x: 1, y: 2]])
+        def out = new StringWriter()
+        out << builder
+        assert out.toString().contains('x,y')
+    }
+
+    void testRoundTrip() {
+        // tag::round_trip[]
+        def original = [[name: 'Alice', age: '30'], [name: 'Bob', age: '25']]
+        def csv = CsvBuilder.toCsv(original)
+        def parsed = new CsvSlurper().parseText(csv)
+        assert parsed[0].name == 'Alice'
+        assert parsed[1].age == '25'
+        // end::round_trip[]
+    }
+
+    // tag::typed_writing[]
+    static class Product {
+        String name
+        BigDecimal price
+    }
+    // end::typed_writing[]
+
+    void testToCsvFromTypedObjects() {
+        // tag::typed_writing_usage[]
+        def products = [new Product(name: 'Widget', price: 9.99),
+                        new Product(name: 'Gadget', price: 24.50)]
+        def csv = CsvBuilder.toCsv(products, Product)
+        assert csv.contains('name,price')
+        assert csv.contains('Widget,9.99')
+        assert csv.contains('Gadget,24.5')
+        // end::typed_writing_usage[]
+    }
+
+    void testTypedRoundTrip() {
+        def products = [new Product(name: 'Widget', price: 9.99)]
+        def csv = CsvBuilder.toCsv(products, Product)
+        def parsed = new CsvSlurper().parseAs(Product, csv)
+        assert parsed[0].name == 'Widget'
+        assert parsed[0].price == 9.99
+    }
+}
diff --git 
a/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvSlurperTest.groovy 
b/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvSlurperTest.groovy
new file mode 100644
index 0000000000..9a19dabcb7
--- /dev/null
+++ b/subprojects/groovy-csv/src/spec/test/groovy/csv/CsvSlurperTest.groovy
@@ -0,0 +1,106 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.csv
+
+import groovy.test.GroovyTestCase
+
+class CsvSlurperTest extends GroovyTestCase {
+
+    void testParseText() {
+        // tag::parse_text[]
+        def csv = new CsvSlurper().parseText('name,age\nAlice,30\nBob,25')
+        assert csv.size() == 2
+        assert csv[0].name == 'Alice'
+        assert csv[0].age == '30'
+        assert csv[1].name == 'Bob'
+        // end::parse_text[]
+    }
+
+    void testPropertyAccess() {
+        // tag::property_access[]
+        def csv = new CsvSlurper().parseText('''\
+            name,city,country
+            Alice,London,UK
+            Bob,Paris,France'''.stripIndent())
+        assert csv[0].city == 'London'
+        assert csv[1].country == 'France'
+        // end::property_access[]
+    }
+
+    void testCustomSeparator() {
+        // tag::custom_separator[]
+        def csv = new CsvSlurper().setSeparator((char) 
'\t').parseText('name\tage\nAlice\t30')
+        assert csv[0].name == 'Alice'
+        assert csv[0].age == '30'
+        // end::custom_separator[]
+    }
+
+    void testQuotedFields() {
+        // tag::quoted_fields[]
+        def csv = new CsvSlurper().parseText('name,note\nAlice,"hello, 
world"\nBob,"say ""hi"""')
+        assert csv[0].note == 'hello, world'
+        assert csv[1].note == 'say "hi"'
+        // end::quoted_fields[]
+    }
+
+    void testEmptyInput() {
+        def csv = new CsvSlurper().parseText('')
+        assert csv.isEmpty()
+    }
+
+    void testSingleRow() {
+        def csv = new CsvSlurper().parseText('name,age\nAlice,30')
+        assert csv.size() == 1
+        assert csv[0].name == 'Alice'
+    }
+
+    void testSemicolonSeparator() {
+        def csv = new CsvSlurper().setSeparator((char) 
';').parseText('name;age\nAlice;30')
+        assert csv[0].name == 'Alice'
+    }
+
+    void testParseFromReader() {
+        def reader = new StringReader('name,age\nAlice,30')
+        def csv = new CsvSlurper().parse(reader)
+        assert csv[0].name == 'Alice'
+    }
+
+    // tag::typed_parsing[]
+    static class Sale {
+        String customer
+        BigDecimal amount
+    }
+    // end::typed_parsing[]
+
+    void testTypedParsing() {
+        // tag::typed_parsing_usage[]
+        def sales = new CsvSlurper().parseAs(Sale, 
'customer,amount\nAcme,1500.00\nGlobex,250.50')
+        assert sales.size() == 2
+        assert sales[0].customer == 'Acme'
+        assert sales[0].amount == 1500.00
+        assert sales[1].customer == 'Globex'
+        // end::typed_parsing_usage[]
+    }
+
+    void testTypedParsingMultipleFields() {
+        def items = new CsvSlurper().parseAs(Sale, 
'customer,amount\nAlice,99.99')
+        assert items[0] instanceof Sale
+        assert items[0].amount instanceof BigDecimal
+    }
+}

Reply via email to