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
+ }
+}