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 1e707de30..90d2337da 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 @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -76,7 +77,7 @@ private static Map getParentAnnotations(FieldInfo f private static Map getParentAnnotations(ClassInfo classInfo) { Map parentAnnotations = new HashMap<>(); - for (AnnotationInstance classAnnotation : classInfo.classAnnotations()) { + for (AnnotationInstance classAnnotation : classInfo.declaredAnnotations()) { parentAnnotations.putIfAbsent(classAnnotation.name(), classAnnotation); } @@ -95,7 +96,7 @@ private static Map getPackageAnnotations(ClassInfo if (packageName != null) { ClassInfo packageInfo = ScanningContext.getIndex().getClassByName(packageName); if (packageInfo != null) { - for (AnnotationInstance packageAnnotation : packageInfo.classAnnotations()) { + for (AnnotationInstance packageAnnotation : packageInfo.declaredAnnotations()) { packageAnnotations.putIfAbsent(packageAnnotation.name(), packageAnnotation); } } @@ -178,7 +179,7 @@ public static Annotations getAnnotationsForClass(ClassInfo classInfo) { Map annotationMap = new HashMap<>(); - for (AnnotationInstance annotationInstance : classInfo.classAnnotations()) { + for (AnnotationInstance annotationInstance : classInfo.declaredAnnotations()) { DotName name = annotationInstance.name(); annotationMap.put(name, annotationInstance); } @@ -386,6 +387,25 @@ public Optional getOneOfTheseMethodParameterAnnotationsValue(DotName... return Optional.empty(); } + /** + * Get a stream of that annotation, maybe empty if not present, maybe a stream of one, or maybe several, if it's repeatable. + */ + public Stream resolve(DotName name) { + var annotationInstance = annotationsMap.get(name); + if (annotationInstance == null) { + var repeatableType = ScanningContext.getIndex().getClassByName(name); + if (repeatableType.hasAnnotation(REPEATABLE)) { + DotName containerName = repeatableType.annotation(REPEATABLE).value().asClass().name(); + AnnotationInstance containerAnnotation = annotationsMap.get(containerName); + if (containerAnnotation != null) { + return Stream.of(containerAnnotation.value().asNestedArray()); + } + } + return Stream.of(); + } + return Stream.of(annotationInstance); + } + @Override public String toString() { return annotationsMap.toString(); @@ -554,6 +574,8 @@ private static Map getAnnotationsWithFilter(org.jbo private static final short ZERO = 0; + public static final DotName REPEATABLE = DotName.createSimple("java.lang.annotation.Repeatable"); + // SmallRye Common Annotations public static final DotName BLOCKING = DotName.createSimple("io.smallrye.common.annotation.Blocking"); public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking"); @@ -609,5 +631,4 @@ private static Map getAnnotationsWithFilter(org.jbo //Kotlin NotNull public static final DotName KOTLIN_NOT_NULL = DotName.createSimple("org.jetbrains.annotations.NotNull"); - } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java index 630766edb..f60ceb07c 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java @@ -34,7 +34,8 @@ public DirectiveType create(ClassInfo classInfo) { directiveType.setClassName(classInfo.name().toString()); directiveType.setName(toDirectiveName(classInfo, annotations)); directiveType.setDescription(DescriptionHelper.getDescriptionForType(annotations).orElse(null)); - directiveType.setLocations(getLocations(classInfo.classAnnotation(DIRECTIVE))); + directiveType.setLocations(getLocations(classInfo.declaredAnnotation(DIRECTIVE))); + directiveType.setRepeatable(classInfo.hasAnnotation(Annotations.REPEATABLE)); for (MethodInfo method : classInfo.methods()) { DirectiveArgument argument = new DirectiveArgument(); 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..7c07851b8 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 @@ -53,10 +53,7 @@ protected static Type getReturnType(MethodInfo methodInfo) { } /** - * 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(); @@ -100,8 +97,7 @@ private void doPopulateField(Direction direction, Field field, Type type, Annota // Directives if (directives != null) { // this happens while scanning for the directive types - field.addDirectiveInstances( - directives.buildDirectiveInstances(name -> annotations.getOneOfTheseAnnotations(name).orElse(null))); + field.addDirectiveInstances(directives.buildDirectiveInstances(annotations)); } } } 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 ce5a2e944..4d6e662b8 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 @@ -74,15 +74,15 @@ public Type create(ClassInfo classInfo, Reference reference) { addOperations(type, classInfo); // Directives - addDirectives(type, classInfo); + addDirectives(type, annotations); return type; } protected abstract void addFields(Type type, ClassInfo classInfo, Reference reference); - private void addDirectives(Type type, ClassInfo classInfo) { - type.setDirectiveInstances(directives.buildDirectiveInstances(classInfo::classAnnotation)); + private void addDirectives(Type type, Annotations annotations) { + type.setDirectiveInstances(directives.buildDirectiveInstances(annotations)); } private void addPolymorphicTypes(Type type, ClassInfo classInfo, Reference reference) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java index d352b1eec..fb8e854f9 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java @@ -78,7 +78,7 @@ public EnumType create(ClassInfo classInfo, Reference reference) { } private List getDirectiveInstances(Annotations annotations) { - return directives.buildDirectiveInstances(dotName -> annotations.getOneOfTheseAnnotations(dotName).orElse(null)); + return directives.buildDirectiveInstances(annotations); } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java index 8d1e5de05..6328e20fe 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java @@ -29,7 +29,7 @@ /** * This creates an input type object. - * + * * The input object has fields that might reference other types * that should still be created. * @@ -137,7 +137,7 @@ public void setDirectives(Directives directives) { } private List getDirectiveInstances(Annotations annotations) { - return directives.buildDirectiveInstances(dotName -> annotations.getOneOfTheseAnnotations(dotName).orElse(null)); + return directives.buildDirectiveInstances(annotations); } private void addFields(InputType inputType, ClassInfo classInfo, Reference reference) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java index a008e1e4d..df781c129 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java @@ -1,17 +1,18 @@ package io.smallrye.graphql.schema.helper; +import static java.util.stream.Collectors.toList; import static org.jboss.jandex.AnnotationValue.Kind.ARRAY; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.model.DirectiveInstance; import io.smallrye.graphql.schema.model.DirectiveType; @@ -37,21 +38,13 @@ public Directives(List directiveTypes) { } } - public List buildDirectiveInstances(Function getAnnotation) { - List result = null; + public List buildDirectiveInstances(Annotations annotations) { // only build directive instances from `@Directive` annotations here (that means the `directiveTypes` map), // because `directiveTypesOther` directives get their instances added on-the-go by classes that extend `ModelCreator` - for (DotName directiveTypeName : directiveTypes.keySet()) { - AnnotationInstance annotationInstance = getAnnotation.apply(directiveTypeName); - if (annotationInstance == null) { - continue; - } - if (result == null) { - result = new ArrayList<>(); - } - result.add(toDirectiveInstance(annotationInstance)); - } - return result; + return directiveTypes.keySet().stream() + .flatMap(annotations::resolve) + .map(this::toDirectiveInstance) + .collect(toList()); } private DirectiveInstance toDirectiveInstance(AnnotationInstance annotationInstance) { diff --git a/docs/directives.md b/docs/directives.md index 139e1e2a4..8902f10e4 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -1,5 +1,20 @@ # Directives +## Custom Directives + +You can add your own [GraphQL Directives](https://spec.graphql.org/draft/#sec-Language.Directives) by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +```java +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +``` + +Directives can be repeatable, see the `@Key` annotation for an example. + ## Directives generated from Bean Validation annotations If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable @@ -23,4 +38,4 @@ BV annotations are listed here): Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. -Constraints will only appear on fields of input types and operation arguments. \ No newline at end of file +Constraints will only appear on fields of input types and operation arguments. diff --git a/docs/federation.md b/docs/federation.md index e7ee83088..7fbee7f22 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -1,6 +1,6 @@ # Federation -To enable support for [GraphQL Federation](https://www.apollographql.com/docs/federation), simply set the `smallrye.graphql.federation.enabled` config key to `true`. +Support for [GraphQL Federation](https://www.apollographql.com/docs/federation) is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared to your schema and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: @@ -37,7 +37,7 @@ import org.eclipse.microprofile.graphql.Query; public class Prices { @Query public Product product(@Id String id) { - return ...; + return ... } } ``` @@ -45,7 +45,7 @@ public class Prices { The GraphQL Schema then contains: ```graphql -type Product @extends @key(fields : ["id"]) { +type Product @extends @key(fields : "id") { id: ID price: Int } @@ -58,3 +58,5 @@ type Query { product(id: ID): Product } ``` + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java index 2220567f0..49b3cdfa1 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java @@ -11,10 +11,15 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @extends on OBJECT | INTERFACE */ +/** + * directive @extends on OBJECT | INTERFACE + * + * @see federation + * spec + */ @Directive(on = { OBJECT, INTERFACE }) -@Description("Some libraries such as graphql-java don't have native support for type extensions in their printer. " + - "Apollo Federation supports using an @extends directive in place of extend type to annotate type references.") +@Description("Indicates that an object or interface definition is an extension of another definition of that same type.\n" + + "If your subgraph library supports GraphQL's built-in extend keyword, do not use this directive! Instead, use extend.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change. " + "Additionally, this annotation is currently only a directive without explicit support from the extension.") diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java index 431a906cd..0bd2d3fd8 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java @@ -1,6 +1,7 @@ package io.smallrye.graphql.api.federation; import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION; +import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; @@ -10,10 +11,17 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @external on FIELD_DEFINITION */ -@Directive(on = FIELD_DEFINITION) -@Description("The @external directive is used to mark a field as owned by another service. " + - "This allows service A to use fields from service B while also knowing at runtime the types of that field.") +/** + * directive @external on FIELD_DEFINITION | OBJECT + * + * @see federation + * spec + */ +@Directive(on = { FIELD_DEFINITION, OBJECT }) +@Description("Indicates that this subgraph usually can't resolve a particular object field, but it still needs to define " + + "that field for other purposes.\n" + + "This directive is always used in combination with another directive that references object fields, " + + "such as @provides or @requires.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface External { diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java index 6ae31a6df..4015e642a 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java @@ -4,6 +4,7 @@ import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import org.eclipse.microprofile.graphql.Description; @@ -11,14 +12,32 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; +import io.smallrye.graphql.api.federation.Key.Keys; -/** directive @key(fields: _FieldSet!) on OBJECT | INTERFACE */ +/** + * directive @key(fields: FieldSet!) repeatable on OBJECT | INTERFACE + * + * @see federation spec + */ @Directive(on = { OBJECT, INTERFACE }) -@Description("The @key directive is used to indicate a combination of fields that can be used to uniquely identify " + - "and fetch an object or interface.") +@Description("Designates an object type as an entity and specifies its key fields (a set of fields that the subgraph " + + "can use to uniquely identify any instance of the entity). You can apply multiple @key directives to " + + "a single entity (to specify multiple valid sets of key fields).") @Retention(RUNTIME) +@Repeatable(Keys.class) @Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface Key { @NonNull - String[] fields(); + @Description("A GraphQL selection set (provided as a string) of fields and subfields that contribute " + + "to the entity's primary key.\n" + + "Examples:\n" + + "\"id\"\n" + + "\"username region\"\n" + + "\"name organization { id }\"") + String fields(); + + @Retention(RUNTIME) + @interface Keys { + Key[] value(); + } } diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java index df63a6d2d..458622e13 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java @@ -11,13 +11,27 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @provides(fields: _FieldSet!) on FIELD_DEFINITION */ +/** + * directive @provides(fields: FieldSet!) on FIELD_DEFINITION + * + * @see federation + * spec + */ @Directive(on = FIELD_DEFINITION) -@Description("When resolving the annotated field, this service can provide additional, normally `@external` fields.") +@Description("Specifies a set of entity fields that a subgraph can resolve, but only at a particular schema path " + + "(at other paths, the subgraph can't resolve those fields).\n" + + "If a subgraph can always resolve a particular entity field, do not apply this directive.\n" + + "Using this directive is always an optional optimization. It can reduce the total number of subgraphs " + + "that your graph router needs to communicate with to resolve certain operations, which can improve performance.") @Retention(RUNTIME) -@Experimental("SmallRye GraphQL Federation is still subject to change. " + - "Additionally, this annotation is currently only a directive without explicit support from the extension.") +@Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface Provides { @NonNull - String[] fields(); + @Description("A GraphQL selection set (provided as a string) of object fields and subfields that the subgraph " + + "can resolve only at this query path.\n" + + "Examples:\n" + + "\"name\"\n" + + "\"name address\"\n" + + "\"... on Person { name address }\" (valid for fields that return a union or interface)") + String fields(); } diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java index 90cce5924..a4e81b0fd 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java @@ -11,14 +11,20 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @requires(fields: _FieldSet!) on FIELD_DEFINITION */ +/** + * directive @requires(fields: FieldSet!) on FIELD_DEFINITION + * + * @see federation + * spec + */ @Directive(on = FIELD_DEFINITION) -@Description("In order to resolve the annotated field, this service needs these additional `@external` fields, " + - "even when the client didn't request them.") +@Description("Indicates that the resolver for a particular entity field depends on the values of other entity fields " + + "that are resolved by other subgraphs. This tells the graph router that it needs to fetch the values " + + "of those externally defined fields first, even if the original client query didn't request them.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change. " + "Additionally, this annotation is currently only a directive without explicit support from the extension.") public @interface Requires { @NonNull - String[] fields(); + String fields(); } diff --git a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java index da91e4500..f9f6f1093 100644 --- a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java +++ b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Repeatable; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; @@ -28,6 +29,11 @@ import org.jboss.jandex.Indexer; import io.smallrye.graphql.api.Entry; +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.External; +import io.smallrye.graphql.api.federation.Key; +import io.smallrye.graphql.api.federation.Provides; +import io.smallrye.graphql.api.federation.Requires; /** * This creates an index from the classpath. @@ -73,6 +79,14 @@ private IndexView createCustomIndex() { try { indexer.index(convertClassToInputStream(Map.class)); indexer.index(convertClassToInputStream(Entry.class)); + indexer.index(convertClassToInputStream(Repeatable.class)); + + // things from the API module + indexer.index(convertClassToInputStream(Extends.class)); + indexer.index(convertClassToInputStream(External.class)); + indexer.index(convertClassToInputStream(Key.class)); + indexer.index(convertClassToInputStream(Provides.class)); + indexer.index(convertClassToInputStream(Requires.class)); } catch (IOException ex) { throw new RuntimeException(ex); } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java index 11bfd425f..1eb3dea3b 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java @@ -1,5 +1,9 @@ package io.smallrye.graphql.schema; +import static graphql.Scalars.GraphQLString; +import static graphql.introspection.Introspection.DirectiveLocation.INTERFACE; +import static graphql.introspection.Introspection.DirectiveLocation.OBJECT; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -9,8 +13,10 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Repeatable; import java.net.URISyntaxException; import java.nio.file.Files; +import java.util.EnumSet; import java.util.stream.Stream; import org.jboss.jandex.IndexView; @@ -25,9 +31,12 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLUnionType; import io.smallrye.graphql.api.Directive; import io.smallrye.graphql.api.federation.Key; +import io.smallrye.graphql.api.federation.Key.Keys; import io.smallrye.graphql.bootstrap.Bootstrap; import io.smallrye.graphql.execution.SchemaPrinter; import io.smallrye.graphql.execution.TestConfig; @@ -76,8 +85,8 @@ void testSchemaWithDirectives() throws URISyntaxException, IOException { assertOperationWithDirectives(graphQLSchema.getSubscriptionType().getField("subscriptionWithDirectives")); String actualSchema = new SchemaPrinter().print(graphQLSchema); - String expectedSchema = Files - .readString(new File(SchemaTest.class.getResource("/schemaTest.graphql").toURI()).toPath()); + var schemaUri = requireNonNull(SchemaTest.class.getResource("/schemaTest.graphql")).toURI(); + String expectedSchema = Files.readString(new File(schemaUri).toPath()); Assertions.assertEquals(expectedSchema, actualSchema); } @@ -91,12 +100,8 @@ void schemaWithEnumDirectives() { "Enum EnumWithDirectives should have directive @enumDirective"); assertNotNull(enumWithDirectives.getValue("A").getDirective("enumDirective"), "Enum value EnumWithDirectives.A should have directive @enumDirective"); - - assertSchemaEndsWith(graphQLSchema, "" + - "enum EnumWithDirectives @enumDirective {\n" + - " A @enumDirective\n" + - " B\n" + - "}\n"); + assertNull(enumWithDirectives.getValue("B").getDirective("enumDirective"), + "Enum value EnumWithDirectives.B should not have directive @enumDirective"); } @Test @@ -111,61 +116,115 @@ void schemaWithInputDirectives() { "Input type field InputWithDirectivesInput.foo should have directive @inputDirective"); assertNotNull(inputWithDirectives.getField("bar").getDirective("inputDirective"), "Input type field InputWithDirectivesInput.bar should have directive @inputDirective"); - - assertSchemaEndsWith(graphQLSchema, "" + - "input InputWithDirectivesInput @inputDirective {\n" + - " bar: Int! @inputDirective\n" + - " foo: Int! @inputDirective\n" + - "}\n"); } @Test void testSchemaWithFederationDisabled() { - GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class, - FederationTestApi.class); - - assertSchemaEndsWith(graphQLSchema, "\n" + - "\"Query root\"\n" + - "type Query {\n" + - " testTypeWithFederation(arg: String): TestTypeWithFederation\n" + - "}\n" + - "\n" + - "type TestTypeWithFederation {\n" + - " id: String\n" + - "}\n"); + config.federationEnabled = false; + // need to set it as system property because the SchemaBuilder doesn't have access to the Config object + System.setProperty("smallrye.graphql.federation.enabled", "false"); + + GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, Keys.class, + TestTypeWithFederation.class, FederationTestApi.class); + + assertNull(graphQLSchema.getDirective("key")); + assertNull(graphQLSchema.getType("_Entity")); + + GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); + assertEquals(1, queryRoot.getFields().size()); + assertNull(queryRoot.getField("_entities")); + assertNull(queryRoot.getField("_service")); + + GraphQLFieldDefinition query = queryRoot.getField("testTypeWithFederation"); + assertEquals(1, query.getArguments().size()); + assertEquals(GraphQLString, query.getArgument("arg").getType()); + assertEquals("TestTypeWithFederation", ((GraphQLObjectType) query.getType()).getName()); + + GraphQLObjectType type = graphQLSchema.getObjectType("TestTypeWithFederation"); + assertEquals(0, type.getDirectives().size()); + assertEquals(3, type.getFields().size()); + assertEquals("id", type.getFields().get(0).getName()); + assertEquals(GraphQLString, type.getFields().get(0).getType()); + assertEquals("type", type.getFields().get(1).getName()); + assertEquals(GraphQLString, type.getFields().get(1).getType()); + assertEquals("value", type.getFields().get(2).getName()); + assertEquals(GraphQLString, type.getFields().get(2).getType()); + + assertNull(graphQLSchema.getObjectType("_Service")); } @Test - void testSchemaWithFederation() { + void testSchemaWithFederationEnabled() { config.federationEnabled = true; // need to set it as system property because the SchemaBuilder doesn't have access to the Config object System.setProperty("smallrye.graphql.federation.enabled", "true"); try { - GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class, - FederationTestApi.class); - - assertSchemaEndsWith(graphQLSchema, "\n" + - "union _Entity = TestTypeWithFederation\n" + - "\n" + - "\"Query root\"\n" + - "type Query {\n" + - " _entities(representations: [_Any!]!): [_Entity]!\n" + - " _service: _Service!\n" + - " testTypeWithFederation(arg: String): TestTypeWithFederation\n" + - "}\n" + - "\n" + - "type TestTypeWithFederation @key(fields : [\"id\"]) {\n" + - " id: String\n" + - "}\n" + - "\n" + - "type _Service {\n" + - " sdl: String!\n" + - "}\n"); + GraphQLSchema graphQLSchema = createGraphQLSchema(Repeatable.class, Directive.class, Key.class, Keys.class, + TestTypeWithFederation.class, FederationTestApi.class); + + GraphQLDirective keyDirective = graphQLSchema.getDirective("key"); + assertEquals("key", keyDirective.getName()); + assertTrue(keyDirective.isRepeatable()); + assertEquals( + "Designates an object type as an entity and specifies its key fields " + + "(a set of fields that the subgraph can use to uniquely identify any instance " + + "of the entity). You can apply multiple @key directives to a single entity " + + "(to specify multiple valid sets of key fields).", + keyDirective.getDescription()); + assertEquals(EnumSet.of(OBJECT, INTERFACE), keyDirective.validLocations()); + assertEquals(1, keyDirective.getArguments().size()); + assertEquals("String", ((GraphQLScalarType) keyDirective.getArgument("fields").getType()).getName()); + + GraphQLUnionType entityType = (GraphQLUnionType) graphQLSchema.getType("_Entity"); + assertNotNull(entityType); + assertEquals(1, entityType.getTypes().size()); + assertEquals(TestTypeWithFederation.class.getSimpleName(), entityType.getTypes().get(0).getName()); + + GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); + assertEquals(3, queryRoot.getFields().size()); + + GraphQLFieldDefinition entities = queryRoot.getField("_entities"); + assertEquals(1, entities.getArguments().size()); + assertEquals("[_Any!]!", entities.getArgument("representations").getType().toString()); + assertEquals("[_Entity]!", entities.getType().toString()); + + GraphQLFieldDefinition service = queryRoot.getField("_service"); + assertEquals(0, service.getArguments().size()); + assertEquals("_Service!", service.getType().toString()); + + GraphQLFieldDefinition query = queryRoot.getField("testTypeWithFederation"); + assertEquals(1, query.getArguments().size()); + assertEquals(GraphQLString, query.getArgument("arg").getType()); + assertEquals("TestTypeWithFederation", ((GraphQLObjectType) query.getType()).getName()); + + GraphQLObjectType type = graphQLSchema.getObjectType("TestTypeWithFederation"); + assertEquals(2, type.getDirectives().size()); + assertKeyDirective(type.getDirectives().get(0), "id"); + assertKeyDirective(type.getDirectives().get(1), "type id"); + assertEquals(3, type.getFields().size()); + assertEquals("id", type.getFields().get(0).getName()); + assertEquals(GraphQLString, type.getFields().get(0).getType()); + assertEquals("type", type.getFields().get(1).getName()); + assertEquals(GraphQLString, type.getFields().get(1).getType()); + assertEquals("value", type.getFields().get(2).getName()); + assertEquals(GraphQLString, type.getFields().get(2).getType()); + + GraphQLObjectType serviceType = graphQLSchema.getObjectType("_Service"); + assertEquals(1, serviceType.getFields().size()); + assertEquals("sdl", serviceType.getFields().get(0).getName()); + assertEquals("String!", serviceType.getFields().get(0).getType().toString()); } finally { System.clearProperty("smallrye.graphql.federation.enabled"); } } + private static void assertKeyDirective(GraphQLDirective graphQLDirective, String value) { + assertEquals("key", graphQLDirective.getName()); + assertEquals(1, graphQLDirective.getArguments().size()); + assertEquals("fields", graphQLDirective.getArguments().get(0).getName()); + assertEquals(value, graphQLDirective.getArguments().get(0).toAppliedArgument().getArgumentValue().getValue()); + } + private GraphQLSchema createGraphQLSchema(Class... api) { Schema schema = SchemaBuilder.build(scan(api)); assertNotNull(schema, "Schema should not be null"); @@ -174,16 +233,6 @@ private GraphQLSchema createGraphQLSchema(Class... api) { return graphQLSchema; } - private static void assertSchemaEndsWith(GraphQLSchema schema, String end) { - String schemaString = new SchemaPrinter().print(schema); - assertSchemaEndsWith(schemaString, end); - } - - private static void assertSchemaEndsWith(String schema, String end) { - // assertEquals(schema, end); // this is convenient for debugging, as the IDE can show the diff - assertTrue(schema.endsWith(end), () -> "<<<\n" + schema + "\n>>> does not end with <<<\n" + end + "\n>>>"); - } - private void assertOperationWithDirectives(GraphQLFieldDefinition operation) { String name = operation.getName(); GraphQLDirective operationDirective = operation.getDirective("operationDirective"); diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java index a9d85d831..b7bad7272 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java @@ -3,8 +3,19 @@ import io.smallrye.graphql.api.federation.Key; @Key(fields = "id") +@Key(fields = "type id") public class TestTypeWithFederation { + private String type; private String id; + private String value; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } public String getId() { return id; @@ -13,4 +24,12 @@ public String getId() { public void setId(String id) { this.id = id; } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } }