From 6ff7bca46fa900aa01bca7b5bcd3b0ebd1425f3b Mon Sep 17 00:00:00 2001 From: Anil Bhaskar Date: Sat, 27 May 2023 20:23:17 +0530 Subject: [PATCH] feat(local-date-time): fixes #7 add java.time.LocalDateTime support --- .../java/graphql/scalars/ExtendedScalars.java | 14 +++ .../scalars/datetime/LocalDateTimeScalar.java | 97 +++++++++++++++ .../datetime/LocalDateTimeScalarTest.groovy | 113 ++++++++++++++++++ .../graphql/scalars/util/TestKit.groovy | 4 + 4 files changed, 228 insertions(+) create mode 100644 src/main/java/graphql/scalars/datetime/LocalDateTimeScalar.java create mode 100644 src/test/groovy/graphql/scalars/datetime/LocalDateTimeScalarTest.groovy diff --git a/src/main/java/graphql/scalars/ExtendedScalars.java b/src/main/java/graphql/scalars/ExtendedScalars.java index ce36232..944c1da 100644 --- a/src/main/java/graphql/scalars/ExtendedScalars.java +++ b/src/main/java/graphql/scalars/ExtendedScalars.java @@ -6,6 +6,7 @@ import graphql.scalars.currency.CurrencyScalar; import graphql.scalars.datetime.DateScalar; import graphql.scalars.datetime.DateTimeScalar; +import graphql.scalars.datetime.LocalDateTimeScalar; import graphql.scalars.datetime.LocalTimeCoercing; import graphql.scalars.datetime.TimeScalar; import graphql.scalars.java.JavaPrimitives; @@ -45,6 +46,19 @@ public class ExtendedScalars { */ public static final GraphQLScalarType DateTime = DateTimeScalar.INSTANCE; + /** + * A date-time without a time-zone in the ISO-8601 calendar system, formatted as 2007-12-03 10:15:30. + * `java.time.LocalDateTime` objects at runtime. + *

