diff --git a/readme.md b/readme.md index 65bd34c..def76bb 100644 --- a/readme.md +++ b/readme.md @@ -93,6 +93,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` instances at runtime. ## Object / JSON Scalars diff --git a/src/main/java/graphql/scalars/ExtendedScalars.java b/src/main/java/graphql/scalars/ExtendedScalars.java index 6ea2ac9..557df81 100644 --- a/src/main/java/graphql/scalars/ExtendedScalars.java +++ b/src/main/java/graphql/scalars/ExtendedScalars.java @@ -8,6 +8,7 @@ import graphql.scalars.datetime.TimeScalar; import graphql.scalars.java.JavaPrimitives; import graphql.scalars.locale.LocaleScalar; +import graphql.scalars.id.UUIDScalar; import graphql.scalars.numeric.NegativeFloatScalar; import graphql.scalars.numeric.NegativeIntScalar; import graphql.scalars.numeric.NonNegativeFloatScalar; @@ -22,6 +23,8 @@ import graphql.scalars.url.UrlScalar; import graphql.schema.GraphQLScalarType; +import java.util.UUID; + /** * This is the API entry point for all the extended scalars */ @@ -135,6 +138,12 @@ public class ExtendedScalars { */ public static GraphQLScalarType Locale = LocaleScalar.INSTANCE; + /** + * A UUID scalar that accepts a universally unique identifier and produces {@link + * java.util.UUID} objects at runtime. + */ + public static GraphQLScalarType UUID = UUIDScalar.INSTANCE; + /** * 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..79868d9 --- /dev/null +++ b/src/main/java/graphql/scalars/id/UUIDScalar.java @@ -0,0 +1,94 @@ +package graphql.scalars.id; + +import graphql.Internal; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.scalars.util.Kit; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +import java.util.UUID; + +import static graphql.scalars.util.Kit.typeName; + +/** + * Access this via {@link graphql.scalars.ExtendedScalars#UUID} + */ +@Internal +public class UUIDScalar { + + public static GraphQLScalarType INSTANCE; + + static { + Coercing coercing = new Coercing() { + private UUID convertImpl(Object input) { + if (input instanceof String) { + try { + return (UUID.fromString((String) input)); + } catch (IllegalArgumentException ex) { + return null; + } + } else if (input instanceof UUID) { + return (UUID) input; + } + return null; + } + + @Override + public String serialize(Object input) throws CoercingSerializeException { + UUID result = convertImpl(input); + if (result == null) { + throw new CoercingSerializeException( + "Expected type 'UUID' but was '" + Kit.typeName(input) + "'." + ); + } + return result.toString(); + } + + @Override + public UUID parseValue(Object input) throws CoercingParseValueException { + UUID result = convertImpl(input); + if (result == null) { + throw new CoercingParseValueException( + "Expected type 'UUID' but was '" + Kit.typeName(input) + "'." + ); + } + return result; + } + + @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" + ); + } + } + + @Override + public Value valueToLiteral(Object input) { + String s = serialize(input); + return StringValue.newStringValue(s).build(); + } + }; + + + INSTANCE = GraphQLScalarType.newScalar() + .name("UUID") + .description("A universally unique identifier compliant UUID Scalar") + .coercing(coercing) + .build(); + + } + +} 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..f9c1e91 --- /dev/null +++ b/src/test/groovy/graphql/scalars/id/UUIDScalarTest.groovy @@ -0,0 +1,105 @@ +package graphql.scalars.id + +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 static graphql.scalars.util.TestKit.mkStringValue +import static graphql.scalars.util.TestKit.mkUUIDValue + +class UUIDScalarTest extends Specification { + + def coercing = ExtendedScalars.UUID.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 + } + + @Unroll + def "UUID valueToLiteral"() { + + when: + def result = coercing.valueToLiteral(input) + then: + result.isEqualTo(expectedValue) + where: + input | expectedValue + "42287d47-c5bd-45e4-b470-53e426d3d503" | mkStringValue("42287d47-c5bd-45e4-b470-53e426d3d503") + "423df0f3-cf05-4eb5-b708-ae2f4b4a052d" | mkStringValue("423df0f3-cf05-4eb5-b708-ae2f4b4a052d") + mkUUIDValue("6a90b1e6-20f3-43e5-a7ba-34db8010c071") | mkStringValue("6a90b1e6-20f3-43e5-a7ba-34db8010c071") + } +} diff --git a/src/test/groovy/graphql/scalars/util/TestKit.groovy b/src/test/groovy/graphql/scalars/util/TestKit.groovy index f3a9460..60a2982 100644 --- a/src/test/groovy/graphql/scalars/util/TestKit.groovy +++ b/src/test/groovy/graphql/scalars/util/TestKit.groovy @@ -85,4 +85,8 @@ class TestKit { return new FloatValue(new BigDecimal(d)) } + static UUID mkUUIDValue(String s) { + return UUID.fromString(s) + } + }