Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions graphql-apt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ private void processInterface(TypeElement typeElement) {
method,
queryAnnotation,
graphqlSchema,
methodConfig.generateDeprecated(),
registry,
generator,
validator,
Expand Down Expand Up @@ -219,6 +220,7 @@ private void processMethod(
ExecutableElement method,
GraphqlQuery queryAnnotation,
GraphQLSchema graphqlSchema,
boolean generateDeprecated,
TypeDefinitionRegistry registry,
TypeGenerator generator,
QueryValidator validator,
Expand All @@ -235,7 +237,8 @@ private void processMethod(
return;
}

if (!validator.validate(graphqlSchema, document, method) || !generateTypes) {
if (!validator.validate(graphqlSchema, document, method, generateDeprecated)
|| !generateTypes) {
return;
}

Expand Down
158 changes: 138 additions & 20 deletions graphql-apt/src/main/java/feign/graphql/apt/QueryValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<DeprecatedUsage> findDeprecatedUsages(GraphQLSchema schema, Document document) {
var usages = new ArrayList<DeprecatedUsage>();
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<DeprecatedUsage> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading