From fa7e10c27f760963721de278af71e044578a7849 Mon Sep 17 00:00:00 2001 From: Brian Dupras Date: Thu, 23 Nov 2023 08:20:55 -0700 Subject: [PATCH 1/6] WIP Proof of concept for Custom Scalars --- .sdkmanrc | 4 + .../smallrye/graphql/schema/Annotations.java | 1 + .../graphql/schema/SchemaBuilder.java | 17 ++ .../creator/type/CustomScalarCreator.java | 56 +++++ .../schema/model/CustomScalarType.java | 59 ++++++ .../graphql/schema/model/Scalars.java | 6 + .../smallrye/graphql/schema/model/Schema.java | 17 ++ .../io/smallrye/graphql/api/CustomScalar.java | 17 ++ .../smallrye/graphql/bootstrap/Bootstrap.java | 53 ++++- .../smallrye/graphql/json/JsonBCreator.java | 9 +- .../graphql/scalar/GraphQLScalarTypes.java | 8 + .../scalar/custom/CustomFloatScalar.java | 48 +++++ .../scalar/custom/CustomIntScalar.java | 48 +++++ .../scalar/custom/CustomStringScalar.java | 47 +++++ .../graphql/scalar/custom/FloatCoercing.java | 83 ++++++++ .../graphql/scalar/custom/IntCoercing.java | 83 ++++++++ .../graphql/scalar/custom/StringCoercing.java | 81 ++++++++ .../tests/customscalars/BigDecimalString.java | 40 ++++ .../tests/customscalars/CustomScalarTest.java | 196 ++++++++++++++++++ .../graphql/tests/customscalars/SomeApi.java | 94 +++++++++ .../tests/customscalars/TwiceTheFloat.java | 40 ++++ 21 files changed, 1005 insertions(+), 2 deletions(-) create mode 100644 .sdkmanrc create mode 100644 common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java create mode 100644 common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java create mode 100644 server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java create mode 100644 server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java create mode 100644 server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java create mode 100644 server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java create mode 100644 server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java create mode 100644 server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 000000000..dea5466ff --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,4 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.9-zulu +maven=3.9.5 diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java index a5821babf..bcf3143f0 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java @@ -586,6 +586,7 @@ private static Map getAnnotationsWithFilter(org.jbo // SmallRye GraphQL Annotations (Experimental) public static final DotName TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.ToScalar"); // TODO: Remove + public static final DotName CUSTOM_SCALAR = DotName.createSimple("io.smallrye.graphql.api.CustomScalar"); public static final DotName ADAPT_TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.AdaptToScalar"); public static final DotName ADAPT_WITH = DotName.createSimple("io.smallrye.graphql.api.AdaptWith"); public static final DotName ERROR_CODE = DotName.createSimple("io.smallrye.graphql.api.ErrorCode"); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java index 0fe4b3ac2..cdf05fdd1 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java @@ -1,5 +1,6 @@ package io.smallrye.graphql.schema; +import static io.smallrye.graphql.schema.Annotations.CUSTOM_SCALAR; import static io.smallrye.graphql.schema.Annotations.DIRECTIVE; import java.util.ArrayList; @@ -28,6 +29,7 @@ import io.smallrye.graphql.schema.creator.OperationCreator; import io.smallrye.graphql.schema.creator.ReferenceCreator; import io.smallrye.graphql.schema.creator.type.Creator; +import io.smallrye.graphql.schema.creator.type.CustomScalarCreator; import io.smallrye.graphql.schema.creator.type.EnumCreator; import io.smallrye.graphql.schema.creator.type.InputTypeCreator; import io.smallrye.graphql.schema.creator.type.InterfaceCreator; @@ -73,6 +75,7 @@ public class SchemaBuilder { private final OperationCreator operationCreator; private final DirectiveTypeCreator directiveTypeCreator; private final UnionCreator unionCreator; + private final CustomScalarCreator customScalarCreator; private final DotName FEDERATION_ANNOTATIONS_PACKAGE = DotName.createSimple("io.smallrye.graphql.api.federation"); @@ -109,6 +112,7 @@ private SchemaBuilder(TypeAutoNameStrategy autoNameStrategy) { interfaceCreator = new InterfaceCreator(referenceCreator, fieldCreator, operationCreator); directiveTypeCreator = new DirectiveTypeCreator(referenceCreator); unionCreator = new UnionCreator(referenceCreator); + customScalarCreator = new CustomScalarCreator(referenceCreator); } private Schema generateSchema() { @@ -127,6 +131,8 @@ private Schema generateSchema() { // add AppliedSchemaDirectives and Schema Description setUpSchemaDirectivesAndDescription(schema, graphQLApiAnnotations, directivesHelper); + addCustomScalarTypes(schema); + for (AnnotationInstance graphQLApiAnnotation : graphQLApiAnnotations) { ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); List methods = getAllMethodsIncludingFromSuperClasses(apiClass); @@ -187,7 +193,18 @@ private void addDirectiveTypes(Schema schema) { schema.addDirectiveType(RolesAllowedDirectivesHelper.ROLES_ALLOWED_DIRECTIVE_TYPE); } + private void addCustomScalarTypes(Schema schema) { + Collection annotations = ScanningContext.getIndex().getAnnotations(CUSTOM_SCALAR); + + for (AnnotationInstance annotationInstance : annotations) { + schema.addCustomScalarType(customScalarCreator.create( + annotationInstance.target().asClass(), + annotationInstance.value().asString())); + } + } + private void setupDirectives(Directives directives) { + customScalarCreator.setDirectives(directives); inputTypeCreator.setDirectives(directives); typeCreator.setDirectives(directives); interfaceCreator.setDirectives(directives); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java new file mode 100644 index 000000000..6c3121771 --- /dev/null +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java @@ -0,0 +1,56 @@ +package io.smallrye.graphql.schema.creator.type; + +import java.util.List; +import java.util.stream.Collectors; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; + +import io.smallrye.graphql.schema.Annotations; +import io.smallrye.graphql.schema.creator.ModelCreator; +import io.smallrye.graphql.schema.creator.ReferenceCreator; +import io.smallrye.graphql.schema.helper.DescriptionHelper; +import io.smallrye.graphql.schema.helper.Directives; +import io.smallrye.graphql.schema.model.CustomScalarType; +import io.smallrye.graphql.schema.model.DirectiveInstance; + +public class CustomScalarCreator extends ModelCreator { + + private static final Logger LOG = Logger.getLogger(CustomScalarCreator.class.getName()); + + private Directives directives; + + public CustomScalarCreator(ReferenceCreator referenceCreator) { + super(referenceCreator); + } + + public CustomScalarType create( + ClassInfo classInfo, + String scalarName) { + LOG.debug("Creating custom scalar from " + classInfo.name().toString()); + + Annotations annotations = Annotations.getAnnotationsForClass(classInfo); + + return new CustomScalarType( + classInfo.name().toString(), + scalarName, + DescriptionHelper.getDescriptionForType(annotations).orElse(null), + classInfo.interfaceNames().stream().map(DotName::toString).collect(Collectors.toSet())); + + } + + @Override + public String getDirectiveLocation() { + return "SCALAR"; + } + + private List getDirectiveInstances(Annotations annotations, + String referenceName) { + return directives.buildDirectiveInstances(annotations, getDirectiveLocation(), referenceName); + } + + public void setDirectives(Directives directives) { + this.directives = directives; + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java new file mode 100644 index 000000000..eb1fed33d --- /dev/null +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java @@ -0,0 +1,59 @@ +package io.smallrye.graphql.schema.model; + +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.FLOAT_TYPE; +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.INT_TYPE; +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.STRING_TYPE; + +import java.util.Set; + +public final class CustomScalarType extends Reference { + + private String description; + private CustomScalarPrimitiveType customScalarPrimitiveType; + + public CustomScalarType() { + } + + public CustomScalarType(String className, String name, String description, Set interfaces) { + super(className, name, ReferenceType.SCALAR); + this.description = description; + if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomIntScalar")) { + customScalarPrimitiveType = INT_TYPE; + } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomFloatScalar")) { + customScalarPrimitiveType = FLOAT_TYPE; + } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomStringScalar")) { + customScalarPrimitiveType = STRING_TYPE; + } else { + //TODO error handle in the expected way + throw new RuntimeException("Required to implement a known CustomScalar primitive type. " + + "(CustomStringScalar, CustomFloatScalar, CustomIntScalar"); + } + } + + public String getDescription() { + return description; + } + + public CustomScalarPrimitiveType customScalarPrimitiveType() { + return customScalarPrimitiveType; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "CustomScalarType{" + + "name='" + getName() + '\'' + + ", description='" + description + '\'' + + ", customScalar='" + customScalarPrimitiveType.name() + '\'' + + '}'; + } + + public enum CustomScalarPrimitiveType { + STRING_TYPE, + INT_TYPE, + FLOAT_TYPE + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java index 901feab8c..8e698ecc2 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java @@ -94,6 +94,12 @@ public static Reference getIDScalar(String className) { .build(); } + public static void registerCustomScalarInSchema( + String graphQlScalarName, + String valueClassName) { + populateScalar(valueClassName, graphQlScalarName, valueClassName); + } + // this is for the UUID from graphql-java-extended-scalars // if used, it will override the original UUID type that is mapped to a String in the schema public static void addUuid() { diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java index 579a17967..8575c949c 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java @@ -26,6 +26,7 @@ public final class Schema implements Serializable { private Map> groupedMutations = new HashMap<>(); private Map> groupedSubscriptions = new HashMap<>(); + private List customScalarTypes = new ArrayList<>(); private List directiveTypes = new ArrayList<>(); private Map inputs = new HashMap<>(); private Map types = new HashMap<>(); @@ -320,6 +321,21 @@ private void addToOperationMap(Map> map, Group group, Oper map.put(group, set); } + public void addCustomScalarType(CustomScalarType customScalarType) { + customScalarTypes.add(customScalarType); + Scalars.registerCustomScalarInSchema( + customScalarType.getName(), + customScalarType.getClassName()); + } + + public List getCustomScalarTypes() { + return customScalarTypes; + } + + public boolean hasCustomScalarTypes() { + return !customScalarTypes.isEmpty(); + } + public List getDirectiveTypes() { return directiveTypes; } @@ -347,6 +363,7 @@ public String toString() { ", groupedMutations=" + groupedMutations + ", groupedSubscriptions=" + groupedSubscriptions + ", directiveTypes=" + directiveTypes + + ", customScalarTypes=" + customScalarTypes + ", inputs=" + inputs + ", types=" + types + ", interfaces=" + interfaces + diff --git a/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java new file mode 100644 index 000000000..76d80f21f --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java @@ -0,0 +1,17 @@ +package io.smallrye.graphql.api; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.smallrye.common.annotation.Experimental; + +@Retention(RUNTIME) +@Target(TYPE) +@Experimental("Mark a type as a custom scalar with the given name in the GraphQL schema.") +public @interface CustomScalar { + + String value(); +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java index 892ae7392..fa9faa1c9 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java @@ -31,6 +31,7 @@ import com.apollographql.federation.graphqljava.Federation; import graphql.introspection.Introspection.DirectiveLocation; +import graphql.schema.Coercing; import graphql.schema.DataFetcher; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; @@ -67,7 +68,12 @@ import io.smallrye.graphql.json.JsonBCreator; import io.smallrye.graphql.json.JsonInputRegistry; import io.smallrye.graphql.scalar.GraphQLScalarTypes; +import io.smallrye.graphql.scalar.custom.FloatCoercing; +import io.smallrye.graphql.scalar.custom.IntCoercing; +import io.smallrye.graphql.scalar.custom.StringCoercing; import io.smallrye.graphql.schema.model.Argument; +import io.smallrye.graphql.schema.model.CustomScalarType; +import io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType; import io.smallrye.graphql.schema.model.DirectiveArgument; import io.smallrye.graphql.schema.model.DirectiveInstance; import io.smallrye.graphql.schema.model.DirectiveType; @@ -160,6 +166,8 @@ private void verifyInjectionIsAvailable() { private void generateGraphQLSchema() { GraphQLSchema.Builder schemaBuilder = GraphQLSchema.newSchema(); + createGraphQLDirectiveTypes(); + createGraphQLCustomScalarTypes(); createGraphQLEnumTypes(); createGraphQLDirectiveTypes(); @@ -223,6 +231,49 @@ private TypeResolver fetchEntityType() { }; } + private void createGraphQLCustomScalarTypes() { + if (schema.hasCustomScalarTypes()) { + for (CustomScalarType customScalarType : schema.getCustomScalarTypes()) { + createGraphQLCustomScalarType(customScalarType); + } + } + } + + private void createGraphQLCustomScalarType(CustomScalarType customScalarType) { + String scalarName = customScalarType.getName(); + + Coercing coercing = getCoercing(customScalarType); + + GraphQLScalarType graphQLScalarType = GraphQLScalarType.newScalar() + .name(scalarName) + .description("Scalar for " + scalarName) + .coercing(coercing) + .build(); + + GraphQLScalarTypes.registerCustomScalar( + scalarName, + customScalarType.getClassName(), + graphQLScalarType); + } + + private static Coercing getCoercing(CustomScalarType customScalarType) { + CustomScalarPrimitiveType primitiveType = customScalarType.customScalarPrimitiveType(); + + Coercing coercing = null; + switch (primitiveType) { + case STRING_TYPE: + coercing = new StringCoercing(customScalarType.getClassName()); + break; + case INT_TYPE: + coercing = new IntCoercing(customScalarType.getClassName()); + break; + case FLOAT_TYPE: + coercing = new FloatCoercing(customScalarType.getClassName()); + break; + } + return coercing; + } + private void createGraphQLDirectiveTypes() { if (schema.hasDirectiveTypes()) { for (DirectiveType directiveType : schema.getDirectiveTypes()) { @@ -526,7 +577,7 @@ private GraphQLInputObjectType createGraphQLInputObjectType(InputType inputType) if (inputType.hasFields()) { inputObjectTypeBuilder = inputObjectTypeBuilder .fields(createGraphQLInputObjectFieldsFromFields(inputType.getFields().values())); - // Register this input for posible JsonB usage + // Register this input for possible JsonB usage JsonInputRegistry.register(inputType); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java index ee68b910b..2340b1010 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java @@ -8,6 +8,9 @@ import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; +import io.smallrye.graphql.scalar.custom.CustomFloatScalar; +import io.smallrye.graphql.scalar.custom.CustomIntScalar; +import io.smallrye.graphql.scalar.custom.CustomStringScalar; import io.smallrye.graphql.schema.model.Field; import io.smallrye.graphql.schema.model.InputType; @@ -21,7 +24,11 @@ public class JsonBCreator { private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig() .withFormatting(true) - .withNullValues(true)); //null values are required by @JsonbCreator + .withNullValues(true) //null values are required by @JsonbCreator + .withSerializers(CustomStringScalar.SERIALIZER, CustomIntScalar.SERIALIZER, + CustomFloatScalar.SERIALIZER) + .withDeserializers(CustomStringScalar.DESERIALIZER, CustomIntScalar.DESERIALIZER, + CustomFloatScalar.DESERIALIZER)); private static final Map jsonMap = new HashMap<>(); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java index 3f33f4180..21babf93a 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java @@ -52,6 +52,14 @@ public static void addUuid() { SCALARS_BY_NAME.put(ExtendedScalars.UUID.getName(), ExtendedScalars.UUID); } + public static void registerCustomScalar( + String graphQlScalarName, + String valueClassName, + GraphQLScalarType graphQLScalarType) { + SCALAR_MAP.put(valueClassName, graphQLScalarType); + SCALARS_BY_NAME.put(graphQlScalarName, graphQLScalarType); + } + // Scalar map we can just create now. private static final Map SCALAR_MAP = new HashMap<>(); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java new file mode 100644 index 000000000..75054534c --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java @@ -0,0 +1,48 @@ +package io.smallrye.graphql.scalar.custom; + +import java.lang.reflect.Type; +import java.math.BigDecimal; + +import jakarta.json.JsonValue.ValueType; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; + +import io.smallrye.graphql.spi.ClassloadingService; + +public interface CustomFloatScalar { + // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it + // hides the parameterized type from Jsonb. + + JsonbSerializer SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomFloatScalar customFloatScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customFloatScalar.floatValue()); + } + }; + + JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomFloatScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomFloatScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(BigDecimal.class) + .newInstance(jsonParser.getBigDecimal()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + BigDecimal floatValue(); +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java new file mode 100644 index 000000000..f049a60d6 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java @@ -0,0 +1,48 @@ +package io.smallrye.graphql.scalar.custom; + +import java.lang.reflect.Type; +import java.math.BigInteger; + +import jakarta.json.JsonValue.ValueType; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; + +import io.smallrye.graphql.spi.ClassloadingService; + +public interface CustomIntScalar { + // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it + // hides the parameterized type from Jsonb. + + JsonbSerializer SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomIntScalar customIntScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customIntScalar.intValue()); + } + }; + + JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomIntScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomIntScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(Integer.class) + .newInstance(jsonParser.getInt()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + BigInteger intValue(); +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java new file mode 100644 index 000000000..17413a5d6 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java @@ -0,0 +1,47 @@ +package io.smallrye.graphql.scalar.custom; + +import java.lang.reflect.Type; + +import jakarta.json.JsonValue.ValueType; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; + +import io.smallrye.graphql.spi.ClassloadingService; + +public interface CustomStringScalar { + // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it + // hides the parameterized type from Jsonb. + + JsonbSerializer SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomStringScalar customStringScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customStringScalar.stringValue()); + } + }; + + JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomStringScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomStringScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(String.class) + .newInstance(jsonParser.getString()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + String stringValue(); +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java new file mode 100644 index 000000000..f9d471b2c --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java @@ -0,0 +1,83 @@ +package io.smallrye.graphql.scalar.custom; + +import java.math.BigDecimal; + +import graphql.language.FloatValue; +import graphql.language.Value; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import io.smallrye.graphql.spi.ClassloadingService; + +public class FloatCoercing implements Coercing { + + private final Class customScalarClass; + + public FloatCoercing(String customScalarClass) { + ClassloadingService classloadingService = ClassloadingService.get(); + this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); + } + + private CustomFloatScalar newInstance(BigDecimal graphqlPrimitiveValue) { + try { + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(BigDecimal.class).newInstance(graphqlPrimitiveValue); + } catch (Exception e) { + throw new CoercingSerializeException("TODO bdupras better error handling here", e); + } + } + + /* Coercing implementation. Forgive the deprecated methods. */ + private static String typeName(Object input) { + if (input == null) { + return "null"; + } + return input.getClass().getSimpleName(); + } + + private CustomFloatScalar convertImpl(Object input) { + if (input instanceof BigDecimal) { + return newInstance((BigDecimal) input); + } else if (input.getClass().isAssignableFrom(customScalarClass)) { + return (CustomFloatScalar) input; + } + return null; + } + + @Override + public BigDecimal serialize(Object input) throws CoercingSerializeException { + CustomFloatScalar result = convertImpl(input); + if (result == null) { + throw new CoercingSerializeException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result.floatValue(); + } + + @Override + public Object parseValue(Object input) throws CoercingParseValueException { + Object result = convertImpl(input); + if (result == null) { + throw new CoercingParseValueException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result; + } + + @Override + public Object parseLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof FloatValue)) { + throw new CoercingParseLiteralException( + "Expected a String AST type object but was '" + typeName(input) + "'."); + } + return newInstance(((FloatValue) input).getValue()); + } + + @Override + public Value valueToLiteral(Object input) { + BigDecimal s = serialize(input); + return FloatValue.newFloatValue(s).build(); + } + +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java new file mode 100644 index 000000000..f3092b7f2 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java @@ -0,0 +1,83 @@ +package io.smallrye.graphql.scalar.custom; + +import java.math.BigInteger; + +import graphql.language.IntValue; +import graphql.language.Value; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import io.smallrye.graphql.spi.ClassloadingService; + +public class IntCoercing implements Coercing { + + private final Class customScalarClass; + + public IntCoercing(String customScalarClass) { + ClassloadingService classloadingService = ClassloadingService.get(); + this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); + } + + private CustomIntScalar newInstance(BigInteger graphqlPrimitiveValue) { + try { + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(BigInteger.class).newInstance(graphqlPrimitiveValue); + } catch (Exception e) { + throw new CoercingSerializeException("TODO bdupras better error handling here", e); + } + } + + /* Coercing implementation. Forgive the deprecated methods. */ + private static String typeName(Object input) { + if (input == null) { + return "null"; + } + return input.getClass().getSimpleName(); + } + + private CustomIntScalar convertImpl(Object input) { + if (input instanceof BigInteger) { + return newInstance((BigInteger) input); + } else if (input.getClass().isAssignableFrom(customScalarClass)) { + return (CustomIntScalar) input; + } + return null; + } + + @Override + public BigInteger serialize(Object input) throws CoercingSerializeException { + CustomIntScalar result = convertImpl(input); + if (result == null) { + throw new CoercingSerializeException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result.intValue(); + } + + @Override + public Object parseValue(Object input) throws CoercingParseValueException { + Object result = convertImpl(input); + if (result == null) { + throw new CoercingParseValueException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result; + } + + @Override + public Object parseLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof IntValue)) { + throw new CoercingParseLiteralException( + "Expected a String AST type object but was '" + typeName(input) + "'."); + } + return newInstance(((IntValue) input).getValue()); + } + + @Override + public Value valueToLiteral(Object input) { + BigInteger s = serialize(input); + return IntValue.newIntValue(s).build(); + } + +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java new file mode 100644 index 000000000..bbb9b5a58 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java @@ -0,0 +1,81 @@ +package io.smallrye.graphql.scalar.custom; + +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 io.smallrye.graphql.spi.ClassloadingService; + +public class StringCoercing implements Coercing { + + private final Class customScalarClass; + + public StringCoercing(String customScalarClass) { + ClassloadingService classloadingService = ClassloadingService.get(); + this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); + } + + private CustomStringScalar newInstance(String graphqlPrimitiveValue) { + try { + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(String.class).newInstance(graphqlPrimitiveValue); + } catch (Exception e) { + throw new CoercingSerializeException("TODO bdupras better error handling here", e); + } + } + + /* Coercing implementation. Forgive the deprecated methods. */ + private static String typeName(Object input) { + if (input == null) { + return "null"; + } + return input.getClass().getSimpleName(); + } + + private CustomStringScalar convertImpl(Object input) { + if (input instanceof String) { + return newInstance((String) input); + } else if (input.getClass().isAssignableFrom(customScalarClass)) { + return (CustomStringScalar) input; + } + return null; + } + + @Override + public String serialize(Object input) throws CoercingSerializeException { + Object result = convertImpl(input); + if (result == null) { + throw new CoercingSerializeException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result.toString(); + } + + @Override + public Object parseValue(Object input) throws CoercingParseValueException { + Object result = convertImpl(input); + if (result == null) { + throw new CoercingParseValueException( + "Expected type String but was '" + typeName(input) + "'."); + } + return result; + } + + @Override + public Object parseLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException( + "Expected a String AST type object but was '" + typeName(input) + "'."); + } + return newInstance(((StringValue) input).getValue()); + } + + @Override + public Value valueToLiteral(Object input) { + String s = serialize(input); + return StringValue.newStringValue(s).build(); + } + +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java new file mode 100644 index 000000000..9531e62aa --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java @@ -0,0 +1,40 @@ +package io.smallrye.graphql.tests.customscalars; + +import java.math.BigDecimal; + +import io.smallrye.graphql.api.CustomScalar; +import io.smallrye.graphql.scalar.custom.CustomStringScalar; + +/** + * An alternative BigDecimal scalar that serializes to a GraphQL String instead of a Float. + *

+ */ +@CustomScalar("BigDecimalString") +public class BigDecimalString implements CustomStringScalar { + + private final BigDecimal value; + + public BigDecimalString(String stringValue) { + this.value = stringValue == null ? null : new BigDecimal(stringValue); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) || value.equals(obj); + } + + @Override + public String toString() { + return value == null ? null : value.toPlainString(); + } + + @Override + public String stringValue() { + return toString(); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java new file mode 100644 index 000000000..6206e54e4 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java @@ -0,0 +1,196 @@ +package io.smallrye.graphql.tests.customscalars; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.smallrye.graphql.tests.GraphQLAssured; + +@RunWith(Arquillian.class) +@RunAsClient +public class CustomScalarTest { + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "customscalar-test.war") + .addClasses(SomeApi.class, BigDecimalString.class, TwiceTheFloat.class); + } + + @ArquillianResource + URL testingURL; + + @Test + public void inAsScalarNullableTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarNullable(scalar: \"1234567890.987654321\") }")) + .contains("{\"data\":{\"inAsScalarNullable\":\"1234567890.987654321\"}}") + .doesNotContain("error"); + assertThat(graphQLAssured + .post("query { inAsFScalarNullable(fScalar: 10.0) }")) + .contains("{\"data\":{\"inAsFScalarNullable\":10.0}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsScalarNullable(scalar: null) }")) + .contains("{\"data\":{\"inAsScalarNullable\":null}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsScalarNullable }")) + .contains("{\"data\":{\"inAsScalarNullable\":null}}") + .doesNotContain("error"); + } + + @Test + public void inAsScalarNullableDefaultNonNullTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarNullableDefaultNonNull }")) + .contains("{\"data\":{\"inAsScalarNullableDefaultNonNull\":\"1234567.89\"}}") + .doesNotContain("error"); + } + + @Test + public void inAsScalarNullableDefaultNullTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarNullableDefaultNull }")) + .contains("{\"data\":{\"inAsScalarNullableDefaultNull\":null}}") + .doesNotContain("error"); + } + + @Test + public void inAsScalarRequiredTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarRequired(scalar: \"1234567890.987654321\") }")) + .contains("{\"data\":{\"inAsScalarRequired\":\"1234567890.987654321\"}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsScalarRequired(scalar: null) }")) + .contains("NullValueForNonNullArgument@[inAsScalarRequired]") + .contains("\"data\":null"); + + assertThat(graphQLAssured + .post("query { inAsScalarRequired }")) + .contains("MissingFieldArgument@[inAsScalarRequired]") + .contains("\"data\":null"); + } + + @Test + public void inAsScalarRequiredDefaultTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarRequiredDefault }")) + .contains("{\"data\":{\"inAsScalarRequiredDefault\":\"1234567.89\"}}") + .doesNotContain("error"); + + } + + @Test + public void inAsScalarListNullableTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarListNullable(scalars: [\"1234567890.987654321\"]) }")) + .contains("{\"data\":{\"inAsScalarListNullable\":[\"1234567890.987654321\"]}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsScalarListNullable(scalars: [null]) }")) + .contains("{\"data\":{\"inAsScalarListNullable\":[null]}}") + .doesNotContain("error"); + } + + @Test + public void inAsScalarListRequiredTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsScalarListRequired(scalars: [\"1234567890.987654321\"]) }")) + .contains("{\"data\":{\"inAsScalarListRequired\":[\"1234567890.987654321\"]}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsScalarListRequired(scalars: [null]) }")) + .contains("WrongType@[inAsScalarListRequired]") + .contains("must not be null") + .contains("\"data\":null"); + } + + @Test + public void inAsFieldNullableTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsFieldNullable(input: { scalar: \"1234567890.987654321\" } ) }")) + .contains("{\"data\":{\"inAsFieldNullable\":\"1234567890.987654321\"}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsFieldNullable(input: { scalar: null } ) }")) + .contains("{\"data\":{\"inAsFieldNullable\":null}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsFieldNullable(input: { } ) }")) + .contains("{\"data\":{\"inAsFieldNullable\":null}}") + .doesNotContain("error"); + } + + @Test + public void inAsFloatFieldNullableTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { inAsFFieldNullable(input: { fScalar: 20.0 } ) }")) + .contains("{\"data\":{\"inAsFFieldNullable\":20.0}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsFFieldNullable(input: { fScalar: null } ) }")) + .contains("{\"data\":{\"inAsFFieldNullable\":null}}") + .doesNotContain("error"); + + assertThat(graphQLAssured + .post("query { inAsFFieldNullable(input: { } ) }")) + .contains("{\"data\":{\"inAsFFieldNullable\":null}}") + .doesNotContain("error"); + } + + @Test + public void outputTwiceTheFloatTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { outputFloat }")) + .contains("{\"data\":{\"outputFloat\":10.0}}") + .doesNotContain("error"); + } + + @Test + public void outputScalarObjectTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + + assertThat(graphQLAssured + .post("query { outputScalars {fScalar, sScalar}}")) + .contains("{\"data\":{\"outputScalars\":{\"fScalar\":30.0,\"sScalar\":\"98765.56789\"}}}") + .doesNotContain("error"); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java new file mode 100644 index 000000000..cf52c33f1 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java @@ -0,0 +1,94 @@ +package io.smallrye.graphql.tests.customscalars; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.graphql.DefaultValue; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class SomeApi { + public static class ObjNullable { + public BigDecimalString scalar; + } + + public static class FObjNullable { + public TwiceTheFloat fScalar; + } + + public static class ObjOfScalars { + public TwiceTheFloat fScalar; + public BigDecimalString sScalar; + } + + @Query + public String inAsScalarNullable(BigDecimalString scalar) { + return null == scalar ? null : scalar.toString(); + } + + @Query + public String inAsScalarNullableDefaultNonNull(@DefaultValue("1234567.89") BigDecimalString scalar) { + return null == scalar ? null : scalar.toString(); + } + + @Query + public String inAsScalarNullableDefaultNull(@DefaultValue() BigDecimalString scalar) { + return null == scalar ? null : scalar.toString(); + } + + @Query + public String inAsScalarRequiredDefault(@NonNull @DefaultValue("1234567.89") BigDecimalString scalar) { + return null == scalar ? null : scalar.toString(); + } + + @Query + public String inAsScalarRequired(@NonNull BigDecimalString scalar) { + return null == scalar ? null : scalar.toString(); + } + + @Query + public List inAsScalarListNullable(List scalars) { + return null == scalars ? null : scalars.stream().map(s -> null == s ? null : s.toString()).collect(Collectors.toList()); + } + + @Query + public List inAsScalarListRequired(List<@NonNull BigDecimalString> scalars) { + return null == scalars ? null : scalars.stream().map(Objects::toString).collect(Collectors.toList()); + } + + @Query + public String inAsFieldNullable(ObjNullable input) { + return null == input ? null : null == input.scalar ? null : input.scalar.toString(); + } + + @Query + public BigDecimal inAsFScalarNullable(TwiceTheFloat fScalar) { + return null == fScalar ? null : fScalar.floatValue().setScale(1, RoundingMode.HALF_EVEN); + } + + @Query + public BigDecimal inAsFFieldNullable(FObjNullable input) { + return null == input ? null + : null == input.fScalar ? null + : input.fScalar.floatValue().setScale(1, RoundingMode.HALF_EVEN); + } + + @Query + public TwiceTheFloat outputFloat() { + return new TwiceTheFloat(BigDecimal.valueOf(10)); + } + + @Query + public ObjOfScalars outputScalars() { + ObjOfScalars oScalars = new ObjOfScalars(); + oScalars.fScalar = new TwiceTheFloat(BigDecimal.valueOf(30)); + oScalars.sScalar = new BigDecimalString("98765.56789"); + return oScalars; + } + +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java new file mode 100644 index 000000000..07d0b46c6 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java @@ -0,0 +1,40 @@ +package io.smallrye.graphql.tests.customscalars; + +import java.math.BigDecimal; + +import io.smallrye.graphql.api.CustomScalar; +import io.smallrye.graphql.scalar.custom.CustomFloatScalar; + +/** + * A float scalar that serializes to a GraphQL Float equal to twice the internal value. + *

+ */ +@CustomScalar("TwiceTheFloat") +public class TwiceTheFloat implements CustomFloatScalar { + + private final BigDecimal value; + + public TwiceTheFloat(BigDecimal floatValue) { + this.value = floatValue == null ? null : floatValue.multiply(BigDecimal.valueOf(.5)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) || value.equals(obj); + } + + @Override + public String toString() { + return value == null ? null : value.toPlainString(); + } + + @Override + public BigDecimal floatValue() { + return value.multiply(BigDecimal.valueOf(2)); + } +} From f8305e4d27d1d97e9ef32d97acefb329a64dfbb5 Mon Sep 17 00:00:00 2001 From: "shane.hirsekorn" Date: Wed, 13 Dec 2023 13:46:10 -0700 Subject: [PATCH 2/6] remove sdkman --- .sdkmanrc | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .sdkmanrc diff --git a/.sdkmanrc b/.sdkmanrc deleted file mode 100644 index dea5466ff..000000000 --- a/.sdkmanrc +++ /dev/null @@ -1,4 +0,0 @@ -# Enable auto-env through the sdkman_auto_env config -# Add key=value pairs of SDKs to use below -java=17.0.9-zulu -maven=3.9.5 From 752141e00f7a5e036070e97488f6c17207c179fc Mon Sep 17 00:00:00 2001 From: "shane.hirsekorn" Date: Mon, 18 Dec 2023 16:09:38 -0700 Subject: [PATCH 3/6] Address PR comments, fix broken test, add documentation. --- .../creator/type/CustomScalarCreator.java | 38 ++++++++++++++++++- .../schema/model/CustomScalarType.java | 23 ++++------- docs/custom-scalar.md | 28 ++++++++++++++ .../io/smallrye/graphql/api/CustomScalar.java | 5 +++ .../smallrye/graphql/bootstrap/Bootstrap.java | 3 +- .../scalar/custom/CustomFloatScalar.java | 15 +++++++- .../scalar/custom/CustomIntScalar.java | 15 +++++++- .../scalar/custom/CustomStringScalar.java | 15 +++++++- .../graphql/scalar/custom/FloatCoercing.java | 2 +- .../graphql/scalar/custom/IntCoercing.java | 2 +- .../graphql/scalar/custom/StringCoercing.java | 4 +- .../tests/customscalars/BigDecimalString.java | 2 +- .../graphql/tests/customscalars/SomeApi.java | 6 +-- .../tests/customscalars/TwiceTheFloat.java | 2 +- 14 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 docs/custom-scalar.md diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java index 6c3121771..debd22fae 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java @@ -1,9 +1,17 @@ package io.smallrye.graphql.schema.creator.type; +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.FLOAT_TYPE; +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.INT_TYPE; +import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.STRING_TYPE; + +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.logging.Logger; @@ -13,6 +21,7 @@ import io.smallrye.graphql.schema.helper.DescriptionHelper; import io.smallrye.graphql.schema.helper.Directives; import io.smallrye.graphql.schema.model.CustomScalarType; +import io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType; import io.smallrye.graphql.schema.model.DirectiveInstance; public class CustomScalarCreator extends ModelCreator { @@ -32,12 +41,39 @@ public CustomScalarType create( Annotations annotations = Annotations.getAnnotationsForClass(classInfo); + Set interfaces = classInfo.interfaceNames().stream().map(DotName::toString) + .collect(Collectors.toSet()); + CustomScalarPrimitiveType customScalarPrimitiveType; + if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomIntScalar")) { + checkForOneArgConstructor(classInfo, BigInteger.class); + customScalarPrimitiveType = INT_TYPE; + } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomFloatScalar")) { + checkForOneArgConstructor(classInfo, BigDecimal.class); + customScalarPrimitiveType = FLOAT_TYPE; + } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomStringScalar")) { + checkForOneArgConstructor(classInfo, String.class); + customScalarPrimitiveType = STRING_TYPE; + } else { + //TODO error handle in the expected way + throw new RuntimeException("Required to implement a known CustomScalar primitive type. " + + "(CustomStringScalar, CustomFloatScalar, CustomIntScalar)"); + } + return new CustomScalarType( classInfo.name().toString(), scalarName, DescriptionHelper.getDescriptionForType(annotations).orElse(null), - classInfo.interfaceNames().stream().map(DotName::toString).collect(Collectors.toSet())); + customScalarPrimitiveType); + + } + private static void checkForOneArgConstructor(final ClassInfo classInfo, Class argType) { + if (classInfo.constructors().stream().noneMatch(methodInfo -> methodInfo.parameters().size() == 1 + && methodInfo.parameterType(0).equals(ClassType.create(argType)))) { + //TODO error handle in the expected way + throw new RuntimeException("Required to implement a one arg constructor with paramtype of " + + argType.getName()); + } } @Override diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java index eb1fed33d..e8d4ba3fc 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/CustomScalarType.java @@ -4,8 +4,6 @@ import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.INT_TYPE; import static io.smallrye.graphql.schema.model.CustomScalarType.CustomScalarPrimitiveType.STRING_TYPE; -import java.util.Set; - public final class CustomScalarType extends Reference { private String description; @@ -14,27 +12,18 @@ public final class CustomScalarType extends Reference { public CustomScalarType() { } - public CustomScalarType(String className, String name, String description, Set interfaces) { + public CustomScalarType(String className, String name, String description, + CustomScalarPrimitiveType customScalarPrimitiveType) { super(className, name, ReferenceType.SCALAR); this.description = description; - if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomIntScalar")) { - customScalarPrimitiveType = INT_TYPE; - } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomFloatScalar")) { - customScalarPrimitiveType = FLOAT_TYPE; - } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomStringScalar")) { - customScalarPrimitiveType = STRING_TYPE; - } else { - //TODO error handle in the expected way - throw new RuntimeException("Required to implement a known CustomScalar primitive type. " - + "(CustomStringScalar, CustomFloatScalar, CustomIntScalar"); - } + this.customScalarPrimitiveType = customScalarPrimitiveType; } public String getDescription() { return description; } - public CustomScalarPrimitiveType customScalarPrimitiveType() { + public CustomScalarPrimitiveType getCustomScalarPrimitiveType() { return customScalarPrimitiveType; } @@ -42,6 +31,10 @@ public void setDescription(String description) { this.description = description; } + public void setCustomScalarPrimitiveType(CustomScalarPrimitiveType customScalarPrimitiveType) { + this.customScalarPrimitiveType = customScalarPrimitiveType; + } + @Override public String toString() { return "CustomScalarType{" + diff --git a/docs/custom-scalar.md b/docs/custom-scalar.md new file mode 100644 index 000000000..ed577cb5e --- /dev/null +++ b/docs/custom-scalar.md @@ -0,0 +1,28 @@ +Creating GraphQL Custom Scalars with SmallRye GraphQL +======= +While by default **MicroProfile GraphQL** specification doesn't provide direct support for creating +custom scalars for GraphQL literal types of String, Int and Float, **SmallRye GraphQL** has implemented +this feature based on user feedback. To implement a **custom scalar** with **SmallRye GraphQL**, +you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below: +```java + @CustomScalar("BigDecimalString") + public class BigDecimalString implements CustomStringScalar { + public BigDecimalString(String stringValue) { + ... + } + @Override + public String stringValueForSerialization() { + ... + } +} +``` +In this example, `BigDecimalString` implements the `CustomStringScalar` which is used to identify the +proper (de) serialization for BigDecimalString. `BigDecimalString` also provides a single argument +constructor which takes a String. Finally, `BigDecimalString` implements +`stringValueForSerialization()` which provides the String representation to be used during +serialization. + +> [NOTE] +> If the user wants to create a literal for GraphQL Int or Float, they would implement either +> CustomIntScalar with the intValueForSerialization method or CustomFloatScalar with the +> floatValueForSerialization method respectively. \ No newline at end of file diff --git a/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java index 76d80f21f..38d60c045 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java @@ -8,6 +8,11 @@ import io.smallrye.common.annotation.Experimental; +/** + * Allows for definition of custom graphql scalars. Types with this annotation should extend one of + * CustomStringScalar, CustomIntScalar, or CustomFloatScalar. Additionally, the Type should provide + * a single argument constructor taking the associated type(String, BigInteger, BigDecimal). + */ @Retention(RUNTIME) @Target(TYPE) @Experimental("Mark a type as a custom scalar with the given name in the GraphQL schema.") diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java index fa9faa1c9..f5e57134d 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java @@ -166,7 +166,6 @@ private void verifyInjectionIsAvailable() { private void generateGraphQLSchema() { GraphQLSchema.Builder schemaBuilder = GraphQLSchema.newSchema(); - createGraphQLDirectiveTypes(); createGraphQLCustomScalarTypes(); createGraphQLEnumTypes(); createGraphQLDirectiveTypes(); @@ -257,7 +256,7 @@ private void createGraphQLCustomScalarType(CustomScalarType customScalarType) { } private static Coercing getCoercing(CustomScalarType customScalarType) { - CustomScalarPrimitiveType primitiveType = customScalarType.customScalarPrimitiveType(); + CustomScalarPrimitiveType primitiveType = customScalarType.getCustomScalarPrimitiveType(); Coercing coercing = null; switch (primitiveType) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java index 75054534c..f7fbd0e46 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java @@ -13,18 +13,29 @@ import io.smallrye.graphql.spi.ClassloadingService; +/** + * A base class for all CustomScalars that are based on GraphQL's Float. + */ public interface CustomFloatScalar { // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it // hides the parameterized type from Jsonb. + /** + * A serializer for CustomScalars based on a GraphQL Float, to inform JsonB how to serialize + * a CustomStringScalar to a BigDecimal value. + */ JsonbSerializer SERIALIZER = new JsonbSerializer<>() { @Override public void serialize(CustomFloatScalar customFloatScalar, JsonGenerator jsonGenerator, SerializationContext serializationContext) { - jsonGenerator.write(customFloatScalar.floatValue()); + jsonGenerator.write(customFloatScalar.floatValueForSerialization()); } }; + /** + * A deserializer for CustomScalars based on a GraphQL Float, to inform JsonB how to deserialize + * to an instance of a CustomFloatScalar. + */ JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { @Override public CustomFloatScalar deserialize(JsonParser jsonParser, @@ -44,5 +55,5 @@ public CustomFloatScalar deserialize(JsonParser jsonParser, } }; - BigDecimal floatValue(); + BigDecimal floatValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java index f049a60d6..ab8c4b57c 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java @@ -13,18 +13,29 @@ import io.smallrye.graphql.spi.ClassloadingService; +/** + * A base class for all CustomScalars that are based on GraphQL's Int. + */ public interface CustomIntScalar { // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it // hides the parameterized type from Jsonb. + /** + * A serializer for CustomScalars based on a GraphQL Int, to inform JsonB how to serialize + * a CustomStringScalar to a BigInteger value. + */ JsonbSerializer SERIALIZER = new JsonbSerializer<>() { @Override public void serialize(CustomIntScalar customIntScalar, JsonGenerator jsonGenerator, SerializationContext serializationContext) { - jsonGenerator.write(customIntScalar.intValue()); + jsonGenerator.write(customIntScalar.intValueForSerialization()); } }; + /** + * A deserializer for CustomScalars based on a GraphQL Int, to inform JsonB how to deserialize + * to an instance of a CustomIntScalar. + */ JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { @Override public CustomIntScalar deserialize(JsonParser jsonParser, @@ -44,5 +55,5 @@ public CustomIntScalar deserialize(JsonParser jsonParser, } }; - BigInteger intValue(); + BigInteger intValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java index 17413a5d6..e1ca84aeb 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java @@ -12,18 +12,29 @@ import io.smallrye.graphql.spi.ClassloadingService; +/** + * A base class for all CustomScalars that are based on GraphQL's String. + */ public interface CustomStringScalar { // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it // hides the parameterized type from Jsonb. + /** + * A serializer for CustomScalars based on GraphQL Strings, to inform JsonB how to serialize + * a CustomStringScalar to a String value. + */ JsonbSerializer SERIALIZER = new JsonbSerializer<>() { @Override public void serialize(CustomStringScalar customStringScalar, JsonGenerator jsonGenerator, SerializationContext serializationContext) { - jsonGenerator.write(customStringScalar.stringValue()); + jsonGenerator.write(customStringScalar.stringValueForSerialization()); } }; + /** + * A deserializer for CustomScalars based on GraphQL Strings, to inform JsonB how to deserialize + * to an instance of a CustomStringScalar. + */ JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { @Override public CustomStringScalar deserialize(JsonParser jsonParser, @@ -43,5 +54,5 @@ public CustomStringScalar deserialize(JsonParser jsonParser, } }; - String stringValue(); + String stringValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java index f9d471b2c..7fc522244 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java @@ -52,7 +52,7 @@ public BigDecimal serialize(Object input) throws CoercingSerializeException { throw new CoercingSerializeException( "Expected type String but was '" + typeName(input) + "'."); } - return result.floatValue(); + return result.floatValueForSerialization(); } @Override diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java index f3092b7f2..0b9bd8409 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java @@ -52,7 +52,7 @@ public BigInteger serialize(Object input) throws CoercingSerializeException { throw new CoercingSerializeException( "Expected type String but was '" + typeName(input) + "'."); } - return result.intValue(); + return result.intValueForSerialization(); } @Override diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java index bbb9b5a58..1a977f295 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java @@ -45,12 +45,12 @@ private CustomStringScalar convertImpl(Object input) { @Override public String serialize(Object input) throws CoercingSerializeException { - Object result = convertImpl(input); + CustomStringScalar result = convertImpl(input); if (result == null) { throw new CoercingSerializeException( "Expected type String but was '" + typeName(input) + "'."); } - return result.toString(); + return result.stringValueForSerialization(); } @Override diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java index 9531e62aa..992c74794 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java @@ -34,7 +34,7 @@ public String toString() { } @Override - public String stringValue() { + public String stringValueForSerialization() { return toString(); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java index cf52c33f1..d0ed9de68 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java @@ -68,14 +68,14 @@ public String inAsFieldNullable(ObjNullable input) { @Query public BigDecimal inAsFScalarNullable(TwiceTheFloat fScalar) { - return null == fScalar ? null : fScalar.floatValue().setScale(1, RoundingMode.HALF_EVEN); + return null == fScalar ? null : fScalar.floatValueForSerialization().setScale(1, RoundingMode.HALF_EVEN); } @Query public BigDecimal inAsFFieldNullable(FObjNullable input) { return null == input ? null : null == input.fScalar ? null - : input.fScalar.floatValue().setScale(1, RoundingMode.HALF_EVEN); + : input.fScalar.floatValueForSerialization().setScale(1, RoundingMode.HALF_EVEN); } @Query @@ -85,7 +85,7 @@ public TwiceTheFloat outputFloat() { @Query public ObjOfScalars outputScalars() { - ObjOfScalars oScalars = new ObjOfScalars(); + ObjOfScalars oScalars = new ObjOfScalars(); oScalars.fScalar = new TwiceTheFloat(BigDecimal.valueOf(30)); oScalars.sScalar = new BigDecimalString("98765.56789"); return oScalars; diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java index 07d0b46c6..c90a6d03f 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java @@ -34,7 +34,7 @@ public String toString() { } @Override - public BigDecimal floatValue() { + public BigDecimal floatValueForSerialization() { return value.multiply(BigDecimal.valueOf(2)); } } From af03f26bea48eb8dccc358ea7d9c95d0f40e6ece Mon Sep 17 00:00:00 2001 From: "shane.hirsekorn" Date: Tue, 19 Dec 2023 15:30:51 -0700 Subject: [PATCH 4/6] Move (de)serialization and change error handling --- .../creator/type/CustomScalarCreator.java | 11 +- docs/custom-scalar.md | 4 +- .../io/smallrye/graphql/api/CustomScalar.java | 6 +- .../smallrye/graphql/json/JsonBCreator.java | 137 +++++++++++++++++- .../scalar/custom/CustomFloatScalar.java | 49 ------- .../scalar/custom/CustomIntScalar.java | 49 ------- .../scalar/custom/CustomStringScalar.java | 50 ------- .../graphql/scalar/custom/FloatCoercing.java | 50 ++++--- .../graphql/scalar/custom/IntCoercing.java | 49 ++++--- .../graphql/scalar/custom/StringCoercing.java | 52 ++++--- .../tests/customscalars/CustomScalarTest.java | 12 +- .../graphql/tests/customscalars/SomeApi.java | 2 +- 12 files changed, 248 insertions(+), 223 deletions(-) diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java index debd22fae..0767cd3b7 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java @@ -54,9 +54,9 @@ public CustomScalarType create( checkForOneArgConstructor(classInfo, String.class); customScalarPrimitiveType = STRING_TYPE; } else { - //TODO error handle in the expected way - throw new RuntimeException("Required to implement a known CustomScalar primitive type. " - + "(CustomStringScalar, CustomFloatScalar, CustomIntScalar)"); + throw new RuntimeException(classInfo.name().toString() + " is required to implement a " + + "known CustomScalar primitive type. (CustomStringScalar, CustomFloatScalar, " + + "CustomIntScalar)"); } return new CustomScalarType( @@ -70,9 +70,8 @@ public CustomScalarType create( private static void checkForOneArgConstructor(final ClassInfo classInfo, Class argType) { if (classInfo.constructors().stream().noneMatch(methodInfo -> methodInfo.parameters().size() == 1 && methodInfo.parameterType(0).equals(ClassType.create(argType)))) { - //TODO error handle in the expected way - throw new RuntimeException("Required to implement a one arg constructor with paramtype of " - + argType.getName()); + throw new RuntimeException(classInfo.name().toString() + " is required to implement a " + + "one arg constructor with a type of " + argType.getName()); } } diff --git a/docs/custom-scalar.md b/docs/custom-scalar.md index ed577cb5e..e8a5fe994 100644 --- a/docs/custom-scalar.md +++ b/docs/custom-scalar.md @@ -3,7 +3,9 @@ Creating GraphQL Custom Scalars with SmallRye GraphQL While by default **MicroProfile GraphQL** specification doesn't provide direct support for creating custom scalars for GraphQL literal types of String, Int and Float, **SmallRye GraphQL** has implemented this feature based on user feedback. To implement a **custom scalar** with **SmallRye GraphQL**, -you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below: +you can annotate your custom scalar class with `@CustomScalar` in addition to following the pattern below +of including a public constructor that takes one of `String`|`BigDecimal`|`BigInteger` depending on +the scalar type: ```java @CustomScalar("BigDecimalString") public class BigDecimalString implements CustomStringScalar { diff --git a/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java index 38d60c045..c4c9860c7 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomScalar.java @@ -9,9 +9,9 @@ import io.smallrye.common.annotation.Experimental; /** - * Allows for definition of custom graphql scalars. Types with this annotation should extend one of - * CustomStringScalar, CustomIntScalar, or CustomFloatScalar. Additionally, the Type should provide - * a single argument constructor taking the associated type(String, BigInteger, BigDecimal). + * Allows for definition of custom graphql scalars. Types with this annotation should extend one of + * CustomStringScalar, CustomIntScalar, or CustomFloatScalar. Additionally, the Type should provide + * a public single argument constructor taking the associated type(String, BigInteger, BigDecimal). */ @Retention(RUNTIME) @Target(TYPE) diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java index 2340b1010..f6a33d243 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java @@ -1,18 +1,35 @@ package io.smallrye.graphql.json; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_FLOAT_DESERIALIZER; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_FLOAT_SERIALIZER; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_INT_DESERIALIZER; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_INT_SERIALIZER; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_STRING_DESERIALIZER; +import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_STRING_SERIALIZER; + +import java.lang.reflect.Type; +import java.math.BigDecimal; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import jakarta.json.JsonValue.ValueType; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; import io.smallrye.graphql.scalar.custom.CustomFloatScalar; import io.smallrye.graphql.scalar.custom.CustomIntScalar; import io.smallrye.graphql.scalar.custom.CustomStringScalar; import io.smallrye.graphql.schema.model.Field; import io.smallrye.graphql.schema.model.InputType; +import io.smallrye.graphql.spi.ClassloadingService; /** * Here we create JsonB Objects for certain input object. @@ -25,10 +42,12 @@ public class JsonBCreator { private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig() .withFormatting(true) .withNullValues(true) //null values are required by @JsonbCreator - .withSerializers(CustomStringScalar.SERIALIZER, CustomIntScalar.SERIALIZER, - CustomFloatScalar.SERIALIZER) - .withDeserializers(CustomStringScalar.DESERIALIZER, CustomIntScalar.DESERIALIZER, - CustomFloatScalar.DESERIALIZER)); + .withSerializers(CUSTOM_STRING_SERIALIZER, + CUSTOM_INT_SERIALIZER, + CUSTOM_FLOAT_SERIALIZER) + .withDeserializers(CUSTOM_STRING_DESERIALIZER, + CUSTOM_INT_DESERIALIZER, + CUSTOM_FLOAT_DESERIALIZER)); private static final Map jsonMap = new HashMap<>(); @@ -82,4 +101,114 @@ private static JsonbConfig createDefaultConfig() { .withNullValues(Boolean.TRUE) .withFormatting(Boolean.TRUE); } + + static class CustomScalarSerializers { + // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it + // hides the parameterized type from Jsonb. + + /** + * A serializer for CustomScalars based on GraphQL Strings, to inform JsonB how to serialize + * a CustomStringScalar to a String value. + */ + static JsonbSerializer CUSTOM_STRING_SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomStringScalar customStringScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customStringScalar.stringValueForSerialization()); + } + }; + + /** + * A deserializer for CustomScalars based on GraphQL Strings, to inform JsonB how to + * deserialize to an instance of a CustomStringScalar. + */ + static JsonbDeserializer CUSTOM_STRING_DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomStringScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomStringScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(String.class) + .newInstance(jsonParser.getString()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + /** + * A serializer for CustomScalars based on a GraphQL Int, to inform JsonB how to serialize + * a CustomStringScalar to a BigInteger value. + */ + static JsonbSerializer CUSTOM_INT_SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomIntScalar customIntScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customIntScalar.intValueForSerialization()); + } + }; + + /** + * A deserializer for CustomScalars based on a GraphQL Int, to inform JsonB how to + * deserialize to an instance of a CustomIntScalar. + */ + static JsonbDeserializer CUSTOM_INT_DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomIntScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomIntScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(Integer.class) + .newInstance(jsonParser.getInt()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + /** + * A serializer for CustomScalars based on a GraphQL Float, to inform JsonB how to serialize + * a CustomStringScalar to a BigDecimal value. + */ + static JsonbSerializer CUSTOM_FLOAT_SERIALIZER = new JsonbSerializer<>() { + @Override + public void serialize(CustomFloatScalar customFloatScalar, JsonGenerator jsonGenerator, + SerializationContext serializationContext) { + jsonGenerator.write(customFloatScalar.floatValueForSerialization()); + } + }; + + /** + * A deserializer for CustomScalars based on a GraphQL Float, to inform JsonB how to + * deserialize to an instance of a CustomFloatScalar. + */ + static JsonbDeserializer CUSTOM_FLOAT_DESERIALIZER = new JsonbDeserializer<>() { + @Override + public CustomFloatScalar deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext, Type type) { + ClassloadingService classloadingService = ClassloadingService.get(); + try { + if (jsonParser.getValue().getValueType() == ValueType.NULL) { + return null; + } else { + return (CustomFloatScalar) classloadingService.loadClass(type.getTypeName()) + .getConstructor(BigDecimal.class) + .newInstance(jsonParser.getBigDecimal()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java index f7fbd0e46..78e475fa8 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java @@ -1,59 +1,10 @@ package io.smallrye.graphql.scalar.custom; -import java.lang.reflect.Type; import java.math.BigDecimal; -import jakarta.json.JsonValue.ValueType; -import jakarta.json.bind.serializer.DeserializationContext; -import jakarta.json.bind.serializer.JsonbDeserializer; -import jakarta.json.bind.serializer.JsonbSerializer; -import jakarta.json.bind.serializer.SerializationContext; -import jakarta.json.stream.JsonGenerator; -import jakarta.json.stream.JsonParser; - -import io.smallrye.graphql.spi.ClassloadingService; - /** * A base class for all CustomScalars that are based on GraphQL's Float. */ public interface CustomFloatScalar { - // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it - // hides the parameterized type from Jsonb. - - /** - * A serializer for CustomScalars based on a GraphQL Float, to inform JsonB how to serialize - * a CustomStringScalar to a BigDecimal value. - */ - JsonbSerializer SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomFloatScalar customFloatScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customFloatScalar.floatValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on a GraphQL Float, to inform JsonB how to deserialize - * to an instance of a CustomFloatScalar. - */ - JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomFloatScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomFloatScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(BigDecimal.class) - .newInstance(jsonParser.getBigDecimal()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - BigDecimal floatValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java index ab8c4b57c..53e719fd1 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java @@ -1,59 +1,10 @@ package io.smallrye.graphql.scalar.custom; -import java.lang.reflect.Type; import java.math.BigInteger; -import jakarta.json.JsonValue.ValueType; -import jakarta.json.bind.serializer.DeserializationContext; -import jakarta.json.bind.serializer.JsonbDeserializer; -import jakarta.json.bind.serializer.JsonbSerializer; -import jakarta.json.bind.serializer.SerializationContext; -import jakarta.json.stream.JsonGenerator; -import jakarta.json.stream.JsonParser; - -import io.smallrye.graphql.spi.ClassloadingService; - /** * A base class for all CustomScalars that are based on GraphQL's Int. */ public interface CustomIntScalar { - // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it - // hides the parameterized type from Jsonb. - - /** - * A serializer for CustomScalars based on a GraphQL Int, to inform JsonB how to serialize - * a CustomStringScalar to a BigInteger value. - */ - JsonbSerializer SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomIntScalar customIntScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customIntScalar.intValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on a GraphQL Int, to inform JsonB how to deserialize - * to an instance of a CustomIntScalar. - */ - JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomIntScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomIntScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(Integer.class) - .newInstance(jsonParser.getInt()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - BigInteger intValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java index e1ca84aeb..22abcf630 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java @@ -1,58 +1,8 @@ package io.smallrye.graphql.scalar.custom; -import java.lang.reflect.Type; - -import jakarta.json.JsonValue.ValueType; -import jakarta.json.bind.serializer.DeserializationContext; -import jakarta.json.bind.serializer.JsonbDeserializer; -import jakarta.json.bind.serializer.JsonbSerializer; -import jakarta.json.bind.serializer.SerializationContext; -import jakarta.json.stream.JsonGenerator; -import jakarta.json.stream.JsonParser; - -import io.smallrye.graphql.spi.ClassloadingService; - /** * A base class for all CustomScalars that are based on GraphQL's String. */ public interface CustomStringScalar { - // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it - // hides the parameterized type from Jsonb. - - /** - * A serializer for CustomScalars based on GraphQL Strings, to inform JsonB how to serialize - * a CustomStringScalar to a String value. - */ - JsonbSerializer SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomStringScalar customStringScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customStringScalar.stringValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on GraphQL Strings, to inform JsonB how to deserialize - * to an instance of a CustomStringScalar. - */ - JsonbDeserializer DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomStringScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomStringScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(String.class) - .newInstance(jsonParser.getString()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - String stringValueForSerialization(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java index 7fc522244..bfcd52dcf 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java @@ -1,5 +1,8 @@ package io.smallrye.graphql.scalar.custom; +import static io.smallrye.graphql.SmallRyeGraphQLServerMessages.msg; + +import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import graphql.language.FloatValue; @@ -19,13 +22,11 @@ public FloatCoercing(String customScalarClass) { this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); } - private CustomFloatScalar newInstance(BigDecimal graphqlPrimitiveValue) { - try { - return graphqlPrimitiveValue == null ? null - : customScalarClass.getConstructor(BigDecimal.class).newInstance(graphqlPrimitiveValue); - } catch (Exception e) { - throw new CoercingSerializeException("TODO bdupras better error handling here", e); - } + private CustomFloatScalar newInstance(BigDecimal graphqlPrimitiveValue) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(BigDecimal.class).newInstance(graphqlPrimitiveValue); + } /* Coercing implementation. Forgive the deprecated methods. */ @@ -36,31 +37,40 @@ private static String typeName(Object input) { return input.getClass().getSimpleName(); } - private CustomFloatScalar convertImpl(Object input) { + private CustomFloatScalar convertImpl(Object input) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { if (input instanceof BigDecimal) { return newInstance((BigDecimal) input); } else if (input.getClass().isAssignableFrom(customScalarClass)) { return (CustomFloatScalar) input; } - return null; + throw new RuntimeException("Unable to convert null input."); } @Override public BigDecimal serialize(Object input) throws CoercingSerializeException { - CustomFloatScalar result = convertImpl(input); - if (result == null) { - throw new CoercingSerializeException( - "Expected type String but was '" + typeName(input) + "'."); + CustomFloatScalar result; + try { + result = convertImpl(input); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new CoercingSerializeException("Unable to serialize input " + input, e); + } catch (RuntimeException e) { + throw msg.coercingSerializeException("BigDecimal or class extending " + + customScalarClass, typeName(input), null); } return result.floatValueForSerialization(); } @Override public Object parseValue(Object input) throws CoercingParseValueException { - Object result = convertImpl(input); - if (result == null) { - throw new CoercingParseValueException( - "Expected type String but was '" + typeName(input) + "'."); + Object result; + try { + result = convertImpl(input); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new CoercingParseValueException("Unable to parse input: " + input, e); + } catch (RuntimeException e) { + throw msg.coercingParseValueException("BigDecimal or class extending " + + customScalarClass, typeName(input), null); } return result; } @@ -71,7 +81,11 @@ public Object parseLiteral(Object input) throws CoercingParseLiteralException { throw new CoercingParseLiteralException( "Expected a String AST type object but was '" + typeName(input) + "'."); } - return newInstance(((FloatValue) input).getValue()); + try { + return newInstance(((FloatValue) input).getValue()); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new CoercingParseLiteralException("Unable to parse literal:" + input, e); + } } @Override diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java index 0b9bd8409..146fa8444 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java @@ -1,5 +1,8 @@ package io.smallrye.graphql.scalar.custom; +import static io.smallrye.graphql.SmallRyeGraphQLServerMessages.msg; + +import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import graphql.language.IntValue; @@ -19,13 +22,10 @@ public IntCoercing(String customScalarClass) { this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); } - private CustomIntScalar newInstance(BigInteger graphqlPrimitiveValue) { - try { - return graphqlPrimitiveValue == null ? null - : customScalarClass.getConstructor(BigInteger.class).newInstance(graphqlPrimitiveValue); - } catch (Exception e) { - throw new CoercingSerializeException("TODO bdupras better error handling here", e); - } + private CustomIntScalar newInstance(BigInteger graphqlPrimitiveValue) + throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(BigInteger.class).newInstance(graphqlPrimitiveValue); } /* Coercing implementation. Forgive the deprecated methods. */ @@ -36,31 +36,40 @@ private static String typeName(Object input) { return input.getClass().getSimpleName(); } - private CustomIntScalar convertImpl(Object input) { + private CustomIntScalar convertImpl(Object input) + throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { if (input instanceof BigInteger) { return newInstance((BigInteger) input); } else if (input.getClass().isAssignableFrom(customScalarClass)) { return (CustomIntScalar) input; } - return null; + throw new RuntimeException("Unable to convert null input."); } @Override public BigInteger serialize(Object input) throws CoercingSerializeException { - CustomIntScalar result = convertImpl(input); - if (result == null) { - throw new CoercingSerializeException( - "Expected type String but was '" + typeName(input) + "'."); + CustomIntScalar result; + try { + result = convertImpl(input); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException | NoSuchMethodException e) { + throw new CoercingSerializeException("Unable to serialize input: " + input, e); + } catch (RuntimeException e) { + throw msg.coercingSerializeException("BigInteger or class extending " + + customScalarClass, typeName(input), e); } return result.intValueForSerialization(); } @Override public Object parseValue(Object input) throws CoercingParseValueException { - Object result = convertImpl(input); - if (result == null) { - throw new CoercingParseValueException( - "Expected type String but was '" + typeName(input) + "'."); + Object result; + try { + result = convertImpl(input); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException | NoSuchMethodException e) { + throw new CoercingParseValueException("Unable to serialize input: " + input, e); + } catch (RuntimeException e) { + throw msg.coercingParseValueException("BigInteger or class extending " + + customScalarClass, typeName(input), null); } return result; } @@ -71,7 +80,11 @@ public Object parseLiteral(Object input) throws CoercingParseLiteralException { throw new CoercingParseLiteralException( "Expected a String AST type object but was '" + typeName(input) + "'."); } - return newInstance(((IntValue) input).getValue()); + try { + return newInstance(((IntValue) input).getValue()); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException | NoSuchMethodException e) { + throw new CoercingParseLiteralException("Unable to parse literal: " + input, e); + } } @Override diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java index 1a977f295..d3be5bb3c 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java @@ -1,5 +1,9 @@ package io.smallrye.graphql.scalar.custom; +import static io.smallrye.graphql.SmallRyeGraphQLServerMessages.msg; + +import java.lang.reflect.InvocationTargetException; + import graphql.language.StringValue; import graphql.language.Value; import graphql.schema.Coercing; @@ -17,13 +21,12 @@ public StringCoercing(String customScalarClass) { this.customScalarClass = (Class) classloadingService.loadClass(customScalarClass); } - private CustomStringScalar newInstance(String graphqlPrimitiveValue) { - try { - return graphqlPrimitiveValue == null ? null - : customScalarClass.getConstructor(String.class).newInstance(graphqlPrimitiveValue); - } catch (Exception e) { - throw new CoercingSerializeException("TODO bdupras better error handling here", e); - } + private CustomStringScalar newInstance(String graphqlPrimitiveValue) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + + return graphqlPrimitiveValue == null ? null + : customScalarClass.getConstructor(String.class).newInstance(graphqlPrimitiveValue); + } /* Coercing implementation. Forgive the deprecated methods. */ @@ -34,31 +37,40 @@ private static String typeName(Object input) { return input.getClass().getSimpleName(); } - private CustomStringScalar convertImpl(Object input) { + private CustomStringScalar convertImpl(Object input) + throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { if (input instanceof String) { return newInstance((String) input); } else if (input.getClass().isAssignableFrom(customScalarClass)) { return (CustomStringScalar) input; } - return null; + throw new RuntimeException("Unable to convert null input."); } @Override public String serialize(Object input) throws CoercingSerializeException { - CustomStringScalar result = convertImpl(input); - if (result == null) { - throw new CoercingSerializeException( - "Expected type String but was '" + typeName(input) + "'."); + CustomStringScalar result; + try { + result = convertImpl(input); + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + throw new CoercingSerializeException("Unable to serialize object.", e); + } catch (RuntimeException e) { + throw msg.coercingSerializeException("String or class extending " + + customScalarClass, typeName(input), null); } return result.stringValueForSerialization(); } @Override public Object parseValue(Object input) throws CoercingParseValueException { - Object result = convertImpl(input); - if (result == null) { - throw new CoercingParseValueException( - "Expected type String but was '" + typeName(input) + "'."); + Object result; + try { + result = convertImpl(input); + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + throw new CoercingParseValueException("Unable to parse value: " + input, e); + } catch (RuntimeException e) { + throw msg.coercingParseValueException("String or class extending " + + customScalarClass, typeName(input), null); } return result; } @@ -69,7 +81,11 @@ public Object parseLiteral(Object input) throws CoercingParseLiteralException { throw new CoercingParseLiteralException( "Expected a String AST type object but was '" + typeName(input) + "'."); } - return newInstance(((StringValue) input).getValue()); + try { + return newInstance(((StringValue) input).getValue()); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { + throw new CoercingParseLiteralException("Unable to parse literal: " + input, e); + } } @Override diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java index 6206e54e4..e9c988a12 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/CustomScalarTest.java @@ -179,9 +179,9 @@ public void outputTwiceTheFloatTest() { GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); assertThat(graphQLAssured - .post("query { outputFloat }")) - .contains("{\"data\":{\"outputFloat\":10.0}}") - .doesNotContain("error"); + .post("query { outputFloat }")) + .contains("{\"data\":{\"outputFloat\":10.0}}") + .doesNotContain("error"); } @Test @@ -189,8 +189,8 @@ public void outputScalarObjectTest() { GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); assertThat(graphQLAssured - .post("query { outputScalars {fScalar, sScalar}}")) - .contains("{\"data\":{\"outputScalars\":{\"fScalar\":30.0,\"sScalar\":\"98765.56789\"}}}") - .doesNotContain("error"); + .post("query { outputScalars {fScalar, sScalar}}")) + .contains("{\"data\":{\"outputScalars\":{\"fScalar\":30.0,\"sScalar\":\"98765.56789\"}}}") + .doesNotContain("error"); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java index d0ed9de68..de71f83ab 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/SomeApi.java @@ -75,7 +75,7 @@ public BigDecimal inAsFScalarNullable(TwiceTheFloat fScalar) { public BigDecimal inAsFFieldNullable(FObjNullable input) { return null == input ? null : null == input.fScalar ? null - : input.fScalar.floatValueForSerialization().setScale(1, RoundingMode.HALF_EVEN); + : input.fScalar.floatValueForSerialization().setScale(1, RoundingMode.HALF_EVEN); } @Query From 528abf14e38ba9dbca5bfe924f491302bd0d1fce Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Wed, 20 Dec 2023 07:30:52 +0100 Subject: [PATCH 5/6] Move Custom*Scalar interfaces into the API module --- .../graphql/schema/creator/type/CustomScalarCreator.java | 6 +++--- .../java/io/smallrye/graphql/api}/CustomFloatScalar.java | 2 +- .../main/java/io/smallrye/graphql/api}/CustomIntScalar.java | 2 +- .../java/io/smallrye/graphql/api}/CustomStringScalar.java | 2 +- .../main/java/io/smallrye/graphql/json/JsonBCreator.java | 6 +++--- .../io/smallrye/graphql/scalar/custom/FloatCoercing.java | 1 + .../java/io/smallrye/graphql/scalar/custom/IntCoercing.java | 1 + .../io/smallrye/graphql/scalar/custom/StringCoercing.java | 1 + .../graphql/tests/customscalars/BigDecimalString.java | 2 +- .../smallrye/graphql/tests/customscalars/TwiceTheFloat.java | 2 +- 10 files changed, 14 insertions(+), 11 deletions(-) rename server/{implementation/src/main/java/io/smallrye/graphql/scalar/custom => api/src/main/java/io/smallrye/graphql/api}/CustomFloatScalar.java (82%) rename server/{implementation/src/main/java/io/smallrye/graphql/scalar/custom => api/src/main/java/io/smallrye/graphql/api}/CustomIntScalar.java (81%) rename server/{implementation/src/main/java/io/smallrye/graphql/scalar/custom => api/src/main/java/io/smallrye/graphql/api}/CustomStringScalar.java (79%) diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java index 0767cd3b7..d4194a985 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/CustomScalarCreator.java @@ -44,13 +44,13 @@ public CustomScalarType create( Set interfaces = classInfo.interfaceNames().stream().map(DotName::toString) .collect(Collectors.toSet()); CustomScalarPrimitiveType customScalarPrimitiveType; - if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomIntScalar")) { + if (interfaces.contains("io.smallrye.graphql.api.CustomIntScalar")) { checkForOneArgConstructor(classInfo, BigInteger.class); customScalarPrimitiveType = INT_TYPE; - } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomFloatScalar")) { + } else if (interfaces.contains("io.smallrye.graphql.api.CustomFloatScalar")) { checkForOneArgConstructor(classInfo, BigDecimal.class); customScalarPrimitiveType = FLOAT_TYPE; - } else if (interfaces.contains("io.smallrye.graphql.scalar.custom.CustomStringScalar")) { + } else if (interfaces.contains("io.smallrye.graphql.api.CustomStringScalar")) { checkForOneArgConstructor(classInfo, String.class); customScalarPrimitiveType = STRING_TYPE; } else { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomFloatScalar.java similarity index 82% rename from server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java rename to server/api/src/main/java/io/smallrye/graphql/api/CustomFloatScalar.java index 78e475fa8..941fadf6a 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomFloatScalar.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomFloatScalar.java @@ -1,4 +1,4 @@ -package io.smallrye.graphql.scalar.custom; +package io.smallrye.graphql.api; import java.math.BigDecimal; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomIntScalar.java similarity index 81% rename from server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java rename to server/api/src/main/java/io/smallrye/graphql/api/CustomIntScalar.java index 53e719fd1..7839fbb9e 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomIntScalar.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomIntScalar.java @@ -1,4 +1,4 @@ -package io.smallrye.graphql.scalar.custom; +package io.smallrye.graphql.api; import java.math.BigInteger; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java b/server/api/src/main/java/io/smallrye/graphql/api/CustomStringScalar.java similarity index 79% rename from server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java rename to server/api/src/main/java/io/smallrye/graphql/api/CustomStringScalar.java index 22abcf630..41bbd20e1 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/CustomStringScalar.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/CustomStringScalar.java @@ -1,4 +1,4 @@ -package io.smallrye.graphql.scalar.custom; +package io.smallrye.graphql.api; /** * A base class for all CustomScalars that are based on GraphQL's String. diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java index f6a33d243..7f4ee2f2a 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java @@ -24,9 +24,9 @@ import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; -import io.smallrye.graphql.scalar.custom.CustomFloatScalar; -import io.smallrye.graphql.scalar.custom.CustomIntScalar; -import io.smallrye.graphql.scalar.custom.CustomStringScalar; +import io.smallrye.graphql.api.CustomFloatScalar; +import io.smallrye.graphql.api.CustomIntScalar; +import io.smallrye.graphql.api.CustomStringScalar; import io.smallrye.graphql.schema.model.Field; import io.smallrye.graphql.schema.model.InputType; import io.smallrye.graphql.spi.ClassloadingService; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java index bfcd52dcf..b14aa6dba 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/FloatCoercing.java @@ -11,6 +11,7 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import io.smallrye.graphql.api.CustomFloatScalar; import io.smallrye.graphql.spi.ClassloadingService; public class FloatCoercing implements Coercing { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java index 146fa8444..47d951c0b 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/IntCoercing.java @@ -11,6 +11,7 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import io.smallrye.graphql.api.CustomIntScalar; import io.smallrye.graphql.spi.ClassloadingService; public class IntCoercing implements Coercing { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java index d3be5bb3c..6ddb37da9 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/custom/StringCoercing.java @@ -10,6 +10,7 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import io.smallrye.graphql.api.CustomStringScalar; import io.smallrye.graphql.spi.ClassloadingService; public class StringCoercing implements Coercing { diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java index 992c74794..b46dd4d13 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/BigDecimalString.java @@ -3,7 +3,7 @@ import java.math.BigDecimal; import io.smallrye.graphql.api.CustomScalar; -import io.smallrye.graphql.scalar.custom.CustomStringScalar; +import io.smallrye.graphql.api.CustomStringScalar; /** * An alternative BigDecimal scalar that serializes to a GraphQL String instead of a Float. diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java index c90a6d03f..89ea30f3f 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/customscalars/TwiceTheFloat.java @@ -2,8 +2,8 @@ import java.math.BigDecimal; +import io.smallrye.graphql.api.CustomFloatScalar; import io.smallrye.graphql.api.CustomScalar; -import io.smallrye.graphql.scalar.custom.CustomFloatScalar; /** * A float scalar that serializes to a GraphQL Float equal to twice the internal value. From 62affe905460c005d808cf9da286ab4773fcebd8 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Fri, 22 Dec 2023 12:03:42 +0100 Subject: [PATCH 6/6] Add custom scalars to the docs navigation page --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index d854d3235..b9d6b4ca1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ nav: - Response extensions: 'extensions.md' - Returning Void from Mutations: 'mutation-void.md' - Handling of the WebSocket's init-payload: 'handling-init-payload-from-the-websocket.md' + - Custom scalars: 'custom-scalar.md' - Typesafe client: - Basic usage: 'typesafe-client-usage.md' - Reactive: 'typesafe-client-reactive-types.md'