From 335c46ba8e43d876b931f308ca078295a54b0ae7 Mon Sep 17 00:00:00 2001 From: Pascal Will <13084595+pascalwill@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:04:21 +0200 Subject: [PATCH 1/2] Copy URL scalar --- .../java/graphql/scalars/ExtendedScalars.java | 6 + .../java/graphql/scalars/uri/UriScalar.java | 118 ++++++++++++++++++ .../graphql/scalars/uri/UriScalarTest.groovy | 111 ++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 src/main/java/graphql/scalars/uri/UriScalar.java create mode 100644 src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy 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..31b8cdb --- /dev/null +++ b/src/main/java/graphql/scalars/uri/UriScalar.java @@ -0,0 +1,118 @@ +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.MalformedURLException; +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 URL serialize(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingSerializeException { + Optional url; + if (input instanceof String) { + url = Optional.of(parseURL(input.toString(), CoercingSerializeException::new)); + } else { + url = toURL(input); + } + if (url.isPresent()) { + return url.get(); + } + throw new CoercingSerializeException( + "Expected a 'URL' like object but was '" + typeName(input) + "'." + ); + } + + @Override + public URL parseValue(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingParseValueException { + String urlStr; + if (input instanceof String) { + urlStr = String.valueOf(input); + } else { + Optional url = toURL(input); + if (url.isEmpty()) { + throw new CoercingParseValueException( + "Expected a 'URL' like object but was '" + typeName(input) + "'." + ); + } + return url.get(); + } + return parseURL(urlStr, CoercingParseValueException::new); + } + + @Override + public URL 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 parseURL(((StringValue) input).getValue(), CoercingParseLiteralException::new); + } + + @Override + public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { + URL url = serialize(input, graphQLContext, locale); + return StringValue.newStringValue(url.toExternalForm()).build(); + } + + + private URL parseURL(String input, Function exceptionMaker) { + try { + return new URI(input).toURL(); + } catch (URISyntaxException | IllegalArgumentException | MalformedURLException e) { + throw exceptionMaker.apply("Invalid URL value : '" + input + "'."); + } + } + }; + + INSTANCE = GraphQLScalarType.newScalar() + .name("Url") + .description("A Url scalar") + .coercing(coercing) + .build(); + } + + private static Optional toURL(Object input) { + if (input instanceof URL) { + return Optional.of((URL) input); + } else if (input instanceof URI) { + try { + return Optional.of(((URI) input).toURL()); + } catch (MalformedURLException ignored) { + } + } else if (input instanceof File) { + try { + return Optional.of(((File) input).toURI().toURL()); + } catch (MalformedURLException ignored) { + } + } + 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..e6896dd --- /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 URL("http://www.graphql-java.com/") + new URI("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") + new File("/this/that") | new URL("file:/this/that") + "http://www.graphql-java.com/" | new URL("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 + "not/a/url" || 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 URL("http://www.graphql-java.com/") + new URI("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") + new File("/this/that") | new URL("file:/this/that") + "http://www.graphql-java.com/" | new URL("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 + "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 URL("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/uri") | CoercingParseLiteralException + } + + +} From e448beee3b0207db56f761d86166a4fd620810a1 Mon Sep 17 00:00:00 2001 From: Pascal Will <13084595+pascalwill@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:05:41 +0200 Subject: [PATCH 2/2] Adjust URI scalar --- .../java/graphql/scalars/uri/UriScalar.java | 70 +++++++++---------- .../graphql/scalars/uri/UriScalarTest.groovy | 36 +++++----- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/main/java/graphql/scalars/uri/UriScalar.java b/src/main/java/graphql/scalars/uri/UriScalar.java index 31b8cdb..a2c7592 100644 --- a/src/main/java/graphql/scalars/uri/UriScalar.java +++ b/src/main/java/graphql/scalars/uri/UriScalar.java @@ -12,7 +12,6 @@ import graphql.schema.GraphQLScalarType; import java.io.File; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -31,86 +30,83 @@ private UriScalar() { public static final GraphQLScalarType INSTANCE; static { - Coercing coercing = new Coercing<>() { + Coercing coercing = new Coercing<>() { @Override - public URL serialize(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingSerializeException { - Optional url; + public URI serialize(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingSerializeException { + Optional uri; if (input instanceof String) { - url = Optional.of(parseURL(input.toString(), CoercingSerializeException::new)); + uri = Optional.of(parseURI(input.toString(), CoercingSerializeException::new)); } else { - url = toURL(input); + uri = toURI(input); } - if (url.isPresent()) { - return url.get(); + if (uri.isPresent()) { + return uri.get(); } throw new CoercingSerializeException( - "Expected a 'URL' like object but was '" + typeName(input) + "'." + "Expected a 'URI' like object but was '" + typeName(input) + "'." ); } @Override - public URL parseValue(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingParseValueException { - String urlStr; + public URI parseValue(Object input, GraphQLContext graphQLContext, Locale locale) throws CoercingParseValueException { + String uriStr; if (input instanceof String) { - urlStr = String.valueOf(input); + uriStr = String.valueOf(input); } else { - Optional url = toURL(input); - if (url.isEmpty()) { + Optional uri = toURI(input); + if (uri.isEmpty()) { throw new CoercingParseValueException( - "Expected a 'URL' like object but was '" + typeName(input) + "'." + "Expected a 'URI' like object but was '" + typeName(input) + "'." ); } - return url.get(); + return uri.get(); } - return parseURL(urlStr, CoercingParseValueException::new); + return parseURI(uriStr, CoercingParseValueException::new); } @Override - public URL parseLiteral(Value input, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) throws CoercingParseLiteralException { + 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 parseURL(((StringValue) input).getValue(), CoercingParseLiteralException::new); + return parseURI(((StringValue) input).getValue(), CoercingParseLiteralException::new); } @Override public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { - URL url = serialize(input, graphQLContext, locale); - return StringValue.newStringValue(url.toExternalForm()).build(); + URI uri = serialize(input, graphQLContext, locale); + return StringValue.newStringValue(uri.toString()).build(); } - private URL parseURL(String input, Function exceptionMaker) { + private URI parseURI(String input, Function exceptionMaker) { try { - return new URI(input).toURL(); - } catch (URISyntaxException | IllegalArgumentException | MalformedURLException e) { - throw exceptionMaker.apply("Invalid URL value : '" + input + "'."); + return new URI(input); + } catch (URISyntaxException e) { + throw exceptionMaker.apply("Invalid URI value : '" + input + "'."); } } }; INSTANCE = GraphQLScalarType.newScalar() - .name("Url") - .description("A Url scalar") + .name("Uri") + .description("A Uri scalar") .coercing(coercing) .build(); } - private static Optional toURL(Object input) { - if (input instanceof URL) { - return Optional.of((URL) input); - } else if (input instanceof URI) { + private static Optional toURI(Object input) { + if (input instanceof URI) { + return Optional.of((URI) input); + } else if (input instanceof URL) { try { - return Optional.of(((URI) input).toURL()); - } catch (MalformedURLException ignored) { + return Optional.of(((URL) input).toURI()); + } catch (URISyntaxException ignored) { } } else if (input instanceof File) { - try { - return Optional.of(((File) input).toURI().toURL()); - } catch (MalformedURLException ignored) { - } + 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 index e6896dd..efc03fd 100644 --- a/src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy +++ b/src/test/groovy/graphql/scalars/uri/UriScalarTest.groovy @@ -25,10 +25,10 @@ class UriScalarTest extends AbstractScalarTest { result == expectedResult where: input | expectedResult - new URL("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") - new URI("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") - new File("/this/that") | new URL("file:/this/that") - "http://www.graphql-java.com/" | new URL("http://www.graphql-java.com/") + 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 @@ -53,9 +53,9 @@ class UriScalarTest extends AbstractScalarTest { then: thrown(exceptionClas) where: - input || exceptionClas - 666 || CoercingSerializeException - "not/a/url" || CoercingSerializeException + input || exceptionClas + 666 || CoercingSerializeException + "1:not/a/uri" || CoercingSerializeException } @Unroll @@ -66,10 +66,10 @@ class UriScalarTest extends AbstractScalarTest { result == expectedResult where: input | expectedResult - new URL("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") - new URI("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") - new File("/this/that") | new URL("file:/this/that") - "http://www.graphql-java.com/" | new URL("http://www.graphql-java.com/") + 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 @@ -79,9 +79,9 @@ class UriScalarTest extends AbstractScalarTest { then: thrown(exceptionClas) where: - input || exceptionClas - 666 || CoercingParseValueException - "not/a/url" || CoercingParseValueException + input || exceptionClas + 666 || CoercingParseValueException + "1:not/a/url" || CoercingParseValueException } @Unroll @@ -92,7 +92,7 @@ class UriScalarTest extends AbstractScalarTest { result == expectedResult where: input | expectedResult - new StringValue("http://www.graphql-java.com/") | new URL("http://www.graphql-java.com/") + new StringValue("http://www.graphql-java.com/") | new URI("http://www.graphql-java.com/") } @Unroll @@ -102,9 +102,9 @@ class UriScalarTest extends AbstractScalarTest { then: thrown(exceptionClas) where: - input | exceptionClas - new BooleanValue(true) | CoercingParseLiteralException - new StringValue("1:not/a/uri") | CoercingParseLiteralException + input | exceptionClas + new BooleanValue(true) | CoercingParseLiteralException + new StringValue("1:not/a/url") | CoercingParseLiteralException }