Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions subprojects/groovy-toml/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ plugins {
dependencies {
api rootProject // TomlBuilder extends GroovyObjectSupport...
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-toml:${versions.jackson}"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions.jackson}"
implementation projects.groovyJson
testImplementation projects.groovyTest
testRuntimeOnly projects.groovyAnt // for JavadocAssertionTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package groovy.toml;

import com.fasterxml.jackson.dataformat.toml.TomlMapper;
import groovy.json.JsonBuilder;
import groovy.lang.Closure;
import groovy.lang.GroovyObjectSupport;
Expand Down Expand Up @@ -59,7 +58,7 @@ public TomlBuilder() {
*/
public static String toToml(Object object) {
try {
return new TomlMapper().writeValueAsString(object);
return TomlSlurper.mapper().writeValueAsString(object);
} catch (IOException e) {
throw new TomlRuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
*/
package groovy.toml;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import groovy.json.JsonSlurper;
import org.apache.groovy.lang.annotation.Incubating;
import org.apache.groovy.toml.util.TomlConverter;
Expand Down Expand Up @@ -126,12 +129,23 @@ public <T> T parseTextAs(Class<T> type, String toml) {
*/
public <T> T parseAs(Class<T> type, Reader reader) {
try {
return new TomlMapper().readValue(reader, type);
return mapper().readValue(reader, type);
} catch (IOException e) {
throw new TomlRuntimeException(e);
}
}

// TomlMapper is thread-safe once configured, so a single shared instance is reused
private static final TomlMapper MAPPER = TomlMapper.builder()
.addModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.build();

static TomlMapper mapper() {
return MAPPER;
}

/**
* Parse TOML from an input stream into a typed object.
*
Expand Down
31 changes: 29 additions & 2 deletions subprojects/groovy-toml/src/spec/doc/toml-userguide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,24 @@ The following table gives an overview of the TOML types and the corresponding Gr
|null
|`null`

|date
|`java.util.Date` based on the `yyyy-MM-dd'T'HH:mm:ssZ` date format
|offset/local date-time, local date, local time
|`java.lang.String` (see the note below)
|===

[NOTE]
Whenever a value in TOML is `null`, `TomlSlurper` supplements it with the Groovy `null` value. This is in contrast to other
TOML parsers that represent a `null` value with a library-provided singleton object.

[NOTE]
====
For the untyped `parse`/`parseText` API, TOML date and time values are returned as
`String`. This is a limitation of the underlying Jackson TOML support, which only
surfaces `java.time.*` types when a target type is supplied. Use the typed
`parseAs`/`parseTextAs` API (see <<toml_typed_temporal>>) for `java.time.LocalDate`,
`java.time.LocalTime`, `java.time.LocalDateTime`, and `java.time.OffsetDateTime`
fidelity.
====

=== Typed parsing

`TomlSlurper` can parse TOML directly into typed objects using Jackson databinding.
Expand All @@ -111,6 +121,23 @@ NOTE: For simple cases, Groovy's `as` coercion also works with the untyped resul
The `parseTextAs` method uses Jackson databinding, which supports richer annotation-driven
mapping via `@JsonProperty`, `@JsonFormat`, etc.

[[toml_typed_temporal]]
=== Typed date and time values

When a target type declares `java.time.*` fields, the typed `parseAs`/`parseTextAs`
API and `TomlBuilder.toToml` round-trip TOML temporal values with full fidelity,
including the original zone offset for `OffsetDateTime`:

[source,groovy]
----
include::../test/groovy/toml/TomlParserTest.groovy[tags=temporal_class,indent=0]
----

[source,groovy]
----
include::../test/groovy/toml/TomlParserTest.groovy[tags=temporal_typed,indent=0]
----

=== Builders

Another way to create TOML from Groovy is to use `TomlBuilder`. The builder provide a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,61 @@ port = 8080
def config = new TomlSlurper().parseAs(ServerConfig, stream)
assert config.port == 3000
}

// tag::temporal_class[]
static class Event {
java.time.OffsetDateTime created
java.time.LocalDate effective
java.time.LocalTime windowStart
java.time.LocalDateTime updated
String name
Comment thread
paulk-asert marked this conversation as resolved.
}
// end::temporal_class[]

@Test
void testTemporalTypedRoundTrip() {
// tag::temporal_typed[]
def original = new Event(
created: java.time.OffsetDateTime.parse('1979-05-27T07:32:00-08:00'),
effective: java.time.LocalDate.of(1979, 5, 27),
windowStart: java.time.LocalTime.of(7, 32, 0),
updated: java.time.LocalDateTime.of(1979, 5, 27, 7, 32, 0),
name: 'demo')

def toml = groovy.toml.TomlBuilder.toToml(original)
def parsed = new TomlSlurper().parseTextAs(Event, toml)

assert parsed.created == original.created // OffsetDateTime, non-UTC offset preserved
assert parsed.effective == original.effective // LocalDate
assert parsed.windowStart == original.windowStart // LocalTime
assert parsed.updated == original.updated // LocalDateTime
assert parsed.name == original.name
// end::temporal_typed[]
assert parsed.created instanceof java.time.OffsetDateTime
assert parsed.effective instanceof java.time.LocalDate
assert parsed.windowStart instanceof java.time.LocalTime
assert parsed.updated instanceof java.time.LocalDateTime
// the round-tripped offset must remain -08:00, not normalised to Z
assert parsed.created.offset == java.time.ZoneOffset.ofHours(-8)
}

@Test
void testTemporalUntypedStringPath() {
// KNOWN LIMITATION: the untyped slurp path returns TOML temporal values
// as String, not java.time.* types. jackson-dataformat-toml's Object-mode
// deserialization does not surface temporal types unless a target type is
// supplied. Use the typed parseAs/parseTextAs API for java.time.* fidelity.
// This test pins the current behaviour so a future change here is deliberate.
def parsed = new TomlSlurper().parseText('''
created = 1979-05-27T07:32:00-08:00
effective = 1979-05-27
windowStart = 07:32:00
''')
assert parsed.created == '1979-05-27T07:32:00-08:00'
assert parsed.effective == '1979-05-27'
assert parsed.windowStart == '07:32:00'
assert parsed.created instanceof String
assert parsed.effective instanceof String
assert parsed.windowStart instanceof String
}
}
Loading