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