From 2d9fc83e32aef1b2ae9ec7e81a85eb2e2c439b3d Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 7 Oct 2026 17:50:02 -0300 Subject: [PATCH 1/2] Hide deprecated schema elements from query validation when generateDeprecated is false Signed-off-by: Marvin Froeder --- .../graphql/apt/GraphqlSchemaProcessor.java | 42 ++++++++- .../apt/GraphqlSchemaProcessorTest.java | 92 ++++++++++++++++++- .../resources/deprecated-test-schema.graphql | 3 +- 3 files changed, 132 insertions(+), 5 deletions(-) diff --git a/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java b/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java index c5c51ca96..a1a19e71f 100644 --- a/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java +++ b/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java @@ -29,10 +29,19 @@ import graphql.language.SelectionSet; import graphql.language.VariableDefinition; import graphql.parser.Parser; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.SchemaTransformer; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import graphql.schema.idl.UnExecutableSchemaGenerator; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -115,6 +124,7 @@ private void processInterface(TypeElement typeElement) { } var graphqlSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry); + var nonDeprecatedSchema = withoutDeprecated(graphqlSchema); var generateTypes = schemaAnnotation.generateTypes(); @@ -141,7 +151,7 @@ private void processInterface(TypeElement typeElement) { processMethod( method, queryAnnotation, - graphqlSchema, + methodConfig.generateDeprecated() ? graphqlSchema : nonDeprecatedSchema, registry, generator, validator, @@ -150,6 +160,36 @@ private void processInterface(TypeElement typeElement) { } } + private static GraphQLSchema withoutDeprecated(GraphQLSchema schema) { + return SchemaTransformer.transformSchema( + schema, + new GraphQLTypeVisitorStub() { + @Override + public TraversalControl visitGraphQLFieldDefinition( + GraphQLFieldDefinition node, TraverserContext context) { + return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitGraphQLArgument( + GraphQLArgument node, TraverserContext context) { + return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitGraphQLInputObjectField( + GraphQLInputObjectField node, TraverserContext context) { + return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitGraphQLEnumValueDefinition( + GraphQLEnumValueDefinition node, TraverserContext context) { + return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; + } + }); + } + private Map collectScalarMappings(TypeElement typeElement) { var scalars = new HashMap(); collectScalarsFromType(typeElement, scalars); diff --git a/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java b/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java index 12d885af1..9dbfeed08 100644 --- a/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java +++ b/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java @@ -19,7 +19,11 @@ import static com.google.testing.compile.Compiler.javac; import com.google.testing.compile.JavaFileObjects; +import java.util.stream.Stream; +import javax.tools.JavaFileObject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class GraphqlSchemaProcessorTest { @@ -2067,7 +2071,7 @@ void deprecatedFieldsSkippedWhenDisabledAtClassLevel() { @GraphqlSchema(value = "deprecated-test-schema.graphql", generateDeprecated = false) interface ClassDisabledDeprecatedApi { @GraphqlQuery(\""" - { user(id: "1") { id name email emails status } } + { user(id: "1") { id name emails status } } \""") UserResult getUser(); @@ -2116,7 +2120,7 @@ interface MethodOverrideDeprecatedApi { WithDeprecatedResult getUserWithDeprecated(); @GraphqlQuery(\""" - { user(id: "1") { id name email emails status } } + { user(id: "1") { id name emails status } } \""") WithoutDeprecatedResult getUserFiltered(); } @@ -2244,7 +2248,7 @@ void methodLevelToggleCanDisableDeprecated() { @GraphqlSchema("deprecated-test-schema.graphql") interface MethodDisableDeprecatedApi { @GraphqlQuery(value = \""" - { user(id: "1") { id name email emails status } } + { user(id: "1") { id name emails status } } \""", generateDeprecated = Toggle.FALSE) FilteredUserResult getUserFiltered(); } @@ -2262,4 +2266,86 @@ interface MethodDisableDeprecatedApi { .contentsAsUtf8String() .doesNotContain("BANNED"); } + + static Stream deprecatedUsages() { + return Stream.of( + """ + { user(id: "1") { id email } }""", + """ + { user(id: "1", legacyId: "2") { id } }""", + """ + { users(status: BANNED) { id } }""", + """ + mutation { createUser(input: { name: "a", email: "b" }) { id } }"""); + } + + private static JavaFileObject deprecatedUsageSource(String query, boolean generateDeprecated) { + return JavaFileObjects.forSourceString( + "test.DeprecatedUsageApi", + """ + package test; + + import feign.graphql.GraphqlSchema; + import feign.graphql.GraphqlQuery; + + @GraphqlSchema(value = "deprecated-test-schema.graphql", generateDeprecated = %s) + interface DeprecatedUsageApi { + @GraphqlQuery(\""" + %s + \""") + DeprecatedUsageResult call(); + } + """ + .formatted(generateDeprecated, query)); + } + + @ParameterizedTest + @MethodSource("deprecatedUsages") + void deprecatedSchemaElementsAcceptedInQueryWhenEnabled(String query) { + var compilation = + javac() + .withProcessors(new GraphqlSchemaProcessor()) + .compile(deprecatedUsageSource(query, true)); + + assertThat(compilation).succeeded(); + } + + @ParameterizedTest + @MethodSource("deprecatedUsages") + void deprecatedSchemaElementsRejectedInQueryWhenDisabled(String query) { + var compilation = + javac() + .withProcessors(new GraphqlSchemaProcessor()) + .compile(deprecatedUsageSource(query, false)); + + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("GraphQL validation error"); + } + + @Test + void methodToggleFalseRejectsDeprecatedFieldInQuery() { + var source = + JavaFileObjects.forSourceString( + "test.MethodHiddenDeprecatedApi", + """ + package test; + + import feign.graphql.GraphqlSchema; + import feign.graphql.GraphqlQuery; + import feign.graphql.Toggle; + + @GraphqlSchema("deprecated-test-schema.graphql") + interface MethodHiddenDeprecatedApi { + @GraphqlQuery(value = \""" + { user(id: "1") { id email } } + \""", generateDeprecated = Toggle.FALSE) + HiddenResult getUser(); + } + """); + + var compilation = javac().withProcessors(new GraphqlSchemaProcessor()).compile(source); + + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("Field 'email' in type 'User' is undefined"); + } } diff --git a/graphql-apt/src/test/resources/deprecated-test-schema.graphql b/graphql-apt/src/test/resources/deprecated-test-schema.graphql index cdb4ae517..392c64c83 100644 --- a/graphql-apt/src/test/resources/deprecated-test-schema.graphql +++ b/graphql-apt/src/test/resources/deprecated-test-schema.graphql @@ -1,5 +1,6 @@ type Query { - user(id: ID!): User + user(id: ID!, legacyId: ID @deprecated(reason: "use id instead")): User + users(status: UserStatus): [User] } type Mutation { From e12afc00a0bfb079e551902a1a85b08a03c66010 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 7 Oct 2026 18:05:17 -0300 Subject: [PATCH 2/2] Report deprecated schema elements used in queries instead of stripping them from the schema Signed-off-by: Marvin Froeder --- graphql-apt/README.md | 8 + .../graphql/apt/GraphqlSchemaProcessor.java | 47 +----- .../feign/graphql/apt/QueryValidator.java | 158 +++++++++++++++--- .../java/feign/graphql/apt/TypeGenerator.java | 3 - .../apt/GraphqlSchemaProcessorTest.java | 129 ++++++++------ .../resources/deprecated-edge-schema.graphql | 29 ++++ 6 files changed, 262 insertions(+), 112 deletions(-) create mode 100644 graphql-apt/src/test/resources/deprecated-edge-schema.graphql diff --git a/graphql-apt/README.md b/graphql-apt/README.md index dc0b27cb4..623a7dda3 100644 --- a/graphql-apt/README.md +++ b/graphql-apt/README.md @@ -108,6 +108,14 @@ public enum Episode { } ``` +### Deprecated schema elements + +Set `generateDeprecated = false` on `@GraphqlSchema`, or `generateDeprecated = Toggle.FALSE` on a single `@GraphqlQuery`, to hide `@deprecated` schema elements. Generated types leave out deprecated fields, input fields and enum values. A query that uses a deprecated field, argument, input field or enum value fails to compile: + +``` +GraphQL validation error at line 1, column 22: Field 'email' is deprecated (use emails instead) and generateDeprecated is false +``` + ## Maven Configuration Add as a `provided` dependency so it runs during compilation but is not included at runtime: diff --git a/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java b/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java index a1a19e71f..a2cdbcd57 100644 --- a/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java +++ b/graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java @@ -29,19 +29,10 @@ import graphql.language.SelectionSet; import graphql.language.VariableDefinition; import graphql.parser.Parser; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLEnumValueDefinition; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.SchemaTransformer; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import graphql.schema.idl.UnExecutableSchemaGenerator; -import graphql.util.TraversalControl; -import graphql.util.TraverserContext; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -124,7 +115,6 @@ private void processInterface(TypeElement typeElement) { } var graphqlSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry); - var nonDeprecatedSchema = withoutDeprecated(graphqlSchema); var generateTypes = schemaAnnotation.generateTypes(); @@ -151,7 +141,8 @@ private void processInterface(TypeElement typeElement) { processMethod( method, queryAnnotation, - methodConfig.generateDeprecated() ? graphqlSchema : nonDeprecatedSchema, + graphqlSchema, + methodConfig.generateDeprecated(), registry, generator, validator, @@ -160,36 +151,6 @@ private void processInterface(TypeElement typeElement) { } } - private static GraphQLSchema withoutDeprecated(GraphQLSchema schema) { - return SchemaTransformer.transformSchema( - schema, - new GraphQLTypeVisitorStub() { - @Override - public TraversalControl visitGraphQLFieldDefinition( - GraphQLFieldDefinition node, TraverserContext context) { - return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; - } - - @Override - public TraversalControl visitGraphQLArgument( - GraphQLArgument node, TraverserContext context) { - return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; - } - - @Override - public TraversalControl visitGraphQLInputObjectField( - GraphQLInputObjectField node, TraverserContext context) { - return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; - } - - @Override - public TraversalControl visitGraphQLEnumValueDefinition( - GraphQLEnumValueDefinition node, TraverserContext context) { - return node.isDeprecated() ? deleteNode(context) : TraversalControl.CONTINUE; - } - }); - } - private Map collectScalarMappings(TypeElement typeElement) { var scalars = new HashMap(); collectScalarsFromType(typeElement, scalars); @@ -259,6 +220,7 @@ private void processMethod( ExecutableElement method, GraphqlQuery queryAnnotation, GraphQLSchema graphqlSchema, + boolean generateDeprecated, TypeDefinitionRegistry registry, TypeGenerator generator, QueryValidator validator, @@ -275,7 +237,8 @@ private void processMethod( return; } - if (!validator.validate(graphqlSchema, document, method) || !generateTypes) { + if (!validator.validate(graphqlSchema, document, method, generateDeprecated) + || !generateTypes) { return; } diff --git a/graphql-apt/src/main/java/feign/graphql/apt/QueryValidator.java b/graphql-apt/src/main/java/feign/graphql/apt/QueryValidator.java index 4fefe5eab..058bf5583 100644 --- a/graphql-apt/src/main/java/feign/graphql/apt/QueryValidator.java +++ b/graphql-apt/src/main/java/feign/graphql/apt/QueryValidator.java @@ -17,15 +17,34 @@ import feign.Param; import graphql.GraphQLError; +import graphql.analysis.QueryTraversalOptions; +import graphql.analysis.QueryTraverser; +import graphql.analysis.QueryVisitorFieldArgumentEnvironment; +import graphql.analysis.QueryVisitorFieldEnvironment; +import graphql.analysis.QueryVisitorStub; +import graphql.execution.CoercedVariables; +import graphql.language.ArrayValue; import graphql.language.Document; +import graphql.language.EnumValue; import graphql.language.ListType; +import graphql.language.Node; import graphql.language.NonNullType; +import graphql.language.ObjectValue; import graphql.language.OperationDefinition; +import graphql.language.SourceLocation; import graphql.language.Type; +import graphql.language.Value; import graphql.language.VariableDefinition; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLTypeUtil; +import graphql.util.TraversalControl; import graphql.validation.Validator; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Locale; import javax.annotation.processing.Messager; import javax.lang.model.element.Element; @@ -40,31 +59,130 @@ public QueryValidator(Messager messager) { this.messager = messager; } - public boolean validate(GraphQLSchema schema, Document document, Element methodElement) { - var validator = new Validator(); - var errors = validator.validateDocument(schema, document, Locale.ENGLISH); + public boolean validate( + GraphQLSchema schema, Document document, Element methodElement, boolean generateDeprecated) { + var errors = new Validator().validateDocument(schema, document, Locale.ENGLISH); + for (GraphQLError error : errors) { + var locations = error.getLocations(); + reportError( + error.getMessage(), + locations == null || locations.isEmpty() ? null : locations.get(0), + methodElement); + } + if (!errors.isEmpty() || generateDeprecated) { + return errors.isEmpty(); + } - if (errors.isEmpty()) { - return true; + var deprecatedUsages = findDeprecatedUsages(schema, document); + for (var usage : deprecatedUsages) { + reportError( + usage.message() + " and generateDeprecated is false", usage.location(), methodElement); } + return deprecatedUsages.isEmpty(); + } - for (GraphQLError error : errors) { - var locations = error.getLocations(); - if (locations != null && !locations.isEmpty()) { - var loc = locations.get(0); - messager.printMessage( - Diagnostic.Kind.ERROR, - "GraphQL validation error at line %d, column %d: %s" - .formatted(loc.getLine(), loc.getColumn(), error.getMessage()), - methodElement); - } else { - messager.printMessage( - Diagnostic.Kind.ERROR, - "GraphQL validation error: " + error.getMessage(), - methodElement); + private void reportError(String message, SourceLocation location, Element methodElement) { + if (location == null) { + messager.printMessage( + Diagnostic.Kind.ERROR, "GraphQL validation error: " + message, methodElement); + return; + } + messager.printMessage( + Diagnostic.Kind.ERROR, + "GraphQL validation error at line %d, column %d: %s" + .formatted(location.getLine(), location.getColumn(), message), + methodElement); + } + + private List findDeprecatedUsages(GraphQLSchema schema, Document document) { + var usages = new ArrayList(); + QueryTraverser.newQueryTraverser() + .schema(schema) + .document(document) + .coercedVariables(CoercedVariables.emptyVariables()) + .options(QueryTraversalOptions.defaultOptions().coerceFieldArguments(false)) + .build() + .visitPreOrder( + new QueryVisitorStub() { + @Override + public void visitField(QueryVisitorFieldEnvironment env) { + var definition = env.getFieldDefinition(); + if (!env.isTypeNameIntrospectionField() && definition.isDeprecated()) { + usages.add( + DeprecatedUsage.of( + "Field", + definition.getName(), + definition.getDeprecationReason(), + env.getField())); + } + } + + @Override + public TraversalControl visitArgument(QueryVisitorFieldArgumentEnvironment env) { + var argument = env.getGraphQLArgument(); + if (argument.isDeprecated()) { + usages.add( + DeprecatedUsage.of( + "Argument", + argument.getName(), + argument.getDeprecationReason(), + env.getArgument())); + } + collectDeprecatedValues(env.getArgument().getValue(), argument.getType(), usages); + return TraversalControl.CONTINUE; + } + }); + + for (var definition : document.getDefinitionsOfType(OperationDefinition.class)) { + for (var variable : definition.getVariableDefinitions()) { + if (variable.getDefaultValue() != null + && schema.getType(GraphqlTypeMapper.unwrapTypeName(variable.getType())) + instanceof GraphQLInputType variableType) { + collectDeprecatedValues(variable.getDefaultValue(), variableType, usages); + } } } - return false; + return usages; + } + + private static void collectDeprecatedValues( + Value value, GraphQLInputType type, List usages) { + var unwrapped = GraphQLTypeUtil.unwrapAll(type); + if (value instanceof ArrayValue array) { + for (var element : array.getValues()) { + collectDeprecatedValues(element, (GraphQLInputType) unwrapped, usages); + } + } else if (value instanceof EnumValue enumValue + && unwrapped instanceof GraphQLEnumType enumType) { + var definition = enumType.getValue(enumValue.getName()); + if (definition != null && definition.isDeprecated()) { + usages.add( + DeprecatedUsage.of( + "Enum value", definition.getName(), definition.getDeprecationReason(), enumValue)); + } + } else if (value instanceof ObjectValue object + && unwrapped instanceof GraphQLInputObjectType inputType) { + for (var field : object.getObjectFields()) { + var definition = inputType.getField(field.getName()); + if (definition == null) { + continue; + } + if (definition.isDeprecated()) { + usages.add( + DeprecatedUsage.of( + "Input field", definition.getName(), definition.getDeprecationReason(), field)); + } + collectDeprecatedValues(field.getValue(), definition.getType(), usages); + } + } + } + + private record DeprecatedUsage(String message, SourceLocation location) { + + static DeprecatedUsage of(String kind, String name, String reason, Node node) { + return new DeprecatedUsage( + "%s '%s' is deprecated (%s)".formatted(kind, name, reason), node.getSourceLocation()); + } } public void validateVariableBindings(OperationDefinition operation, ExecutableElement method) { diff --git a/graphql-apt/src/main/java/feign/graphql/apt/TypeGenerator.java b/graphql-apt/src/main/java/feign/graphql/apt/TypeGenerator.java index dbce74a80..2bff1fdc9 100644 --- a/graphql-apt/src/main/java/feign/graphql/apt/TypeGenerator.java +++ b/graphql-apt/src/main/java/feign/graphql/apt/TypeGenerator.java @@ -146,9 +146,6 @@ private ResultTypeDefinition buildResultType( continue; } var deprecated = isDeprecated(schemaDef); - if (!annotationConfig.generateDeprecated() && deprecated) { - continue; - } var fieldName = responseKey(field); var fieldType = schemaDef.getType(); diff --git a/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java b/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java index 9dbfeed08..2bb784ce3 100644 --- a/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java +++ b/graphql-apt/src/test/java/feign/graphql/apt/GraphqlSchemaProcessorTest.java @@ -23,7 +23,9 @@ import javax.tools.JavaFileObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; class GraphqlSchemaProcessorTest { @@ -2086,10 +2088,6 @@ mutation createUser($input: CreateUserInput!) { var compilation = javac().withProcessors(new GraphqlSchemaProcessor()).compile(source); assertThat(compilation).succeeded(); - assertThat(compilation) - .generatedSourceFile("test.UserResult") - .contentsAsUtf8String() - .doesNotContain(" email,"); assertThat(compilation) .generatedSourceFile("test.CreateUserInput") .contentsAsUtf8String() @@ -2133,10 +2131,6 @@ interface MethodOverrideDeprecatedApi { .generatedSourceFile("test.WithDeprecatedResult") .contentsAsUtf8String() .contains(" email,"); - assertThat(compilation) - .generatedSourceFile("test.WithoutDeprecatedResult") - .contentsAsUtf8String() - .doesNotContain(" email,"); } @Test @@ -2234,7 +2228,7 @@ interface EnumDefaultApi { } @Test - void methodLevelToggleCanDisableDeprecated() { + void methodLevelToggleFalseFiltersDeprecatedEnumValues() { var source = JavaFileObjects.forSourceString( "test.MethodDisableDeprecatedApi", @@ -2257,29 +2251,62 @@ interface MethodDisableDeprecatedApi { var compilation = javac().withProcessors(new GraphqlSchemaProcessor()).compile(source); assertThat(compilation).succeeded(); - assertThat(compilation) - .generatedSourceFile("test.FilteredUserResult") - .contentsAsUtf8String() - .doesNotContain(" email,"); assertThat(compilation) .generatedSourceFile("test.UserStatus") .contentsAsUtf8String() .doesNotContain("BANNED"); } - static Stream deprecatedUsages() { + static Stream queriesUsingDeprecatedSchemaElements() { return Stream.of( - """ - { user(id: "1") { id email } }""", - """ - { user(id: "1", legacyId: "2") { id } }""", - """ - { users(status: BANNED) { id } }""", - """ - mutation { createUser(input: { name: "a", email: "b" }) { id } }"""); + Arguments.of( + """ + { user(id: "1") { id email } }""", + "Field 'email' is deprecated (use emails instead) and generateDeprecated is false"), + Arguments.of( + """ + query user($id: ID!) { user(id: $id) { id email } }""", + "Field 'email' is deprecated (use emails instead)"), + Arguments.of( + """ + { user(id: "1", legacyId: "2") { id } }""", + "Argument 'legacyId' is deprecated (use id instead)"), + Arguments.of( + """ + { users(status: BANNED) { id } }""", + "Enum value 'BANNED' is deprecated (no longer used)"), + Arguments.of( + """ + query users($status: UserStatus = BANNED) { users(status: $status) { id } }""", + "Enum value 'BANNED' is deprecated (no longer used)"), + Arguments.of( + """ + mutation { createUser(input: { name: "a", email: "b" }) { id } }""", + "Input field 'email' is deprecated (use emails instead)")); + } + + static Stream deprecatedUsagesWhenEnabled() { + return withToggleSettings( + new String[] {"true", "Toggle.INHERIT"}, new String[] {"false", "Toggle.TRUE"}); + } + + static Stream deprecatedUsagesWhenDisabled() { + return withToggleSettings( + new String[] {"false", "Toggle.INHERIT"}, new String[] {"true", "Toggle.FALSE"}); } - private static JavaFileObject deprecatedUsageSource(String query, boolean generateDeprecated) { + private static Stream withToggleSettings(String[]... settings) { + return queriesUsingDeprecatedSchemaElements() + .flatMap( + usage -> + Stream.of(settings) + .map( + setting -> + Arguments.of(usage.get()[0], setting[0], setting[1], usage.get()[1]))); + } + + private static JavaFileObject createDeprecatedSchemaApiSource( + String query, String schemaGenerateDeprecated, String queryGenerateDeprecated) { return JavaFileObjects.forSourceString( "test.DeprecatedUsageApi", """ @@ -2287,65 +2314,73 @@ private static JavaFileObject deprecatedUsageSource(String query, boolean genera import feign.graphql.GraphqlSchema; import feign.graphql.GraphqlQuery; + import feign.graphql.Toggle; @GraphqlSchema(value = "deprecated-test-schema.graphql", generateDeprecated = %s) interface DeprecatedUsageApi { - @GraphqlQuery(\""" + @GraphqlQuery(value = \""" %s - \""") - DeprecatedUsageResult call(); + \""", generateDeprecated = %s) + DeprecatedUsageResult sendOperation(String id); } """ - .formatted(generateDeprecated, query)); + .formatted(schemaGenerateDeprecated, query, queryGenerateDeprecated)); } @ParameterizedTest - @MethodSource("deprecatedUsages") - void deprecatedSchemaElementsAcceptedInQueryWhenEnabled(String query) { + @MethodSource("deprecatedUsagesWhenEnabled") + void deprecatedSchemaElementsAcceptedInQueryWhenEnabled( + String query, String schemaGenerateDeprecated, String queryGenerateDeprecated) { var compilation = javac() .withProcessors(new GraphqlSchemaProcessor()) - .compile(deprecatedUsageSource(query, true)); + .compile( + createDeprecatedSchemaApiSource( + query, schemaGenerateDeprecated, queryGenerateDeprecated)); assertThat(compilation).succeeded(); } @ParameterizedTest - @MethodSource("deprecatedUsages") - void deprecatedSchemaElementsRejectedInQueryWhenDisabled(String query) { + @MethodSource("deprecatedUsagesWhenDisabled") + void deprecatedSchemaElementsRejectedInQueryWhenDisabled( + String query, + String schemaGenerateDeprecated, + String queryGenerateDeprecated, + String expectedError) { var compilation = javac() .withProcessors(new GraphqlSchemaProcessor()) - .compile(deprecatedUsageSource(query, false)); + .compile( + createDeprecatedSchemaApiSource( + query, schemaGenerateDeprecated, queryGenerateDeprecated)); assertThat(compilation).failed(); - assertThat(compilation).hadErrorContaining("GraphQL validation error"); + assertThat(compilation).hadErrorContaining(expectedError); } - @Test - void methodToggleFalseRejectsDeprecatedFieldInQuery() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void schemaWithDeprecatedElementsBehindDefaultsAndInterfacesCompiles(boolean generateDeprecated) { var source = JavaFileObjects.forSourceString( - "test.MethodHiddenDeprecatedApi", + "test.EdgeSchemaApi", """ package test; import feign.graphql.GraphqlSchema; import feign.graphql.GraphqlQuery; - import feign.graphql.Toggle; - @GraphqlSchema("deprecated-test-schema.graphql") - interface MethodHiddenDeprecatedApi { - @GraphqlQuery(value = \""" - { user(id: "1") { id email } } - \""", generateDeprecated = Toggle.FALSE) - HiddenResult getUser(); + @GraphqlSchema(value = "deprecated-edge-schema.graphql", generateDeprecated = %s) + interface EdgeSchemaApi { + @GraphqlQuery("{ ping }") + String ping(); } - """); + """ + .formatted(generateDeprecated)); var compilation = javac().withProcessors(new GraphqlSchemaProcessor()).compile(source); - assertThat(compilation).failed(); - assertThat(compilation).hadErrorContaining("Field 'email' in type 'User' is undefined"); + assertThat(compilation).succeeded(); } } diff --git a/graphql-apt/src/test/resources/deprecated-edge-schema.graphql b/graphql-apt/src/test/resources/deprecated-edge-schema.graphql new file mode 100644 index 000000000..281ac2c10 --- /dev/null +++ b/graphql-apt/src/test/resources/deprecated-edge-schema.graphql @@ -0,0 +1,29 @@ +interface Node { + id: ID +} + +type Legacy implements Node { + id: ID @deprecated(reason: "no longer exposed") +} + +type Archived { + old: String @deprecated(reason: "archived") +} + +enum Level { + LOW + HIGH @deprecated(reason: "use LOW") +} + +input Filter { + name: String + legacy: String @deprecated(reason: "use name") +} + +type Query { + ping: String + levels(level: Level = HIGH): [String] + filtered(filter: Filter = { legacy: "x" }): [String] + legacy: Legacy + archived: Archived +}