+ * Its {@link graphql.schema.Coercing#serialize(java.lang.Object)} and {@link graphql.schema.Coercing#parseValue(java.lang.Object)} methods + * accept LocalDateTime and formatted Strings as valid objects. + *

+ * See the rfc3339 spec for more details on the format. + * + * @see java.time.LocalDateTime + */ + public static final GraphQLScalarType LocalDateTime = LocalDateTimeScalar.INSTANCE; + /** * An RFC-3339 compliant date scalar that accepts string values like `1996-12-19` and produces * `java.time.LocalDate` objects at runtime. diff --git a/src/main/java/graphql/scalars/datetime/LocalDateTimeScalar.java b/src/main/java/graphql/scalars/datetime/LocalDateTimeScalar.java new file mode 100644 index 0000000..ee8f7f5 --- /dev/null +++ b/src/main/java/graphql/scalars/datetime/LocalDateTimeScalar.java @@ -0,0 +1,97 @@ +package graphql.scalars.datetime; + +import graphql.Internal; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.util.function.Function; + +import static graphql.scalars.util.Kit.typeName; +import static java.lang.String.format; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.ChronoField.*; + +/** + * Access this via {@link graphql.scalars.ExtendedScalars#LocalDateTime} + */ +@Internal +public final class LocalDateTimeScalar { + + public static final GraphQLScalarType INSTANCE; + + private LocalDateTimeScalar() {} + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + static { + Coercing coercing = new Coercing() { + @Override + public String serialize(Object dataFetcherResult) throws CoercingSerializeException { + try { + LocalDateTime localDateTime = (LocalDateTime) dataFetcherResult; + return localDateTime.format(DATE_TIME_FORMATTER); + } catch (ClassCastException exception) { + throw new CoercingSerializeException("Input is not a LocalDateTime", exception); + } + } + + @Override + public LocalDateTime parseValue(Object input) throws CoercingParseValueException { + // Will be String if the value is specified via external variables object, and a StringValue + // if provided direct in the query. + if (input instanceof StringValue) { + return parseString(((StringValue) input).getValue()); + } + if (input instanceof String) { + return parseString((String) input); + } + if (input instanceof LocalDateTime) { + return (LocalDateTime) input; + } + throw new CoercingParseValueException(format("Unable to parse %s as LocalDateTime", input)); + } + + private LocalDateTime parseString(String input) { + try { + return LocalDateTime.parse(input, DATE_TIME_FORMATTER); + } catch (DateTimeParseException parseException) { + throw new CoercingParseValueException( + format("Unable to parse %s as LocalDateTime", input), parseException); + } + } + + @Override + public Value valueToLiteral(Object input) { + String s = serialize(input); + return StringValue.newStringValue(s).build(); + } + + @Override + public LocalDateTime parseLiteral(Object input) throws CoercingParseLiteralException { + try { + return parseValue(input); + } catch (CoercingParseValueException exception) { + throw new CoercingParseLiteralException(exception); + } + } + }; + + INSTANCE = GraphQLScalarType.newScalar() + .name("LocalDateTime") + .description("A date-time without a time-zone in the ISO-8601 calendar system, formatted as 2007-12-03 10:15:30") + .specifiedByUrl("https://scalars.graphql.org/andimarek/local-date-time") // TODO: Change to .specifiedByURL when builder added to graphql-java + .coercing(coercing) + .build(); + } +} diff --git a/src/test/groovy/graphql/scalars/datetime/LocalDateTimeScalarTest.groovy b/src/test/groovy/graphql/scalars/datetime/LocalDateTimeScalarTest.groovy new file mode 100644 index 0000000..2774771 --- /dev/null +++ b/src/test/groovy/graphql/scalars/datetime/LocalDateTimeScalarTest.groovy @@ -0,0 +1,113 @@ +package graphql.scalars.datetime + +import graphql.language.StringValue +import graphql.scalars.ExtendedScalars +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import spock.lang.Specification +import spock.lang.Unroll + +import java.time.LocalDateTime + +import static graphql.scalars.util.TestKit.* + +class LocalDateTimeScalarTest extends Specification { + + def coercing = ExtendedScalars.LocalDateTime.getCoercing() + + @Unroll + def "localDatetime parseValue"() { + + when: + def result = coercing.parseValue(input) + then: + result == expectedValue + where: + input | expectedValue + "1985-04-12 23:20:50" | mkLocalDT("1985-04-12T23:20:50") + "1985-04-12 23:20:50" | mkLocalDT("1985-04-12T23:20:50.000") + LocalDateTime.of(1985, 04, 12, 23, 20, 50) | mkLocalDT("1985-04-12T23:20:50.000") + LocalDateTime.of(1985, 04, 12, 23, 20) | mkLocalDT("1985-04-12T23:20:00") + } + + @Unroll + def "localDatetime parseLiteral"() { + + when: + def result = coercing.parseLiteral(input) + then: + result == expectedValue + where: + input | expectedValue + "1985-04-12 23:20:50" | mkLocalDT("1985-04-12T23:20:50.00") + "1996-12-19 16:39:57" | mkLocalDT("1996-12-19T16:39:57") + new StringValue("1996-12-19 16:39:57") | mkLocalDT("1996-12-19T16:39:57") + LocalDateTime.of(1996, 12, 19, 16, 39, 57) | mkLocalDT("1996-12-19T16:39:57") + } + + @Unroll + def "localDatetime valueToLiteral"() { + + when: + def result = coercing.valueToLiteral(input) + then: + result.isEqualTo(expectedValue) + where: + input | expectedValue + LocalDateTime.of(1996, 12, 19, 16, 39, 57) | new StringValue("1996-12-19 16:39:57") + } + + @Unroll + def "localDatetime parseValue bad inputs"() { + + when: + coercing.parseValue(input) + then: + thrown(expectedValue) + where: + input | expectedValue + "1985-04-12" | CoercingParseValueException + "2022-11-24T01:00:01.02-00:00" | CoercingParseValueException + mkOffsetDT("1985-04-12T23:20:50.52Z") | CoercingParseValueException + 666 || CoercingParseValueException + } + + def "localDatetime serialisation"() { + + when: + def result = coercing.serialize(input) + then: + result == expectedValue + where: + input | expectedValue + LocalDateTime.of(1996, 12, 19, 16, 39, 57) | "1996-12-19 16:39:57" + } + + def "localDatetime serialisation bad inputs"() { + + when: + coercing.serialize(input) + then: + thrown(expectedValue) + where: + input | expectedValue + "1985-04-12" | CoercingSerializeException + "2022-11-24T01:00:01.02-00:00" | CoercingSerializeException + mkOffsetDT(year: 1980, hour: 3) | CoercingSerializeException + 666 || CoercingSerializeException + } + + @Unroll + def "localDatetime parseLiteral bad inputs"() { + + when: + coercing.parseLiteral(input) + then: + thrown(expectedValue) + where: + input | expectedValue + "2022-11-24T01:00:01.02-00:00" | CoercingParseLiteralException + } + +} diff --git a/src/test/groovy/graphql/scalars/util/TestKit.groovy b/src/test/groovy/graphql/scalars/util/TestKit.groovy index 2a96dee..e88b445 100644 --- a/src/test/groovy/graphql/scalars/util/TestKit.groovy +++ b/src/test/groovy/graphql/scalars/util/TestKit.groovy @@ -28,6 +28,10 @@ class TestKit { OffsetDateTime.parse(s) } + static LocalDateTime mkLocalDT(String s) { + LocalDateTime.parse(s) + } + static OffsetTime mkOffsetT(String s) { OffsetTime.parse(s) }