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 2a51f06d7..84b560d04 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 @@ -572,6 +572,7 @@ private static Map getAnnotationsWithFilter(org.jbo public static final DotName DIRECTIVE = DotName.createSimple("io.smallrye.graphql.api.Directive"); public static final DotName DEFAULT_NON_NULL = DotName.createSimple("io.smallrye.graphql.api.DefaultNonNull"); public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable"); + public static final DotName RESULT = DotName.createSimple("io.smallrye.graphql.api.GraphQLResult"); // MicroProfile GraphQL Annotations public static final DotName GRAPHQL_API = DotName.createSimple("org.eclipse.microprofile.graphql.GraphQLApi"); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java index 7b27d67e8..0baeeca38 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java @@ -1,5 +1,7 @@ package io.smallrye.graphql.schema.creator; +import java.util.List; + import org.jboss.jandex.FieldInfo; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -15,10 +17,11 @@ import io.smallrye.graphql.schema.helper.NonNullHelper; import io.smallrye.graphql.schema.helper.TypeAutoNameStrategy; import io.smallrye.graphql.schema.model.Field; +import io.smallrye.graphql.schema.model.Operation; /** * Abstract creator - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public abstract class ModelCreator { @@ -43,36 +46,30 @@ public TypeAutoNameStrategy getTypeAutoNameStrategy() { } /** - * The the return type.This is usually the method return type, but can also be adapted to something else - * - * @param methodInfo method - * @return the return type + * The return type. This is usually the method return type, but can also be adapted to something else */ protected static Type getReturnType(MethodInfo methodInfo) { return methodInfo.returnType(); } /** - * The the return type.This is usually the method return type, but can also be adapted to something else - * - * @param fieldInfo - * @return the return type + * The return type. This is usually the method return type, but can also be adapted to something else */ protected static Type getReturnType(FieldInfo fieldInfo) { return fieldInfo.type(); - } protected void populateField(Direction direction, Field field, Type type, Annotations annotations) { // Wrapper - field.setWrapper(WrapperCreator.createWrapper(type).orElse(null)); + List declaredErrors = (field instanceof Operation) ? ((Operation) field).getDeclaredErrors() : null; + field.setWrapper(WrapperCreator.createWrapper(null, type, annotations, declaredErrors).orElse(null)); doPopulateField(direction, field, type, annotations); } protected void populateField(Direction direction, Field field, Type fieldType, Type methodType, Annotations annotations) { // Wrapper - field.setWrapper(WrapperCreator.createWrapper(fieldType, methodType).orElse(null)); + field.setWrapper(WrapperCreator.createWrapper(fieldType, methodType, annotations).orElse(null)); doPopulateField(direction, field, methodType, annotations); } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java index 1f51741cc..ed609bb2a 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java @@ -39,7 +39,7 @@ public OperationCreator(ReferenceCreator referenceCreator, ArgumentCreator argum * * @param methodInfo the java method * @param operationType the type of operation (Query / Mutation) - * @param type + * @param type the GraphQL type of the operation * @return a Operation that defines this GraphQL Operation */ public Operation createOperation(MethodInfo methodInfo, OperationType operationType, @@ -84,11 +84,24 @@ public Operation createOperation(MethodInfo methodInfo, OperationType operationT maybeArgument.ifPresent(operation::addArgument); } + // Declared Throwables + methodInfo.exceptions().stream().map(this::toError).forEach(operation::addDeclaredError); + populateField(Direction.OUT, operation, fieldType, annotationsForMethod); return operation; } + private String toError(Type type) { + var name = type.name().toString(); + name = name.substring(name.lastIndexOf('.') + 1); + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + if (name.endsWith("Exception")) { + name = name.substring(0, name.length() - 9); + } + return name; + } + private static void validateFieldType(MethodInfo methodInfo, OperationType operationType) { Type returnType = methodInfo.returnType(); if (returnType.kind().equals(Type.Kind.VOID)) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java index a9b3342b5..d632952e7 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java @@ -1,11 +1,17 @@ package io.smallrye.graphql.schema.creator; +import static java.util.Collections.emptyList; + +import java.util.List; import java.util.Optional; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.Type; import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.Classes; +import io.smallrye.graphql.schema.ScanningContext; import io.smallrye.graphql.schema.helper.NonNullHelper; import io.smallrye.graphql.schema.model.Wrapper; import io.smallrye.graphql.schema.model.WrapperType; @@ -22,29 +28,43 @@ public class WrapperCreator { private WrapperCreator() { } - public static Optional createWrapper(Type type) { - return createWrapper(null, type); + public static Optional createWrapper(Type type, Annotations annotations) { + return createWrapper(null, type, annotations); + } + + public static Optional createWrapper(Type fieldType, Type methodType, Annotations annotations) { + return createWrapper(fieldType, methodType, annotations, emptyList()); } /** * Create a Wrapper for a Field (that has properties and methods) - * + * * @param fieldType the java field type * @param methodType the java method type - * @return optional array + * @param annotations the annotations on the method + * @param declaredErrors the errors that the method declares + * @return optional Wrapper */ - public static Optional createWrapper(Type fieldType, Type methodType) { + public static Optional createWrapper(Type fieldType, Type methodType, Annotations annotations, + List declaredErrors) { + Optional resultAnnotation = annotations.getOneOfTheseAnnotations(Annotations.RESULT); + if (resultAnnotation.isPresent()) { + AnnotationValue mode = resultAnnotation.get().valueWithDefault(ScanningContext.getIndex(), "mode"); + if ("ERROR_FIELDS".equals(mode.asString())) { + Wrapper wrapper = new Wrapper(WrapperType.RESULT, methodType.name().toString(), true); + wrapper.setDeclaredErrors(declaredErrors); + return Optional.of(wrapper); + } + } if (Classes.isWrapper(methodType)) { Wrapper wrapper = new Wrapper(getWrapperType(methodType), methodType.name().toString()); // NotNull if (markParameterizedTypeNonNull(fieldType, methodType)) { - wrapper.setNotEmpty(true); + wrapper.setNonNull(true); } // Wrapper of wrapper - Optional wrapperOfWrapper = getWrapperOfWrapper(methodType); - if (wrapperOfWrapper.isPresent()) { - wrapper.setWrapper(wrapperOfWrapper.get()); - } + Optional wrapperOfWrapper = getWrapperOfWrapper(methodType, annotations); + wrapperOfWrapper.ifPresent(wrapper::setWrapper); return Optional.of(wrapper); } @@ -66,13 +86,13 @@ private static WrapperType getWrapperType(Type type) { return null; } - private static Optional getWrapperOfWrapper(Type type) { + private static Optional getWrapperOfWrapper(Type type, Annotations annotations) { if (Classes.isArray(type)) { Type typeInArray = type.asArrayType().component(); - return createWrapper(typeInArray); + return createWrapper(typeInArray, annotations); } else if (Classes.isParameterized(type)) { Type typeInCollection = type.asParameterizedType().arguments().get(0); - return createWrapper(typeInCollection); + return createWrapper(typeInCollection, annotations); } return Optional.empty(); } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java index bd2794399..84882bf90 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java @@ -77,19 +77,19 @@ public static Optional getAdaptWith(Direction direction, ReferenceCre Type from = arguments.get(0); Type to = arguments.get(1); adaptWith.setAdapterClass(type.name().toString()); - r.setWrapper(WrapperCreator.createWrapper(from).orElse(null)); + r.setWrapper(WrapperCreator.createWrapper(from, annotations).orElse(null)); adaptWith.setFromReference(r); if (Scalars.isScalar(to.name().toString())) { adaptWith.setToReference(Scalars.getScalar(to.name().toString())); } else { - Annotations annotationsAplicableToMe = annotations.removeAnnotations(Annotations.ADAPT_WITH, + Annotations annotationsApplicableToMe = annotations.removeAnnotations(Annotations.ADAPT_WITH, Annotations.JAKARTA_JSONB_TYPE_ADAPTER, Annotations.JAVAX_JSONB_TYPE_ADAPTER); // Remove the adaption annotation, as this is the type being adapted to Reference toRef = referenceCreator.createReferenceForAdapter(to, - annotationsAplicableToMe, direction); - toRef.setWrapper(WrapperCreator.createWrapper(to).orElse(null)); + annotationsApplicableToMe, direction); + toRef.setWrapper(WrapperCreator.createWrapper(to, annotations).orElse(null)); adaptWith.setToReference(toRef); } diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Operation.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Operation.java index b2ebb79b9..de6bd6b8b 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Operation.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Operation.java @@ -1,5 +1,6 @@ package io.smallrye.graphql.schema.model; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -41,6 +42,12 @@ public final class Operation extends Field { */ private Execute execute; + /** + * The error codes that the method declares to throw. + * This is required to build the error fields for a GraphQLResult. + */ + private List declaredErrors; + public Operation() { } @@ -104,9 +111,20 @@ public void setExecute(Execute execute) { this.execute = execute; } + public List getDeclaredErrors() { + return declaredErrors; + } + + public void addDeclaredError(String error) { + if (this.declaredErrors == null) { + this.declaredErrors = new ArrayList<>(); + } + this.declaredErrors.add(error); + } + @Override public String toString() { return "Operation{" + "className=" + className + ", arguments=" + arguments + ", operationType=" + operationType - + ", sourceFieldOn=" + sourceFieldOn + '}'; + + ", sourceFieldOn=" + sourceFieldOn + ", declaredErrors=" + declaredErrors + '}'; } } diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Wrapper.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Wrapper.java index ccc4bd14a..9ad666e8e 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Wrapper.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Wrapper.java @@ -1,18 +1,20 @@ package io.smallrye.graphql.schema.model; import java.io.Serializable; +import java.util.List; import java.util.Objects; /** * If the type is wrapped in a generics bucket or in an array, keep the info here. - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class Wrapper implements Serializable { private String wrapperClassName; - private boolean notEmpty = false; // Mark this to be not empty + private boolean nonNull = false; // Mark this to be non-null private WrapperType wrapperType = WrapperType.UNKNOWN; + private List declaredErrors; private Wrapper wrapper = null; @@ -24,10 +26,10 @@ public Wrapper(WrapperType wrapperType, String wrapperClassName) { this.wrapperClassName = wrapperClassName; } - public Wrapper(WrapperType wrapperType, String wrapperClassName, boolean notEmpty) { + public Wrapper(WrapperType wrapperType, String wrapperClassName, boolean nonNull) { this.wrapperType = wrapperType; this.wrapperClassName = wrapperClassName; - this.notEmpty = notEmpty; + this.nonNull = nonNull; } public WrapperType getWrapperType() { @@ -46,12 +48,20 @@ public void setWrapperClassName(String wrapperClassName) { this.wrapperClassName = wrapperClassName; } - public void setNotEmpty(boolean notEmpty) { - this.notEmpty = notEmpty; + public void setNonNull(boolean nonNull) { + this.nonNull = nonNull; } - public boolean isNotEmpty() { - return this.notEmpty; + public boolean isNonNull() { + return this.nonNull; + } + + public List getDeclaredErrors() { + return declaredErrors; + } + + public void setDeclaredErrors(List declaredErrors) { + this.declaredErrors = declaredErrors; } public Wrapper getWrapper() { @@ -86,23 +96,28 @@ public boolean isOptional() { return wrapperType.equals(WrapperType.OPTIONAL); } + public boolean isResult() { + return wrapperType.equals(WrapperType.RESULT); + } + public boolean isUnknown() { return wrapperType.equals(WrapperType.UNKNOWN); } @Override public String toString() { - return "Wrapper{" + "wrapperClassName=" + wrapperClassName + ", notEmpty=" + notEmpty + ", wrapperType=" + wrapperType - + ", wrapper=" + wrapper + '}'; + return "Wrapper{" + "wrapperClassName=" + wrapperClassName + ", nonNull=" + nonNull + ", wrapperType=" + wrapperType + + ", wrapper=" + wrapper + ", declaredErrors=" + declaredErrors + '}'; } @Override public int hashCode() { int hash = 7; hash = 59 * hash + Objects.hashCode(this.wrapperClassName); - hash = 59 * hash + (this.notEmpty ? 1 : 0); + hash = 59 * hash + (this.nonNull ? 1 : 0); hash = 59 * hash + Objects.hashCode(this.wrapperType); hash = 59 * hash + Objects.hashCode(this.wrapper); + hash = 59 * hash + Objects.hashCode(this.declaredErrors); return hash; } @@ -118,7 +133,7 @@ public boolean equals(Object obj) { return false; } final Wrapper other = (Wrapper) obj; - if (this.notEmpty != other.notEmpty) { + if (this.nonNull != other.nonNull) { return false; } if (!Objects.equals(this.wrapperClassName, other.wrapperClassName)) { @@ -130,6 +145,9 @@ public boolean equals(Object obj) { if (!Objects.equals(this.wrapper, other.wrapper)) { return false; } + if (!Objects.equals(this.declaredErrors, other.declaredErrors)) { + return false; + } return true; } } diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/WrapperType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/WrapperType.java index edb955af9..2a7e12a4d 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/WrapperType.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/WrapperType.java @@ -1,8 +1,8 @@ package io.smallrye.graphql.schema.model; /** - * Represent an wrapper type in the Schema. - * + * Represent a wrapper type in the Schema. + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public enum WrapperType { @@ -10,5 +10,6 @@ public enum WrapperType { COLLECTION, MAP, ARRAY, + RESULT, // see GraphQLResult UNKNOWN // Could be a plugged in type, or normal generics -} \ No newline at end of file +} diff --git a/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResult.java b/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResult.java new file mode 100644 index 000000000..ee4a3f584 --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResult.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.smallrye.graphql.api; + +import static io.smallrye.graphql.api.GraphQLResultMode.ERROR_FIELDS; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +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; + +/** + * Instead of directly returning the data of the return type, this annotation wraps it in a Result type, + * that also contains fields for all declared exceptions, + * e.g. annotating a query like + *
+ * SuperHero findByName(String name) throws SuperHeroNotFoundException {...} + *
+ * results in a GraphQL Schema like this: + *
+ * + *
+ * 
+ * type SuperHeroResult {
+ *   superHero: SuperHero
+ *   error_superHeroNotFound: SuperHeroNotFound
+ * }
+ *
+ * type Query {
+ *   findByName(name: String): SuperHeroResult
+ * }
+ * 
+ * 
+ *

+ * When placed at a method, only this method is wrapped. + * When placed at a class, all methods in the class are wrapped. + * When placed at a package, all methods in all classes in the package are wrapped. + */ +@Retention(RUNTIME) +@Target({ METHOD, TYPE, ANNOTATION_TYPE }) +@Experimental("Not covered by the specification. Subject to change.") +public @interface GraphQLResult { + GraphQLResultMode mode() default ERROR_FIELDS; +} diff --git a/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResultMode.java b/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResultMode.java new file mode 100644 index 000000000..e92300ed4 --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/GraphQLResultMode.java @@ -0,0 +1,7 @@ +package io.smallrye.graphql.api; + +public enum GraphQLResultMode { + NONE, + ERROR_FIELDS + // later: UNION +} 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 49d9f9a71..f27e545cb 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 @@ -1,5 +1,6 @@ package io.smallrye.graphql.bootstrap; +import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLList.list; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; import static graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY; @@ -37,8 +38,10 @@ import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLObjectType.Builder; import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; @@ -692,7 +695,7 @@ private GraphQLInputType createGraphQLInputType(Field field) { // Collection if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) { // Mandatory in the collection - if (wrapper.isNotEmpty()) { + if (wrapper.isNonNull()) { graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType); } // Collection depth @@ -719,10 +722,26 @@ private GraphQLOutputType createGraphQLOutputType(Field field, boolean isBatch) Wrapper wrapper = dataFetcherFactory.unwrap(field, isBatch); + if (wrapper != null && wrapper.isResult()) { + Builder wrapperType = new Builder() + .name(((GraphQLNamedOutputType) graphQLOutputType).getName() + "Result") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name(field.getMethodName()) + .type(graphQLOutputType) + .build()); + wrapper.getDeclaredErrors() + .forEach(declaredThrowable -> wrapperType.field(GraphQLFieldDefinition.newFieldDefinition() + .name("error_" + declaredThrowable) + .type(GraphQLString) // TODO 1423: use fields in exception to build a real type + .build())); + graphQLOutputType = GraphQLNonNull.nonNull(wrapperType.build()); // the Result type is always non-null + wrapper = wrapper.getWrapper(); + } + // Collection if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) { // Mandatory in the collection - if (wrapper.isNotEmpty()) { + if (wrapper.isNonNull()) { graphQLOutputType = GraphQLNonNull.nonNull(graphQLOutputType); } // Collection depth @@ -822,7 +841,7 @@ private GraphQLArgument createGraphQLArgument(Argument argument) { // Collection if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) { // Mandatory in the collection - if (wrapper.isNotEmpty()) { + if (wrapper.isNonNull()) { graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType); } // Collection depth diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/DataFetcherFactory.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/DataFetcherFactory.java index c361a507e..a82baf704 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/DataFetcherFactory.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/DataFetcherFactory.java @@ -31,7 +31,7 @@ */ public class DataFetcherFactory { - private List dataFetcherServices = new ArrayList<>(); + private final List dataFetcherServices = new ArrayList<>(); public DataFetcherFactory() { Iterator i = ServiceLoader.load(DataFetcherService.class).iterator(); @@ -64,6 +64,8 @@ public Wrapper unwrap(Field field, boolean isBatch) { return field.getWrapper().getWrapper(); } else if (field.hasWrapper() && field.getWrapper().isCollectionOrArrayOrMap()) { return field.getWrapper(); + } else if (field.hasWrapper() && field.getWrapper().isResult()) { + return field.getWrapper(); } else if (field.hasWrapper()) { // TODO: Move Generics logic here ? } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java index 7e20f1642..0aad41037 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java @@ -46,6 +46,11 @@ public ExecutionResponse(ExecutionResult executionResult) { this.executionResult = executionResult; } + @Override + public String toString() { + return getClass().getSimpleName() + "[" + executionResult + "]"; + } + public ExecutionResult getExecutionResult() { return this.executionResult; } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/AbstractHelper.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/AbstractHelper.java index 25e00b6d8..9b4f1e372 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/AbstractHelper.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/AbstractHelper.java @@ -32,8 +32,8 @@ public abstract class AbstractHelper { protected final ClassloadingService classloadingService = ClassloadingService.get(); - protected final DefaultMapAdapter mapAdapter = new DefaultMapAdapter(); - private final Map transformerMap = new HashMap<>(); + protected final DefaultMapAdapter mapAdapter = new DefaultMapAdapter<>(); + private final Map> transformerMap = new HashMap<>(); private final Map invokerMap = new HashMap<>(); protected AbstractHelper() { @@ -127,6 +127,8 @@ Object recursiveTransform(Object inputValue, Field field, DataFetchingEnvironmen } else if (field.hasWrapper() && field.getWrapper().isOptional()) { // Also handle optionals return recursiveTransformOptional(inputValue, field, dfe); + } else if (field.hasWrapper() && field.getWrapper().isResult()) { + return transformResult(inputValue, field); } else { // we need to transform before we make sure the type is correct inputValue = singleTransform(inputValue, field); @@ -155,7 +157,7 @@ Object recursiveAdapting(Object inputValue, Field field, DataFetchingEnvironment } else if (Classes.isMap(inputValue) && shouldAdaptWithToMap(field)) { return singleAdapting(inputValue, field, dfe); } else if (shouldAdaptWithFromMap(field)) { - return singleAdapting(new HashSet((Collection) inputValue), field, dfe); + return singleAdapting(new HashSet<>((Collection) inputValue), field, dfe); } else if (field.hasWrapper() && field.getWrapper().isCollection()) { return recursiveAdaptCollection(inputValue, field, dfe); } else if (field.hasWrapper() && field.getWrapper().isOptional()) { @@ -182,10 +184,10 @@ Object recursiveAdapting(Object inputValue, Field field, DataFetchingEnvironment private Object recursiveTransformArray(Object array, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { if (Classes.isCollection(array)) { - array = ((Collection) array).toArray(); + array = ((Collection) array).toArray(); } - Class classInCollection = getArrayType(field); + Class classInCollection = getArrayType(field); //Skip transform if not needed if (array.getClass().getComponentType().equals(classInCollection)) { @@ -216,10 +218,10 @@ private Object recursiveTransformArray(Object array, Field field, DataFetchingEn private Object recursiveAdaptArray(Object array, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { if (Classes.isCollection(array)) { - array = ((Collection) array).toArray(); + array = ((Collection) array).toArray(); } - Class classInCollection = getArrayType(field); + Class classInCollection = getArrayType(field); //Skip mapping if not needed if (array.getClass().getComponentType().equals(classInCollection)) { @@ -251,7 +253,7 @@ private Object recursiveAdaptArray(Object array, Field field, DataFetchingEnviro */ private Object recursiveTransformCollection(Object argumentValue, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { - Collection givenCollection = getGivenCollection(argumentValue); + Collection givenCollection = getGivenCollection(argumentValue); String collectionClassName = field.getWrapper().getWrapperClassName(); @@ -277,7 +279,7 @@ private Object recursiveTransformCollection(Object argumentValue, Field field, D */ private Object recursiveAdaptCollection(Object argumentValue, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { - Collection givenCollection = getGivenCollection(argumentValue); + Collection givenCollection = getGivenCollection(argumentValue); String collectionClassName = field.getWrapper().getWrapperClassName(); Collection convertedCollection = CollectionCreator.newCollection(collectionClassName, givenCollection.size()); @@ -302,16 +304,28 @@ private Object recursiveAdaptCollection(Object argumentValue, Field field, DataF private Optional recursiveTransformOptional(Object argumentValue, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { // Check the type and maybe apply transformation - if (argumentValue == null || !((Optional) argumentValue).isPresent()) { + if (argumentValue == null || ((Optional) argumentValue).isEmpty()) { return Optional.empty(); } else { - Optional optional = (Optional) argumentValue; + Optional optional = (Optional) argumentValue; Object o = optional.get(); Field f = getFieldInField(field); return Optional.of(recursiveTransform(o, f, dfe)); } } + /** + * Re-wrap the {@link io.smallrye.graphql.api.GraphQLResult result} in a one-element map. + * This is not yet specified by MicroProfile GraphQL. + * + * @param argumentValue the value as from graphql-java + * @param field the graphql-field + * @return a optional with the transformed value in. + */ + private Map transformResult(Object argumentValue, Field field) { + return Map.of(field.getName(), argumentValue); + } + /** * Add support for mapping an optional element. * @@ -322,10 +336,10 @@ private Optional recursiveTransformOptional(Object argumentValue, Field private Optional recursiveAdaptOptional(Object argumentValue, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { // Check the type and maybe apply transformation - if (argumentValue == null || !((Optional) argumentValue).isPresent()) { + if (argumentValue == null || !((Optional) argumentValue).isPresent()) { return Optional.empty(); } else { - Optional optional = (Optional) argumentValue; + Optional optional = (Optional) argumentValue; Object o = optional.get(); Field f = getFieldInField(field); return Optional.of(recursiveAdapting(o, f, dfe)); @@ -335,15 +349,14 @@ private Optional recursiveAdaptOptional(Object argumentValue, Field fiel protected Class getArrayType(Field field) { String classNameInCollection = field.getReference().getClassName(); - Class classInCollection = classloadingService.loadClass(classNameInCollection); - return classInCollection; + return classloadingService.loadClass(classNameInCollection); } - protected Transformer getTransformer(Field field) { + protected Transformer getTransformer(Field field) { if (transformerMap.containsKey(field.getName())) { return transformerMap.get(field.getName()); } - Transformer transformer = Transformer.transformer(field); + Transformer transformer = Transformer.transformer(field); transformerMap.put(field.getName(), transformer); return transformer; } @@ -384,7 +397,7 @@ private ReflectionInvoker getReflectionInvoker(String className, String methodNa } private Integer getKey(String className, String methodName, List parameterClasses) { - return Objects.hash(className, methodName, parameterClasses.toArray()); + return Objects.hash(className, methodName, Arrays.hashCode(parameterClasses.toArray())); } /** @@ -415,7 +428,6 @@ private Field getFieldInField(Field owner) { child.setDefaultValue(owner.getDefaultValue()); // wrapper - Wrapper wrapper = owner.getWrapper(); if (owner.hasWrapper()) { Wrapper ownerWrapper = owner.getWrapper(); if (ownerWrapper.getWrapper() != null) { @@ -428,9 +440,9 @@ private Field getFieldInField(Field owner) { } - private Collection getGivenCollection(Object argumentValue) { + private Collection getGivenCollection(Object argumentValue) { if (Classes.isCollection(argumentValue)) { - return (Collection) argumentValue; + return (Collection) argumentValue; } else { return Arrays.asList((T[]) argumentValue); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java index c8ec89c47..305ddcfca 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java @@ -102,7 +102,7 @@ public Field getAdaptedField(Field original) { original.getReference()); Wrapper wrapper = new Wrapper(); - wrapper.setNotEmpty(original.getWrapper().isNotEmpty()); + wrapper.setNonNull(original.getWrapper().isNonNull()); wrapper.setWrapperType(WrapperType.COLLECTION); wrapper.setWrapperClassName(Set.class.getName()); wrapper.setWrapper(original.getWrapper().getWrapper()); diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/Order.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/Order.java new file mode 100644 index 000000000..b884f5dce --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/Order.java @@ -0,0 +1,36 @@ +package io.smallrye.graphql.test.apps.result.api; + +import java.time.LocalDate; + +import org.eclipse.microprofile.graphql.Id; + +public class Order { + private @Id String id; + private LocalDate orderDate; + + public Order() { + this.id = null; + this.orderDate = null; + } + + public Order(String id, LocalDate orderDate) { + this.id = id; + this.orderDate = orderDate; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public LocalDate getOrderDate() { + return orderDate; + } + + public void setOrderDate(LocalDate orderDate) { + this.orderDate = orderDate; + } +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedException.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedException.java new file mode 100644 index 000000000..e8c661d6e --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedException.java @@ -0,0 +1,13 @@ +package io.smallrye.graphql.test.apps.result.api; + +public class OrderBlockedException extends RuntimeException { + private final OrderBlockedReason reason; + + public OrderBlockedException(OrderBlockedReason reason) { + this.reason = reason; + } + + public OrderBlockedReason getReason() { + return reason; + } +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedReason.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedReason.java new file mode 100644 index 000000000..94da2509c --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderBlockedReason.java @@ -0,0 +1,6 @@ +package io.smallrye.graphql.test.apps.result.api; + +public enum OrderBlockedReason { + INVALID_ADDRESS, + PAYMENT_FAILED +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderNotFoundException.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderNotFoundException.java new file mode 100644 index 000000000..7c2ea14a0 --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/OrderNotFoundException.java @@ -0,0 +1,4 @@ +package io.smallrye.graphql.test.apps.result.api; + +public class OrderNotFoundException extends RuntimeException { +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/ResultWrapperTestingApi.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/ResultWrapperTestingApi.java new file mode 100644 index 000000000..84ff4794a --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/result/api/ResultWrapperTestingApi.java @@ -0,0 +1,39 @@ +package io.smallrye.graphql.test.apps.result.api; + +import static io.smallrye.graphql.test.apps.result.api.OrderBlockedReason.INVALID_ADDRESS; +import static io.smallrye.graphql.test.apps.result.api.OrderBlockedReason.PAYMENT_FAILED; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.graphql.api.GraphQLResult; + +@GraphQLApi +public class ResultWrapperTestingApi { + private static final List ORDER_LIST = List.of( + new Order("1", LocalDate.of(2022, 5, 11)), + new Order("2", LocalDate.of(2022, 5, 21)), + new Order("3", LocalDate.of(2022, 5, 31))); + private static final Map ORDERS = ORDER_LIST.stream().collect(toMap(Order::getId, identity())); + + @Query + @GraphQLResult + public Order order(String id) throws OrderNotFoundException, OrderBlockedException { + if ("7".equals(id)) + throw new OrderBlockedException(INVALID_ADDRESS); + if ("8".equals(id)) + throw new OrderBlockedException(PAYMENT_FAILED); + if ("9".equals(id)) + throw new RuntimeException("we have a (technical) problem"); + var order = ORDERS.get(id); + if (order == null) + throw new OrderNotFoundException(); + return order; + } +} diff --git a/server/tck/src/test/resources/tests/result/input.graphql b/server/tck/src/test/resources/tests/result/input.graphql new file mode 100644 index 000000000..f053f9373 --- /dev/null +++ b/server/tck/src/test/resources/tests/result/input.graphql @@ -0,0 +1,8 @@ +{ + order(id: "1") { + order { + id + orderDate + } + } +} diff --git a/server/tck/src/test/resources/tests/result/output.json b/server/tck/src/test/resources/tests/result/output.json new file mode 100644 index 000000000..e4ba5a525 --- /dev/null +++ b/server/tck/src/test/resources/tests/result/output.json @@ -0,0 +1,10 @@ +{ + "data": { + "order": { + "order": { + "id": "1", + "orderDate": "2022-05-11" + } + } + } +} diff --git a/server/tck/src/test/resources/tests/result/test.properties b/server/tck/src/test/resources/tests/result/test.properties new file mode 100644 index 000000000..db74c7f9b --- /dev/null +++ b/server/tck/src/test/resources/tests/result/test.properties @@ -0,0 +1,2 @@ +ignore=false +priority=100