diff --git a/src/main/java/graphql/scalars/ExtendedScalars.java b/src/main/java/graphql/scalars/ExtendedScalars.java index e038c30..4c1a885 100644 --- a/src/main/java/graphql/scalars/ExtendedScalars.java +++ b/src/main/java/graphql/scalars/ExtendedScalars.java @@ -28,6 +28,7 @@ import graphql.scalars.object.JsonScalar; import graphql.scalars.object.ObjectScalar; import graphql.scalars.regex.RegexScalar; +import graphql.scalars.uri.UriScalar; import graphql.scalars.url.UrlScalar; import graphql.schema.GraphQLScalarType; @@ -209,6 +210,11 @@ public class ExtendedScalars { */ public static final GraphQLScalarType Json = JsonScalar.INSTANCE; + /** + * A URI scalar that accepts URI strings and produces {@link java.net.URI} objects at runtime + */ + public static final GraphQLScalarType Uri = UriScalar.INSTANCE; + /** * A URL scalar that accepts URL strings and produces {@link java.net.URL} objects at runtime */ diff --git a/src/main/java/graphql/scalars/uri/UriScalar.java b/src/main/java/graphql/scalars/uri/UriScalar.java new file mode 100644 index 0000000..a2c7592 --- /dev/null +++ b/src/main/java/graphql/scalars/uri/UriScalar.java @@ -0,0 +1,114 @@ +package graphql.scalars.uri; + +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +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.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; + +import static graphql.scalars.util.Kit.typeName; + +@Internal +public final class UriScalar { + + private UriScalar() { + } + + public static final GraphQLScalarType INSTANCE; + + static { + Coercing coercing = new Coercing<>() { + @Override + public URI serialize(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingSerializeException { + Optional uri; + if (input instanceof String) { + uri = Optional.of(parseURI(input.toString(), CoercingSerializeException::new)); + } else { + uri = toURI(input); + } + if (uri.isPresent()) { + return uri.get(); + } + throw new CoercingSerializeException( + "Expected a 'URI' like object but was '" + typeName(input) + "'." + ); + } + + @Override + public URI parseValue(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingParseValueException { + String uriStr; + if (input instanceof String) { + uriStr = String.valueOf(input); + } else { + Optional uri = toURI(input); + if (uri.isEmpty()) { + throw new CoercingParseValueException( + "Expected a 'URI' like object but was '" + typeName(input) + "'." + ); + } + return uri.get(); + } + return parseURI(uriStr, CoercingParseValueException::new); + } + + @Override + public URI parseLiteral(Value input, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException( + "Expected AST type 'StringValue' but was '" + typeName(input) + "'." + ); + } + return parseURI(((StringValue) input).getValue(), CoercingParseLiteralException::new); + } + + @Override + public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { + URI uri = serialize(input, graphQLContext, locale); + return StringValue.newStringValue(uri.toString()).build(); + } + + + private URI parseURI(String input, Function exceptionMaker) { + try { + return new URI(input); + } catch (URISyntaxException e) { + throw exceptionMaker.apply("Invalid URI value : '" + input + "'."); + } + } + }; + + INSTANCE = GraphQLScalarType.newScalar() + .name("Uri") + .description("A Uri scalar") + .coercing(coercing) + .build(); + } + + private static Optional toURI(Object input) { + if (input instanceof URI) { + return Optional.of((URI) input); + } else if (input instanceof URL) { + try { + return Optional.of(((URL) input).toURI()); + } catch (URISyntaxException ignored) { + } + } else if (input instanceof File) { + return Optional.of(((File) input).toURI()); + } + return Optional.empty(); + } + +} diff --git a/src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy b/src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy new file mode 100644 index 0000000..efc03fd --- /dev/null +++ b/src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy @@ -0,0 +1,111 @@ +package graphql.scalars.uri + + +import graphql.language.BooleanValue +import graphql.language.StringValue +import graphql.scalars.ExtendedScalars +import graphql.scalars.util.AbstractScalarTest +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import spock.lang.Unroll + +import static graphql.scalars.util.TestKit.mkStringValue + +class UriScalarTest extends AbstractScalarTest { + + def coercing = ExtendedScalars.Uri.getCoercing() + + @Unroll + def "test serialize"() { + + when: + def result = coercing.serialize(input, graphQLContext, locale) + then: + result == expectedResult + where: + input | expectedResult + new URL("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") + new URI("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") + new File("/this/that") | new URI("file:/this/that") + "http://www.graphql-java.com/" | new URI("http://www.graphql-java.com/") + } + + @Unroll + def "test valueToLiteral"() { + + when: + def result = coercing.valueToLiteral(input, graphQLContext, locale) + then: + result.isEqualTo(expectedResult) + where: + input | expectedResult + new URL("http://www.graphql-java.com/") | mkStringValue("http://www.graphql-java.com/") + new URI("http://www.graphql-java.com/") | mkStringValue("http://www.graphql-java.com/") + new File("/this/that") | mkStringValue("file:/this/that") + "http://www.graphql-java.com/" | mkStringValue("http://www.graphql-java.com/") + } + + @Unroll + def "test serialize bad inputs"() { + when: + coercing.serialize(input, graphQLContext, locale) + then: + thrown(exceptionClas) + where: + input || exceptionClas + 666 || CoercingSerializeException + "1:not/a/uri" || CoercingSerializeException + } + + @Unroll + def "test parseValue"() { + when: + def result = coercing.parseValue(input, graphQLContext, locale) + then: + result == expectedResult + where: + input | expectedResult + new URL("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") + new URI("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") + new File("/this/that") | new URI("file:/this/that") + "http://www.graphql-java.com/" | new URI("http://www.graphql-java.com/") + } + + @Unroll + def "test parseValue bad inputs"() { + when: + coercing.parseValue(input, graphQLContext, locale) + then: + thrown(exceptionClas) + where: + input || exceptionClas + 666 || CoercingParseValueException + "1:not/a/url" || CoercingParseValueException + } + + @Unroll + def "test parseLiteral"() { + when: + def result = coercing.parseLiteral(input, variables, graphQLContext, locale) + then: + result == expectedResult + where: + input | expectedResult + new StringValue("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") + } + + @Unroll + def "test parseLiteral bad inputs"() { + when: + coercing.parseLiteral(input, variables, graphQLContext, locale) + then: + thrown(exceptionClas) + where: + input | exceptionClas + new BooleanValue(true) | CoercingParseLiteralException + new StringValue("1:not/a/url") | CoercingParseLiteralException + } + + +}