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 d36fe7dc2..1e707de30 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 @@ -207,7 +207,7 @@ public static Annotations getAnnotationsForArray(org.jboss.jandex.Type typeInCol return new Annotations(annotationMap); } - // + // /** * Used when we are creating operation and arguments for these operations @@ -576,6 +576,7 @@ private static Map getAnnotationsWithFilter(org.jbo public static final DotName INPUT = DotName.createSimple("org.eclipse.microprofile.graphql.Input"); public static final DotName TYPE = DotName.createSimple("org.eclipse.microprofile.graphql.Type"); public static final DotName INTERFACE = DotName.createSimple("org.eclipse.microprofile.graphql.Interface"); + public static final DotName UNION = DotName.createSimple("io.smallrye.graphql.api.Union"); public static final DotName ENUM = DotName.createSimple("org.eclipse.microprofile.graphql.Enum"); public static final DotName ID = DotName.createSimple("org.eclipse.microprofile.graphql.Id"); public static final DotName DESCRIPTION = DotName.createSimple("org.eclipse.microprofile.graphql.Description"); 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 cf1711924..529bdc3ec 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 @@ -28,6 +28,7 @@ import io.smallrye.graphql.schema.creator.type.InputTypeCreator; import io.smallrye.graphql.schema.creator.type.InterfaceCreator; import io.smallrye.graphql.schema.creator.type.TypeCreator; +import io.smallrye.graphql.schema.creator.type.UnionCreator; import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper; import io.smallrye.graphql.schema.helper.Directives; import io.smallrye.graphql.schema.helper.GroupHelper; @@ -64,6 +65,7 @@ public class SchemaBuilder { private final ReferenceCreator referenceCreator; private final OperationCreator operationCreator; private final DirectiveTypeCreator directiveTypeCreator; + private final UnionCreator unionCreator; /** * This builds the Schema from Jandex @@ -98,6 +100,7 @@ private SchemaBuilder(TypeAutoNameStrategy autoNameStrategy) { typeCreator = new TypeCreator(referenceCreator, fieldCreator, operationCreator); interfaceCreator = new InterfaceCreator(referenceCreator, fieldCreator, operationCreator); directiveTypeCreator = new DirectiveTypeCreator(referenceCreator); + unionCreator = new UnionCreator(referenceCreator); } private Schema generateSchema() { @@ -130,7 +133,7 @@ private Schema generateSchema() { // Add all custom datafetchers addDataFetchers(schema); - // Reset the maps. + // Reset the maps. referenceCreator.clear(); return schema; @@ -162,6 +165,9 @@ private void addTypesToSchema(Schema schema) { // Add the interface types createAndAddToSchema(ReferenceType.INTERFACE, interfaceCreator, schema::addInterface); + // Add the union types + createAndAddToSchema(ReferenceType.UNION, unionCreator, schema::addUnion); + // Add the enum types createAndAddToSchema(ReferenceType.ENUM, enumCreator, schema::addEnum); } @@ -185,6 +191,11 @@ private void addOutstandingTypesToSchema(Schema schema) { keepGoing = true; } + // See if there is any unions we missed + if (findOutstandingAndAddToSchema(ReferenceType.UNION, unionCreator, schema::containsUnion, schema::addUnion)) { + keepGoing = true; + } + // See if there is any enums we missed if (findOutstandingAndAddToSchema(ReferenceType.ENUM, enumCreator, schema::containsEnum, schema::addEnum)) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java index f2f7f1f0c..39eea5917 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java @@ -34,9 +34,9 @@ /** * Here we create references to things that might not yet exist. - * + * * We store all references to be created later. - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class ReferenceCreator { @@ -46,12 +46,14 @@ public class ReferenceCreator { private final Queue typeReferenceQueue = new ArrayDeque<>(); private final Queue enumReferenceQueue = new ArrayDeque<>(); private final Queue interfaceReferenceQueue = new ArrayDeque<>(); + private final Queue unionReferenceQueue = new ArrayDeque<>(); // Some maps we populate during scanning private final Map inputReferenceMap = new HashMap<>(); private final Map typeReferenceMap = new HashMap<>(); private final Map enumReferenceMap = new HashMap<>(); private final Map interfaceReferenceMap = new HashMap<>(); + private final Map unionReferenceMap = new HashMap<>(); private final TypeAutoNameStrategy autoNameStrategy; @@ -68,16 +70,18 @@ public void clear() { typeReferenceMap.clear(); enumReferenceMap.clear(); interfaceReferenceMap.clear(); + unionReferenceMap.clear(); inputReferenceQueue.clear(); typeReferenceQueue.clear(); enumReferenceQueue.clear(); interfaceReferenceQueue.clear(); + unionReferenceQueue.clear(); } /** * Get the values for a certain type - * + * * @param referenceType the type * @return the references */ @@ -87,7 +91,7 @@ public Queue values(ReferenceType referenceType) { /** * Get the Type auto name strategy - * + * * @return the strategy as supplied */ public TypeAutoNameStrategy getTypeAutoNameStrategy() { @@ -96,7 +100,7 @@ public TypeAutoNameStrategy getTypeAutoNameStrategy() { /** * Get a reference to a field type for an adapter on a field - * + * * @param direction the direction * @param fieldType the java type * @param annotations annotation on this operations method @@ -111,7 +115,7 @@ public Reference createReferenceForAdapter(Type fieldType, /** * Get a reference to a field type for an operation Direction is OUT on a field (and IN on an argument) In the case * of operations, there is no fields (only methods) - * + * * @param fieldType the java type * @param annotationsForMethod annotation on this operations method * @return a reference to the type @@ -123,7 +127,7 @@ public Reference createReferenceForOperationField(Type fieldType, Annotations an /** * Get a reference to a argument type for an operation Direction is IN on an argument (and OUT on a field) In the * case of operation, there is no field (only methods) - * + * * @param argumentType the java type * @param annotationsForThisArgument annotations on this argument * @return a reference to the argument @@ -145,9 +149,9 @@ public Reference createReferenceForSourceArgument(Type argumentType, Annotations /** * Get a reference to a field (method response) on an interface - * + * * Interfaces is only usable on Type, so the direction in OUT. - * + * * @param methodType the method response type * @param annotationsForThisMethod annotations on this method * @return a reference to the type @@ -159,9 +163,9 @@ public Reference createReferenceForInterfaceField(Type methodType, Annotations a /** * Get a reference to a Field Type for a InputType or Type. - * + * * We need both the type and the getter/setter method as both is applicable. - * + * * @param direction in or out * @param fieldType the field type * @param methodType the method type @@ -180,7 +184,7 @@ public Reference createReferenceForPojoField(Type fieldType, /** * This method create a reference to type that might not yet exist. It also store to be created later, if we do not * already know about it. - * + * * @param direction the direction (in or out) * @param classInfo the Java class * @param createAdapedToType create the type in the schema @@ -197,7 +201,7 @@ private Reference createReference(Direction direction, ReferenceType referenceType = getCorrectReferenceType(classInfo, annotationsForClass, direction); - if (referenceType.equals(ReferenceType.INTERFACE)) { + if (referenceType.equals(ReferenceType.INTERFACE) || referenceType.equals(ReferenceType.UNION)) { // Also check that we create all implementations Collection knownDirectImplementors = ScanningContext.getIndex() .getAllKnownImplementors(classInfo.name()); @@ -229,7 +233,7 @@ private Reference createReference(Direction direction, createReference(direction, impl, createAdapedToType, createAdapedWithType, parametrizedTypeArgumentsReferencesImpl, - true); + referenceType.equals(ReferenceType.INTERFACE)); } } @@ -276,8 +280,20 @@ private Reference createReference(Direction direction, private static boolean isInterface(ClassInfo classInfo, Annotations annotationsForClass) { boolean isJavaInterface = Classes.isInterface(classInfo); if (isJavaInterface) { - if (annotationsForClass.containsOneOfTheseAnnotations(Annotations.TYPE, Annotations.INPUT)) { - // This should be mapped to a type/input and not an interface + if (annotationsForClass.containsOneOfTheseAnnotations(Annotations.TYPE, Annotations.INPUT, Annotations.UNION)) { + // This should be mapped to a type/input/union and not an interface + return false; + } + return true; + } + return false; + } + + private static boolean isUnion(ClassInfo classInfo, Annotations annotationsForClass) { + boolean isJavaInterface = Classes.isInterface(classInfo); + if (isJavaInterface) { + if (annotationsForClass.containsOneOfTheseAnnotations(Annotations.TYPE, Annotations.INPUT, Annotations.INTERFACE)) { + // This should be mapped to a type/input/interface and not a union return false; } return true; @@ -468,6 +484,8 @@ private Map getReferenceMap(ReferenceType referenceType) { return inputReferenceMap; case INTERFACE: return interfaceReferenceMap; + case UNION: + return unionReferenceMap; case TYPE: return typeReferenceMap; default: @@ -483,6 +501,8 @@ private Queue getReferenceQueue(ReferenceType referenceType) { return inputReferenceQueue; case INTERFACE: return interfaceReferenceQueue; + case UNION: + return unionReferenceQueue; case TYPE: return typeReferenceQueue; default: @@ -493,6 +513,8 @@ private Queue getReferenceQueue(ReferenceType referenceType) { private static ReferenceType getCorrectReferenceType(ClassInfo classInfo, Annotations annotations, Direction direction) { if (isInterface(classInfo, annotations)) { return ReferenceType.INTERFACE; + } else if (isUnion(classInfo, annotations)) { + return ReferenceType.UNION; } else if (Classes.isEnum(classInfo)) { return ReferenceType.ENUM; } else if (direction.equals(Direction.IN)) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java index 5374b565f..ce5a2e944 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java @@ -67,8 +67,8 @@ public Type create(ClassInfo classInfo, Reference reference) { // Fields addFields(type, classInfo, reference); - // Interfaces - addInterfaces(type, classInfo, reference); + // Interfaces/Unions + addPolymorphicTypes(type, classInfo, reference); // Operations addOperations(type, classInfo); @@ -85,19 +85,25 @@ private void addDirectives(Type type, ClassInfo classInfo) { type.setDirectiveInstances(directives.buildDirectiveInstances(classInfo::classAnnotation)); } - private void addInterfaces(Type type, ClassInfo classInfo, Reference reference) { + private void addPolymorphicTypes(Type type, ClassInfo classInfo, Reference reference) { List interfaceNames = classInfo.interfaceTypes(); for (org.jboss.jandex.Type interfaceType : interfaceNames) { + String interfaceFullName = interfaceType.name().toString(); // Ignore java interfaces (like Serializable) - if (InterfaceCreator.canAddInterfaceIntoScheme(interfaceType.name().toString())) { + // TODO: should this check be renamed now that it is used for both union and interface checks? + if (InterfaceCreator.canAddInterfaceIntoScheme(interfaceFullName)) { ClassInfo interfaceInfo = ScanningContext.getIndex().getClassByName(interfaceType.name()); if (interfaceInfo != null) { Annotations annotationsForInterface = Annotations.getAnnotationsForClass(interfaceInfo); Reference interfaceRef = referenceCreator.createReferenceForInterfaceField(interfaceType, annotationsForInterface, reference); - type.addInterface(interfaceRef); - // add all parent interfaces recursively as GraphQL schema requires it - addInterfaces(type, interfaceInfo, reference); + if (annotationsForInterface.containsOneOfTheseAnnotations(Annotations.UNION)) { + type.addUnion(interfaceRef); + } else { + type.addInterface(interfaceRef); + // add all parent interfaces recursively as GraphQL schema requires it + addPolymorphicTypes(type, interfaceInfo, reference); + } } } } @@ -134,6 +140,17 @@ protected Map toOperations(Map methodParameterInfos = sourceFields.get(DotName.createSimple(className)); + for (MethodParameterInfo methodParameterInfo : methodParameterInfos) { + MethodInfo methodInfo = methodParameterInfo.method(); + Operation o = operationCreator.createOperation(methodInfo, OperationType.QUERY, type); + operations.put(o.getName(), o); + } + } + } return operations; } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/UnionCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/UnionCreator.java new file mode 100644 index 000000000..0df64f3af --- /dev/null +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/UnionCreator.java @@ -0,0 +1,44 @@ +package io.smallrye.graphql.schema.creator.type; + +import java.util.Optional; + +import org.jboss.jandex.ClassInfo; +import org.jboss.logging.Logger; + +import io.smallrye.graphql.schema.Annotations; +import io.smallrye.graphql.schema.creator.ReferenceCreator; +import io.smallrye.graphql.schema.helper.DescriptionHelper; +import io.smallrye.graphql.schema.helper.TypeNameHelper; +import io.smallrye.graphql.schema.model.Reference; +import io.smallrye.graphql.schema.model.ReferenceType; +import io.smallrye.graphql.schema.model.UnionType; + +public class UnionCreator implements Creator { + + private static final Logger LOG = Logger.getLogger(UnionCreator.class.getName()); + + private final ReferenceCreator referenceCreator; + + public UnionCreator(ReferenceCreator referenceCreator) { + this.referenceCreator = referenceCreator; + } + + @Override + public UnionType create(ClassInfo classInfo, Reference reference) { + LOG.debug("Creating union from " + classInfo.name().toString()); + + Annotations annotations = Annotations.getAnnotationsForClass(classInfo); + + // Name + String name = TypeNameHelper.getAnyTypeName(classInfo, + annotations, + referenceCreator.getTypeAutoNameStrategy(), + ReferenceType.UNION, + reference.getClassParametrizedTypes()); + + // Description + Optional maybeDescription = DescriptionHelper.getDescriptionForType(annotations); + + return new UnionType(classInfo.name().toString(), name, maybeDescription.orElse(null)); + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/ReferenceType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/ReferenceType.java index 6da3491f2..94f82e0a2 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/ReferenceType.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/ReferenceType.java @@ -2,9 +2,9 @@ /** * Type of reference - * + * * Because we refer to types before they might exist, we need an indication of the type - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public enum ReferenceType { @@ -12,5 +12,6 @@ public enum ReferenceType { TYPE, ENUM, INTERFACE, + UNION, SCALAR -} \ No newline at end of file +} 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 bae397673..f1418d312 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 @@ -29,6 +29,7 @@ public final class Schema implements Serializable { private Map inputs = new HashMap<>(); private Map types = new HashMap<>(); private Map interfaces = new HashMap<>(); + private Map unions = new HashMap<>(); private Map enums = new HashMap<>(); private Map errors = new HashMap<>(); @@ -201,6 +202,26 @@ public boolean hasInterfaces() { return !this.interfaces.isEmpty(); } + public Map getUnions() { + return unions; + } + + public void setUnions(Map unions) { + this.unions = unions; + } + + public void addUnion(UnionType unionType) { + this.unions.put(unionType.getName(), unionType); + } + + public boolean containsUnion(String name) { + return this.unions.containsKey(name); + } + + public boolean hasUnions() { + return !this.unions.isEmpty(); + } + public Map getEnums() { return enums; } diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Type.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Type.java index 29846a2cb..ea0a5dc08 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Type.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Type.java @@ -7,19 +7,19 @@ /** * Represent a GraphQL Type. - * + * * A Type is one of the options for a response, it's a complex type that contains * fields that itself is of a certain type. - * + * * It's a Java Bean that we only care about the getter methods and properties. - * + * * A Type is a java bean with fields, but can optionally also have operations (queries) * that is done with the Source annotation. - * + * * A Type can also optionally implements interfaces. - * + * * @see Object - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public final class Type extends Reference { @@ -33,6 +33,7 @@ public final class Type extends Reference { private Map batchOperations = new LinkedHashMap<>(); private Set interfaces = new LinkedHashSet<>(); + private Set unionMemberships = new LinkedHashSet<>(); public Type() { } @@ -131,13 +132,37 @@ public boolean isInterface() { } public void setIsInterface(boolean isInterface) { - isInterface = isInterface; + this.isInterface = isInterface; + } + + public Set getUnionMemberships() { + return unionMemberships; + } + + public void addUnion(Reference unionType) { + this.unionMemberships.add(unionType); + } + + public boolean hasUnionMemberships() { + return !this.unionMemberships.isEmpty(); + } + + public boolean isMemberOfUnion(Reference unionType) { + for (Reference u : unionMemberships) { + if (u.getName().equals(unionType.getName()) + && u.getClassName().equals(unionType.getClassName()) + && u.getGraphQLClassName().equals(unionType.getGraphQLClassName())) { + return true; + } + } + return false; } @Override public String toString() { return "Type{" + "description=" + description + ", isInterface=" + isInterface + ", fields=" + fields + ", operations=" - + operations + ", batchOperations=" + batchOperations + ", interfaces=" + interfaces + '}'; + + operations + ", batchOperations=" + batchOperations + ", interfaces=" + interfaces + ", unionMemberships=" + + unionMemberships + '}'; } } diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/UnionType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/UnionType.java new file mode 100644 index 000000000..2a94845c7 --- /dev/null +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/UnionType.java @@ -0,0 +1,24 @@ +package io.smallrye.graphql.schema.model; + +public final class UnionType extends Reference { + + private String description; + + public UnionType(String className, String name, String description) { + super(className, name, ReferenceType.UNION); + this.description = description; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "UnionType{" + "description=" + description + '}'; + } +} diff --git a/server/api/src/main/java/io/smallrye/graphql/api/Union.java b/server/api/src/main/java/io/smallrye/graphql/api/Union.java new file mode 100644 index 000000000..11edc91bd --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/Union.java @@ -0,0 +1,18 @@ +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("Allow you to mark an interface as a GraphQL Union. Not covered by the specification. " + + "Subject to change.") +public @interface Union { + + String value() default ""; +} 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 8578e5051..cf7c4a048 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 @@ -43,6 +43,7 @@ import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeReference; +import graphql.schema.GraphQLUnionType; import graphql.schema.visibility.BlockedFields; import graphql.schema.visibility.GraphqlFieldVisibility; import io.smallrye.graphql.SmallRyeGraphQLServerMessages; @@ -54,6 +55,8 @@ import io.smallrye.graphql.execution.event.EventEmitter; import io.smallrye.graphql.execution.resolver.InterfaceOutputRegistry; import io.smallrye.graphql.execution.resolver.InterfaceResolver; +import io.smallrye.graphql.execution.resolver.UnionOutputRegistry; +import io.smallrye.graphql.execution.resolver.UnionResolver; import io.smallrye.graphql.json.JsonBCreator; import io.smallrye.graphql.json.JsonInputRegistry; import io.smallrye.graphql.scalar.GraphQLScalarTypes; @@ -71,6 +74,7 @@ import io.smallrye.graphql.schema.model.ReferenceType; import io.smallrye.graphql.schema.model.Schema; import io.smallrye.graphql.schema.model.Type; +import io.smallrye.graphql.schema.model.UnionType; import io.smallrye.graphql.schema.model.Wrapper; import io.smallrye.graphql.spi.ClassloadingService; import io.smallrye.graphql.spi.LookupService; @@ -90,6 +94,7 @@ public class Bootstrap { private final Set directiveTypes = new LinkedHashSet<>(); private final Map enumMap = new HashMap<>(); private final Map interfaceMap = new HashMap<>(); + private final Map unionMap = new HashMap<>(); private final Map inputMap = new HashMap<>(); private final Map typeMap = new HashMap<>(); @@ -151,6 +156,7 @@ private void generateGraphQLSchema() { createGraphQLDirectiveTypes(); createGraphQLEnumTypes(); createGraphQLInterfaceTypes(); + createGraphQLUnionTypes(); createGraphQLObjectTypes(); createGraphQLInputObjectTypes(); @@ -161,6 +167,7 @@ private void generateGraphQLSchema() { schemaBuilder.additionalDirectives(directiveTypes); schemaBuilder.additionalTypes(new HashSet<>(enumMap.values())); schemaBuilder.additionalTypes(new HashSet<>(interfaceMap.values())); + schemaBuilder.additionalTypes(new HashSet<>(unionMap.values())); schemaBuilder.additionalTypes(new HashSet<>(typeMap.values())); schemaBuilder.additionalTypes(new HashSet<>(inputMap.values())); @@ -403,6 +410,33 @@ private void createGraphQLInterfaceType(Type interfaceType) { this.interfaceMap.put(interfaceType.getName(), graphQLInterfaceType); } + private void createGraphQLUnionTypes() { + // We can't create unions if there are no types to be a member of them + if (schema.hasUnions() && schema.hasTypes()) { + for (UnionType unionType : schema.getUnions().values()) { + createGraphQLUnionType(unionType); + } + } + } + + private void createGraphQLUnionType(UnionType unionType) { + GraphQLUnionType.Builder unionTypeBuilder = GraphQLUnionType.newUnionType() + .name(unionType.getName()) + .description(unionType.getDescription()); + + // Members + for (Type type : schema.getTypes().values()) { + if (type.isMemberOfUnion(unionType)) { + unionTypeBuilder.possibleType(GraphQLTypeReference.typeRef(type.getName())); + } + } + + GraphQLUnionType graphQLUnionType = unionTypeBuilder.build(); + // To resolve the concrete class + this.codeRegistryBuilder.typeResolver(graphQLUnionType, new UnionResolver(unionType)); + this.unionMap.put(unionType.getName(), graphQLUnionType); + } + private void createGraphQLInputObjectTypes() { if (schema.hasInputs()) { for (InputType inputType : schema.getInputs().values()) { @@ -498,8 +532,9 @@ private void createGraphQLObjectType(Type type) { GraphQLObjectType graphQLObjectType = objectTypeBuilder.build(); typeMap.put(type.getName(), graphQLObjectType); - // Register this output for interface type resolving + // Register this output for interface/union type resolving InterfaceOutputRegistry.register(type, graphQLObjectType); + UnionOutputRegistry.register(type, graphQLObjectType); } private GraphQLDirective createGraphQLDirectiveFrom(DirectiveInstance directiveInstance) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionOutputRegistry.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionOutputRegistry.java new file mode 100644 index 000000000..cd7973817 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionOutputRegistry.java @@ -0,0 +1,46 @@ +package io.smallrye.graphql.execution.resolver; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import graphql.schema.GraphQLObjectType; +import io.smallrye.graphql.schema.model.Reference; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.graphql.schema.model.UnionType; + +public class UnionOutputRegistry { + + private static final Map> unionMap = new HashMap<>(); + + private UnionOutputRegistry() { + } + + public static void register(Type type, GraphQLObjectType graphQLObjectType) { + if (type.hasUnionMemberships()) { + Set memberships = type.getUnionMemberships(); + for (Reference i : memberships) { + String union = i.getName(); + Map concreteMap = getConcreteMap(union); + concreteMap.put(type.getClassName(), graphQLObjectType); + unionMap.put(union, concreteMap); + } + } + } + + public static GraphQLObjectType getGraphQLObjectType(UnionType unionType, String concreteName) { + String union = unionType.getName(); + if (unionMap.containsKey(union)) { + return unionMap.get(union).get(concreteName); + } + return null; + } + + private static Map getConcreteMap(String union) { + if (unionMap.containsKey(union)) { + return unionMap.get(union); + } else { + return new HashMap<>(); + } + } +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionResolver.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionResolver.java new file mode 100644 index 000000000..295fbbd41 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/resolver/UnionResolver.java @@ -0,0 +1,29 @@ +package io.smallrye.graphql.execution.resolver; + +import static io.smallrye.graphql.SmallRyeGraphQLServerMessages.msg; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.TypeResolver; +import io.smallrye.graphql.schema.model.UnionType; + +public class UnionResolver implements TypeResolver { + + private final UnionType unionType; + + public UnionResolver(UnionType unionType) { + this.unionType = unionType; + } + + @Override + public GraphQLObjectType getType(TypeResolutionEnvironment tre) { + String concreteClassName = tre.getObject().getClass().getName(); + + GraphQLObjectType graphQLObjectType = UnionOutputRegistry.getGraphQLObjectType(unionType, concreteClassName); + if (graphQLObjectType != null) { + return graphQLObjectType; + } else { + throw msg.concreteClassNotFoundForInterface(concreteClassName, unionType.getName()); + } + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/ExecutionUnionsTest.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/ExecutionUnionsTest.java new file mode 100644 index 000000000..f4fb56dce --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/ExecutionUnionsTest.java @@ -0,0 +1,91 @@ +package io.smallrye.graphql.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import javax.json.JsonObject; + +import org.junit.jupiter.api.Test; + +/** + * Test against unions + * + * @author Craig Day (cday@zendesk.com) + */ +public class ExecutionUnionsTest extends ExecutionTestBase { + + @Test + public void testBasicUnion() { + JsonObject data = executeAndGetData(TEST_BASIC_UNION); + JsonObject testObject = data.getJsonObject("basicUnion"); + + assertNotNull(testObject); + + assertFalse(testObject.isNull("name"), "name should not be null"); + assertEquals("my name", testObject.getString("name")); + } + + @Test + public void testUnionOfInterfacesReturningDirectImplementer() { + JsonObject data = executeAndGetData(TEST_NESTED_INTERFACE_DIRECT_IMPL); + JsonObject testObject = data.getJsonObject("unionOfInterfacesDirectImplementor"); + + assertNotNull(testObject); + + assertFalse(testObject.isNull("message"), "message should not be null"); + assertEquals("im in many unions", testObject.getString("message")); + } + + @Test + public void testUnionOfInterfacesReturningNestedInterfaceImpl1() { + JsonObject data = executeAndGetData(TEST_NESTED_INTERFACE_1); + JsonObject testObject = data.getJsonObject("unionOfInterfacesNestedInterface1"); + + assertNotNull(testObject); + + assertFalse(testObject.isNull("name"), "name should not be null"); + assertEquals("my name", testObject.getString("name")); + } + + @Test + public void testUnionOfInterfacesReturningNestedInterfaceImpl2() { + JsonObject data = executeAndGetData(TEST_NESTED_INTERFACE_2); + JsonObject testObject = data.getJsonObject("unionOfInterfacesNestedInterface2"); + + assertNotNull(testObject); + + assertFalse(testObject.isNull("color"), "color should not be null"); + assertEquals("purple", testObject.getString("color")); + } + + private static final String TEST_BASIC_UNION = "{\n" + + " basicUnion {\n" + + " ... on UnionMember {\n" + + " name\n" + + " }\n" + + " }\n" + + "}"; + + private static final String TEST_NESTED_INTERFACE_DIRECT_IMPL = "{\n" + + " unionOfInterfacesDirectImplementor {\n" + + " ... on MemberOfManyUnions {\n" + + " message\n" + + " }\n" + + " }\n" + + "}"; + + private static final String TEST_NESTED_INTERFACE_1 = "{\n" + + " unionOfInterfacesNestedInterface1 {\n" + + " ... on ObjectWithName {\n" + + " name\n" + + " }\n" + + " }\n" + + "}"; + + private static final String TEST_NESTED_INTERFACE_2 = "{\n" + + " unionOfInterfacesNestedInterface2 {\n" + + " ... on ObjectWithColor {\n" + + " color\n" + + " }\n" + + " }\n" + + "}"; +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/MemberOfManyUnions.java b/server/implementation/src/test/java/io/smallrye/graphql/test/MemberOfManyUnions.java new file mode 100644 index 000000000..f1c19ec5b --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/MemberOfManyUnions.java @@ -0,0 +1,21 @@ +package io.smallrye.graphql.test; + +public class MemberOfManyUnions implements TestUnion, UnionOfInterfaces { + + private String message; + + public MemberOfManyUnions() { + } + + public MemberOfManyUnions(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithColor.java b/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithColor.java new file mode 100644 index 000000000..a7fede8cf --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithColor.java @@ -0,0 +1,22 @@ +package io.smallrye.graphql.test; + +public class ObjectWithColor implements UnionInterfaceTwo { + + private String color; + + public ObjectWithColor() { + } + + public ObjectWithColor(String color) { + this.color = color; + } + + @Override + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithName.java b/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithName.java new file mode 100644 index 000000000..97ce68ce4 --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/ObjectWithName.java @@ -0,0 +1,22 @@ +package io.smallrye.graphql.test; + +public class ObjectWithName implements UnionInterfaceOne { + + private String name; + + public ObjectWithName() { + } + + public ObjectWithName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/TestEndpoint.java b/server/implementation/src/test/java/io/smallrye/graphql/test/TestEndpoint.java index 2e32ac883..edc70762f 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/test/TestEndpoint.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/TestEndpoint.java @@ -16,7 +16,7 @@ /** * Basic test endpoint - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ @GraphQLApi @@ -57,6 +57,26 @@ public InterfaceWithOneGenericsParam getGeneric2() { return new ClassWithOneGenericsParam<>(22, "my name"); } + @Query + public TestUnion basicUnion() { + return new UnionMember("my name"); + } + + @Query + public UnionOfInterfaces unionOfInterfacesDirectImplementor() { + return new MemberOfManyUnions("im in many unions"); + } + + @Query + public UnionOfInterfaces unionOfInterfacesNestedInterface1() { + return new ObjectWithName("my name"); + } + + @Query + public UnionOfInterfaces unionOfInterfacesNestedInterface2() { + return new ObjectWithColor("purple"); + } + // This method will be ignored, with a WARN in the log, due to below duplicate @Name("timestamp") public TestSource getTestSource(@Source TestObject testObject, String indicator) { diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/TestUnion.java b/server/implementation/src/test/java/io/smallrye/graphql/test/TestUnion.java new file mode 100644 index 000000000..95e55acdb --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/TestUnion.java @@ -0,0 +1,8 @@ +package io.smallrye.graphql.test; + +import io.smallrye.graphql.api.Union; + +@Union +public interface TestUnion { + +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceOne.java b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceOne.java new file mode 100644 index 000000000..6da821593 --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceOne.java @@ -0,0 +1,9 @@ +package io.smallrye.graphql.test; + +import org.eclipse.microprofile.graphql.Interface; + +@Interface +public interface UnionInterfaceOne extends UnionOfInterfaces { + + String getName(); +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceTwo.java b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceTwo.java new file mode 100644 index 000000000..f1674cbce --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionInterfaceTwo.java @@ -0,0 +1,9 @@ +package io.smallrye.graphql.test; + +import org.eclipse.microprofile.graphql.Interface; + +@Interface +public interface UnionInterfaceTwo extends UnionOfInterfaces { + + String getColor(); +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/UnionMember.java b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionMember.java new file mode 100644 index 000000000..88b9a163f --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionMember.java @@ -0,0 +1,14 @@ +package io.smallrye.graphql.test; + +public class UnionMember implements TestUnion { + + String name; + + public UnionMember(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/UnionOfInterfaces.java b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionOfInterfaces.java new file mode 100644 index 000000000..180c8450c --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/UnionOfInterfaces.java @@ -0,0 +1,7 @@ +package io.smallrye.graphql.test; + +import io.smallrye.graphql.api.Union; + +@Union +public interface UnionOfInterfaces { +}