Skip to content

Commit c2e844f

Browse files
authored
Merge pull request #39 from dump247/local-time
Add LocalTime scalar
2 parents 9df62b5 + 0377f56 commit c2e844f

File tree

5 files changed

+192
-1
lines changed

5 files changed

+192
-1
lines changed

readme.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ And use it in your schema
5656
`java.time.OffsetDateTime` objects at runtime
5757
* `Time`
5858
* An RFC-3339 compliant time scalar that accepts string values like `16:39:57-08:00` and produces
59-
`java.time.OffsetTime` objects at runtime
59+
`java.time.OffsetTime` objects at runtime
60+
* `LocalTime`
61+
* 24-hour clock time string in the format `hh:mm:ss.sss` or `hh:mm:ss` if partial seconds is zero and
62+
produces `java.time.LocalTime` objects at runtime.
6063
* `Date`
6164
* An RFC-3339 compliant date scalar that accepts string values like `1996-12-19` and produces
6265
`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 = TimeScalar.INSTANCE;
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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.mkLocalT
11+
12+
class LocalTimeScalarTest extends Specification {
13+
14+
def coercing = ExtendedScalars.LocalTime.getCoercing()
15+
16+
@Unroll
17+
def "localtime parseValue"() {
18+
19+
when:
20+
def result = coercing.parseValue(input)
21+
then:
22+
result == expectedValue
23+
where:
24+
input | expectedValue
25+
"23:20:50.123456789" | mkLocalT("23:20:50.123456789")
26+
"16:39:57.000000000" | mkLocalT("16:39:57")
27+
"16:39:57.0" | mkLocalT("16:39:57")
28+
"16:39:57" | mkLocalT("16:39:57")
29+
}
30+
31+
@Unroll
32+
def "localtime parseValue bad inputs"() {
33+
34+
when:
35+
coercing.parseValue(input)
36+
then:
37+
thrown(expectedValue)
38+
where:
39+
input | expectedValue
40+
"23:20:50.52Z" | CoercingParseValueException
41+
"16:39:57-08:00" | CoercingParseValueException
42+
666 || CoercingParseValueException
43+
}
44+
45+
def "localtime AST literal"() {
46+
47+
when:
48+
def result = coercing.parseLiteral(input)
49+
then:
50+
result == expectedValue
51+
where:
52+
input | expectedValue
53+
new StringValue("23:20:50.123456789") | mkLocalT("23:20:50.123456789")
54+
new StringValue("16:39:57.000000000") | mkLocalT("16:39:57")
55+
new StringValue("16:39:57.0") | mkLocalT("16:39:57")
56+
new StringValue("16:39:57") | mkLocalT("16:39:57")
57+
}
58+
59+
def "localtime serialisation"() {
60+
61+
when:
62+
def result = coercing.serialize(input)
63+
then:
64+
result == expectedValue
65+
where:
66+
input | expectedValue
67+
"23:20:50.123456789" | "23:20:50.123456789"
68+
"23:20:50" | "23:20:50"
69+
mkLocalT("16:39:57") | "16:39:57"
70+
mkLocalT("16:39:57.1") | "16:39:57.1"
71+
}
72+
73+
def "datetime serialisation bad inputs"() {
74+
75+
when:
76+
coercing.serialize(input)
77+
then:
78+
thrown(expectedValue)
79+
where:
80+
input | expectedValue
81+
"23:20:50.52Z" | CoercingSerializeException
82+
"16:39:57-08:00" | CoercingSerializeException
83+
666 || CoercingSerializeException
84+
}
85+
}

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

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

77
import java.time.LocalDate
88
import java.time.LocalDateTime
9+
import java.time.LocalTime
910
import java.time.OffsetDateTime
1011
import java.time.OffsetTime
1112
import java.time.ZoneId
@@ -30,6 +31,10 @@ class TestKit {
3031
OffsetTime.parse(s)
3132
}
3233

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

0 commit comments

Comments
 (0)