Skip to content

Commit ea2fa01

Browse files
committed
Add LocalTime scalar
1 parent f03414a commit ea2fa01

File tree

5 files changed

+195
-1
lines changed

5 files changed

+195
-1
lines changed

readme.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ And use it in your schema
5454
`java.time.OffsetDateTime` objects at runtime
5555
* `Time`
5656
* An RFC-3339 compliant time scalar that accepts string values like `16:39:57-08:00` and produces
57-
`java.time.OffsetTime` objects at runtime
57+
`java.time.OffsetTime` objects at runtime
58+
* `LocalTime`
59+
* 24-hour clock time string in the format `hh:mm:ss.sss` or `hh:mm:ss` if partial seconds is zero and
60+
produces `java.time.LocalTime` objects at runtime.
5861
* `Date`
5962
* An RFC-3339 compliant date scalar that accepts string values like `1996-12-19` and produces
6063
`java.time.LocalDate` objects at runtime

src/main/java/graphql/scalars/ExtendedScalars.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import graphql.scalars.alias.AliasedScalar;
55
import graphql.scalars.datetime.DateScalar;
66
import graphql.scalars.datetime.DateTimeScalar;
7+
import graphql.scalars.datetime.LocalTimeCoercing;
78
import graphql.scalars.datetime.TimeScalar;
89
import graphql.scalars.java.JavaPrimitives;
910
import graphql.scalars.locale.LocaleScalar;
@@ -66,6 +67,21 @@ public class ExtendedScalars {
6667
*/
6768
public static GraphQLScalarType Time = new TimeScalar();
6869

70+
/**
71+
* A 24-hour local time scalar that accepts strings like `hh:mm:ss` and `hh:mm:ss.sss` and produces
72+
* `java.time.LocalTime` objects at runtime.
73+
* <p>
74+
* Its {@link graphql.schema.Coercing#serialize(java.lang.Object)} and {@link graphql.schema.Coercing#parseValue(java.lang.Object)} methods
75+
* accept time {@link java.time.temporal.TemporalAccessor}s and formatted Strings as valid objects.
76+
*
77+
* @see java.time.LocalTime
78+
*/
79+
public static GraphQLScalarType LocalTime = GraphQLScalarType.newScalar()
80+
.name("LocalTime")
81+
.description("24-hour clock time value string in the format `hh:mm:ss` or `hh:mm:ss.sss`.")
82+
.coercing(new LocalTimeCoercing())
83+
.build();
84+
6985
/**
7086
* An object scalar allows you to have a multi level data value without defining it in the graphql schema.
7187
* <p>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package graphql.scalars.datetime;
2+
3+
import graphql.language.StringValue;
4+
import graphql.schema.Coercing;
5+
import graphql.schema.CoercingParseLiteralException;
6+
import graphql.schema.CoercingParseValueException;
7+
import graphql.schema.CoercingSerializeException;
8+
9+
import java.time.DateTimeException;
10+
import java.time.LocalTime;
11+
import java.time.format.DateTimeFormatter;
12+
import java.time.format.DateTimeParseException;
13+
import java.time.temporal.TemporalAccessor;
14+
import java.util.function.Function;
15+
16+
import static graphql.scalars.util.Kit.typeName;
17+
18+
public class LocalTimeCoercing implements Coercing<LocalTime, String> {
19+
20+
private final static DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_LOCAL_TIME;
21+
22+
@Override
23+
public String serialize(final Object input) throws CoercingSerializeException {
24+
TemporalAccessor temporalAccessor;
25+
if (input instanceof TemporalAccessor) {
26+
temporalAccessor = (TemporalAccessor) input;
27+
} else if (input instanceof String) {
28+
temporalAccessor = parseTime(input.toString(), CoercingSerializeException::new);
29+
} else {
30+
throw new CoercingSerializeException(
31+
"Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + typeName(input) + "'."
32+
);
33+
}
34+
try {
35+
return dateFormatter.format(temporalAccessor);
36+
} catch (DateTimeException e) {
37+
throw new CoercingSerializeException(
38+
"Unable to turn TemporalAccessor into full time because of : '" + e.getMessage() + "'."
39+
);
40+
}
41+
}
42+
43+
@Override
44+
public LocalTime parseValue(final Object input) throws CoercingParseValueException {
45+
TemporalAccessor temporalAccessor;
46+
if (input instanceof TemporalAccessor) {
47+
temporalAccessor = (TemporalAccessor) input;
48+
} else if (input instanceof String) {
49+
temporalAccessor = parseTime(input.toString(), CoercingParseValueException::new);
50+
} else {
51+
throw new CoercingParseValueException(
52+
"Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + typeName(input) + "'."
53+
);
54+
}
55+
try {
56+
return LocalTime.from(temporalAccessor);
57+
} catch (DateTimeException e) {
58+
throw new CoercingParseValueException(
59+
"Unable to turn TemporalAccessor into full time because of : '" + e.getMessage() + "'."
60+
);
61+
}
62+
}
63+
64+
@Override
65+
public LocalTime parseLiteral(final Object input) throws CoercingParseLiteralException {
66+
if (!(input instanceof StringValue)) {
67+
throw new CoercingParseLiteralException(
68+
"Expected AST type 'StringValue' but was '" + typeName(input) + "'."
69+
);
70+
}
71+
return parseTime(((StringValue) input).getValue(), CoercingParseLiteralException::new);
72+
}
73+
74+
private static LocalTime parseTime(String s, Function<String, RuntimeException> exceptionMaker) {
75+
try {
76+
TemporalAccessor temporalAccessor = dateFormatter.parse(s);
77+
return LocalTime.from(temporalAccessor);
78+
} catch (DateTimeParseException e) {
79+
throw exceptionMaker.apply("Invalid local time value : '" + s + "'. because of : '" + e.getMessage() + "'");
80+
}
81+
}
82+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package graphql.scalars.datetime
2+
3+
import graphql.language.StringValue
4+
import graphql.scalars.ExtendedScalars
5+
import graphql.schema.CoercingParseValueException
6+
import graphql.schema.CoercingSerializeException
7+
import spock.lang.Specification
8+
import spock.lang.Unroll
9+
10+
import static graphql.scalars.util.TestKit.mkLocalDT
11+
import static graphql.scalars.util.TestKit.mkLocalT
12+
13+
class LocalTimeScalarTest extends Specification {
14+
15+
def coercing = ExtendedScalars.LocalTime
16+
17+
@Unroll
18+
def "localtime parseValue"() {
19+
20+
when:
21+
def result = coercing.parseValue(input)
22+
then:
23+
result == expectedValue
24+
where:
25+
input | expectedValue
26+
"23:20:50.123456789" | mkLocalT("23:20:50.123456789")
27+
"16:39:57.000000000" | mkLocalT("16:39:57")
28+
"16:39:57.0" | mkLocalT("16:39:57")
29+
"16:39:57" | mkLocalT("16:39:57")
30+
}
31+
32+
@Unroll
33+
def "localtime parseValue bad inputs"() {
34+
35+
when:
36+
coercing.parseValue(input)
37+
then:
38+
thrown(expectedValue)
39+
where:
40+
input | expectedValue
41+
"23:20:50.52Z" | CoercingParseValueException
42+
"16:39:57-08:00" | CoercingParseValueException
43+
mkLocalDT(year: 1980, hour: 3) | CoercingParseValueException
44+
666 || CoercingParseValueException
45+
}
46+
47+
def "localtime AST literal"() {
48+
49+
when:
50+
def result = coercing.parseLiteral(input)
51+
then:
52+
result == expectedValue
53+
where:
54+
input | expectedValue
55+
new StringValue("23:20:50.123456789") | mkLocalT("23:20:50.123456789")
56+
new StringValue("16:39:57.000000000") | mkLocalT("16:39:57")
57+
new StringValue("16:39:57.0") | mkLocalT("16:39:57")
58+
new StringValue("16:39:57") | mkLocalT("16:39:57")
59+
}
60+
61+
def "localtime serialisation"() {
62+
63+
when:
64+
def result = coercing.serialize(input)
65+
then:
66+
result == expectedValue
67+
where:
68+
input | expectedValue
69+
"23:20:50.123456789" | "23:20:50.123456789"
70+
"23:20:50" | "16:39:57-08:00"
71+
mkLocalT("16:39:57") | "16:39:57"
72+
mkLocalT("16:39:57.1") | "16:39:57.1"
73+
}
74+
75+
def "datetime serialisation bad inputs"() {
76+
77+
when:
78+
coercing.serialize(input)
79+
then:
80+
thrown(expectedValue)
81+
where:
82+
input | expectedValue
83+
"23:20:50.52Z" | CoercingSerializeException
84+
"16:39:57-08:00" | CoercingSerializeException
85+
mkLocalDT(year: 1980, hour: 3) | CoercingSerializeException
86+
666 || CoercingSerializeException
87+
}
88+
}

src/test/groovy/graphql/scalars/util/TestKit.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import graphql.language.IntValue
55

66
import java.time.LocalDate
77
import java.time.LocalDateTime
8+
import java.time.LocalTime
89
import java.time.OffsetDateTime
910
import java.time.OffsetTime
1011
import java.time.ZoneId
@@ -29,6 +30,10 @@ class TestKit {
2930
OffsetTime.parse(s)
3031
}
3132

33+
static LocalTime mkLocalT(String s) {
34+
LocalTime.parse(s)
35+
}
36+
3237
static OffsetDateTime mkOffsetDT(args) {
3338
OffsetDateTime.of(args.year ?: 1969, args.month ?: 8, args.day ?: 8, args.hour ?: 11,
3439
args.min ?: 10, args.secs ?: 9, args.nanos ?: 0, ZoneOffset.ofHours(10))

0 commit comments

Comments
 (0)