diff --git a/readme.md b/readme.md index 75436e8..835e546 100644 --- a/readme.md +++ b/readme.md @@ -73,6 +73,11 @@ And example query might look like: } ``` +## ID Scalars + +* `UUID` + * A universally unique identifier scalar that accepts uuid values like `2423f0a0-3b81-4115-a189-18df8b35e8fc` and produces + `java.util.UUID` type at runtime ## Object / JSON Scalars diff --git a/src/main/java/graphql/scalars/ExtendedScalars.java b/src/main/java/graphql/scalars/ExtendedScalars.java index 2d15434..5ca2aac 100644 --- a/src/main/java/graphql/scalars/ExtendedScalars.java +++ b/src/main/java/graphql/scalars/ExtendedScalars.java @@ -5,6 +5,7 @@ import graphql.scalars.datetime.DateScalar; import graphql.scalars.datetime.DateTimeScalar; import graphql.scalars.datetime.TimeScalar; +import graphql.scalars.id.UUIDScalar; import graphql.scalars.numeric.NegativeFloatScalar; import graphql.scalars.numeric.NegativeIntScalar; import graphql.scalars.numeric.NonNegativeFloatScalar; @@ -20,6 +21,8 @@ import graphql.scalars.locale.LocaleScalar; import graphql.schema.GraphQLScalarType; +import java.util.UUID; + /** * This is the API entry point for all the extended scalars */ @@ -118,6 +121,12 @@ public class ExtendedScalars { */ public static GraphQLScalarType Locale = new LocaleScalar(); + /** + * A UUID scalar that accepts a universally unique identifier and produces {@link + * java.util.UUID} objects at runtime. + */ + public static GraphQLScalarType UUID = new UUIDScalar(); + /** * An `Int` scalar that MUST be greater than zero * diff --git a/src/main/java/graphql/scalars/id/UUIDScalar.java b/src/main/java/graphql/scalars/id/UUIDScalar.java new file mode 100644 index 0000000..29cde68 --- /dev/null +++ b/src/main/java/graphql/scalars/id/UUIDScalar.java @@ -0,0 +1,87 @@ +package graphql.scalars.id; + +import graphql.Internal; +import graphql.language.StringValue; +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.format.DateTimeFormatter; +import java.util.UUID; + +import static graphql.scalars.util.Kit.typeName; + +/** + * Access this via {@link graphql.scalars.ExtendedScalars#UUID} + */ +@Internal +public class UUIDScalar extends GraphQLScalarType { + + public UUIDScalar() { + super("UUID", "A universally unique identifier compliant UUID Scalar", new Coercing() { + @Override + public String serialize(Object input) throws CoercingSerializeException { + if (input instanceof String) { + try { + return (UUID.fromString((String)input)).toString(); + } catch ( IllegalArgumentException ex) { + throw new CoercingSerializeException( + "Expected a UUID value that can be converted : '" + ex.getMessage() + "'." + ); + } + } + else if(input instanceof UUID) { + return input.toString(); + } + else { + throw new CoercingSerializeException( + "Expected something we can convert to 'java.util.UUID' but was '" + typeName(input) + "'." + ); + } + } + + @Override + public UUID parseValue(Object input) throws CoercingParseValueException { + if(input instanceof String) { + try { + return UUID.fromString((String) input); + } catch (IllegalArgumentException ex) { + throw new CoercingParseValueException( + "Expected a 'String' of UUID type but was '" + typeName(input) + "'." + ); + } + } + else if(input instanceof UUID) { + return (UUID) input; + } + else { + throw new CoercingParseValueException( + "Expected a 'String' or 'UUID' type but was '" + typeName(input) + "'." + ); + } + } + + @Override + public UUID parseLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException( + "Expected a 'java.util.UUID' AST type object but was '" + typeName(input) + "'." + ); + } + try { + return UUID.fromString(((StringValue) input).getValue()); + } catch (IllegalArgumentException ex) { + throw new CoercingParseLiteralException( + "Expected something that we can convert to a UUID but was invalid" + ); + } + + } + + }); + } + +} diff --git a/src/test/groovy/graphql/scalars/id/UUIDScalarTest.groovy b/src/test/groovy/graphql/scalars/id/UUIDScalarTest.groovy new file mode 100644 index 0000000..043a74d --- /dev/null +++ b/src/test/groovy/graphql/scalars/id/UUIDScalarTest.groovy @@ -0,0 +1,90 @@ +package graphql.scalars.id + +import graphql.language.StringValue +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import spock.lang.Specification +import spock.lang.Unroll + +import static graphql.scalars.util.TestKit.* + +class UUIDScalarTest extends Specification { + + def coercing = new UUIDScalar().getCoercing() + + @Unroll + def "UUID parseValue"() { + + when: + def result = coercing.parseValue(input) + then: + result == expectedValue + where: + input | expectedValue + "43f20307-603c-4ad1-83c6-6010d224fabf" | mkUUIDValue("43f20307-603c-4ad1-83c6-6010d224fabf") + "787dbc2b-3ddb-4098-ad1d-63d026bac111" | mkUUIDValue("787dbc2b-3ddb-4098-ad1d-63d026bac111") + } + + @Unroll + def "UUID parseValue bad inputs"() { + + when: + coercing.parseValue(input) + then: + thrown(expectedValue) + where: + input | expectedValue + "a-string-that-is-not-uuid" | CoercingParseValueException + 100 | CoercingParseValueException + "1985-04-12" | CoercingParseValueException + } + + def "UUID AST literal"() { + + when: + def result = coercing.parseLiteral(input) + then: + result == expectedValue + where: + input | expectedValue + new StringValue("6972117d-3963-4214-ab2c-fa973d7e996b") | mkUUIDValue("6972117d-3963-4214-ab2c-fa973d7e996b") + } + + def "UUID AST literal bad inputs"() { + + when: + coercing.parseLiteral(input) + then: + thrown(expectedValue) + where: + input | expectedValue + new StringValue("a-string-that-us-not-uuid") | CoercingParseLiteralException + } + + def "UUID serialization"() { + + when: + def result = coercing.serialize(input) + then: + result == expectedValue + where: + input | expectedValue + "42287d47-c5bd-45e4-b470-53e426d3d503" | "42287d47-c5bd-45e4-b470-53e426d3d503" + "423df0f3-cf05-4eb5-b708-ae2f4b4a052d" | "423df0f3-cf05-4eb5-b708-ae2f4b4a052d" + mkUUIDValue("6a90b1e6-20f3-43e5-a7ba-34db8010c071") | "6a90b1e6-20f3-43e5-a7ba-34db8010c071" + } + + def "UUID serialization bad inputs"() { + + when: + coercing.serialize(input) + then: + thrown(expectedValue) + where: + input | expectedValue + "1985-04-12" | CoercingSerializeException + 100 | CoercingSerializeException + } + +} diff --git a/src/test/groovy/graphql/scalars/util/TestKit.groovy b/src/test/groovy/graphql/scalars/util/TestKit.groovy index 35ca49e..142902d 100644 --- a/src/test/groovy/graphql/scalars/util/TestKit.groovy +++ b/src/test/groovy/graphql/scalars/util/TestKit.groovy @@ -75,4 +75,8 @@ class TestKit { return new FloatValue(new BigDecimal(d)) } + static UUID mkUUIDValue(String s) { + return UUID.fromString(s) + } + }