Skip to content

Commit 51569ac

Browse files
authored
Merge pull request #3269 from OpenFeign/graphql-apt-optional-and-type-annotations
Add Optional wrapping, type/field annotations, and type overrides for GraphQL APT
2 parents 87bf53d + 0e5d0a7 commit 51569ac

11 files changed

Lines changed: 1613 additions & 156 deletions

File tree

graphql-apt/src/main/java/feign/graphql/apt/GraphqlSchemaProcessor.java

Lines changed: 154 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@
1717

1818
import com.google.auto.service.AutoService;
1919
import com.squareup.javapoet.TypeName;
20+
import feign.graphql.GraphqlField;
2021
import feign.graphql.GraphqlQuery;
2122
import feign.graphql.GraphqlSchema;
2223
import feign.graphql.Scalar;
24+
import feign.graphql.Toggle;
2325
import graphql.language.Document;
2426
import graphql.language.Field;
25-
import graphql.language.FieldDefinition;
26-
import graphql.language.ListType;
27-
import graphql.language.NonNullType;
2827
import graphql.language.ObjectTypeDefinition;
2928
import graphql.language.OperationDefinition;
3029
import graphql.language.SelectionSet;
31-
import graphql.language.Type;
3230
import graphql.language.VariableDefinition;
3331
import graphql.parser.Parser;
3432
import graphql.schema.GraphQLSchema;
@@ -39,6 +37,8 @@
3937
import java.util.List;
4038
import java.util.Map;
4139
import java.util.Set;
40+
import java.util.TreeSet;
41+
import java.util.function.Supplier;
4242
import javax.annotation.processing.AbstractProcessor;
4343
import javax.annotation.processing.Filer;
4444
import javax.annotation.processing.Messager;
@@ -48,10 +48,13 @@
4848
import javax.annotation.processing.SupportedAnnotationTypes;
4949
import javax.annotation.processing.SupportedSourceVersion;
5050
import javax.lang.model.SourceVersion;
51+
import javax.lang.model.element.Element;
5152
import javax.lang.model.element.ExecutableElement;
5253
import javax.lang.model.element.PackageElement;
5354
import javax.lang.model.element.TypeElement;
5455
import javax.lang.model.type.DeclaredType;
56+
import javax.lang.model.type.MirroredTypeException;
57+
import javax.lang.model.type.MirroredTypesException;
5558
import javax.lang.model.type.TypeKind;
5659
import javax.lang.model.type.TypeMirror;
5760
import javax.tools.Diagnostic;
@@ -117,6 +120,9 @@ private void processInterface(TypeElement typeElement) {
117120
var validator = new QueryValidator(messager);
118121
var generator = new TypeGenerator(filer, messager, registry, typeMapper, targetPackage);
119122

123+
var classFieldAnnotations = extractFieldAnnotations(typeElement);
124+
var classConfig = resolveClassConfig(schemaAnnotation, classFieldAnnotations);
125+
120126
for (var enclosed : typeElement.getEnclosedElements()) {
121127
if (!(enclosed instanceof ExecutableElement method)) {
122128
continue;
@@ -126,6 +132,9 @@ private void processInterface(TypeElement typeElement) {
126132
continue;
127133
}
128134

135+
var methodConfig = resolveMethodConfig(method, queryAnnotation, classConfig);
136+
generator.setAnnotationConfig(methodConfig);
137+
129138
processMethod(
130139
method,
131140
queryAnnotation,
@@ -242,9 +251,9 @@ private void processMethod(
242251
if (rootType != null) {
243252
var rootField = findRootField(operation.getSelectionSet());
244253
if (rootField != null && rootField.getSelectionSet() != null) {
245-
var rootFieldDef = findFieldDefinition(rootType, rootField.getName());
254+
var rootFieldDef = GraphqlTypeMapper.findFieldDefinition(rootType, rootField.getName());
246255
if (rootFieldDef != null) {
247-
var fieldTypeName = unwrapTypeName(rootFieldDef.getType());
256+
var fieldTypeName = GraphqlTypeMapper.unwrapTypeName(rootFieldDef.getType());
248257
var fieldObjectType =
249258
registry.getType(fieldTypeName, ObjectTypeDefinition.class).orElse(null);
250259
if (fieldObjectType != null) {
@@ -297,77 +306,39 @@ private Field findRootField(SelectionSet selectionSet) {
297306
return null;
298307
}
299308

300-
private FieldDefinition findFieldDefinition(ObjectTypeDefinition typeDef, String fieldName) {
301-
for (var fd : typeDef.getFieldDefinitions()) {
302-
if (fd.getName().equals(fieldName)) {
303-
return fd;
304-
}
305-
}
306-
return null;
307-
}
308-
309309
private ObjectTypeDefinition getRootType(
310310
OperationDefinition operation, TypeDefinitionRegistry registry) {
311-
var rootTypeName =
311+
var operationName =
312312
switch (operation.getOperation()) {
313-
case MUTATION ->
314-
registry
315-
.schemaDefinition()
316-
.flatMap(
317-
sd ->
318-
sd.getOperationTypeDefinitions().stream()
319-
.filter(otd -> otd.getName().equals("mutation"))
320-
.findFirst())
321-
.map(otd -> otd.getTypeName().getName())
322-
.orElse("Mutation");
323-
case SUBSCRIPTION ->
324-
registry
325-
.schemaDefinition()
326-
.flatMap(
327-
sd ->
328-
sd.getOperationTypeDefinitions().stream()
329-
.filter(otd -> otd.getName().equals("subscription"))
330-
.findFirst())
331-
.map(otd -> otd.getTypeName().getName())
332-
.orElse("Subscription");
333-
default ->
334-
registry
335-
.schemaDefinition()
336-
.flatMap(
337-
sd ->
338-
sd.getOperationTypeDefinitions().stream()
339-
.filter(otd -> otd.getName().equals("query"))
340-
.findFirst())
341-
.map(otd -> otd.getTypeName().getName())
342-
.orElse("Query");
313+
case MUTATION -> "mutation";
314+
case SUBSCRIPTION -> "subscription";
315+
default -> "query";
343316
};
317+
var fallback = Character.toUpperCase(operationName.charAt(0)) + operationName.substring(1);
318+
var rootTypeName =
319+
registry
320+
.schemaDefinition()
321+
.flatMap(
322+
sd ->
323+
sd.getOperationTypeDefinitions().stream()
324+
.filter(otd -> otd.getName().equals(operationName))
325+
.findFirst())
326+
.map(otd -> otd.getTypeName().getName())
327+
.orElse(fallback);
344328
return registry.getType(rootTypeName, ObjectTypeDefinition.class).orElse(null);
345329
}
346330

347331
private String findGraphqlInputType(
348332
String javaParamTypeName, List<VariableDefinition> variableDefs) {
349333
for (var varDef : variableDefs) {
350-
var graphqlTypeName = unwrapTypeName(varDef.getType());
334+
var graphqlTypeName = GraphqlTypeMapper.unwrapTypeName(varDef.getType());
351335
if (graphqlTypeName.equals(javaParamTypeName)) {
352336
return graphqlTypeName;
353337
}
354338
}
355339
return javaParamTypeName;
356340
}
357341

358-
private String unwrapTypeName(Type<?> type) {
359-
if (type instanceof NonNullType nullType) {
360-
return unwrapTypeName(nullType.getType());
361-
}
362-
if (type instanceof ListType listType) {
363-
return unwrapTypeName(listType.getType());
364-
}
365-
if (type instanceof graphql.language.TypeName name) {
366-
return name.getName();
367-
}
368-
return "String";
369-
}
370-
371342
private static final Set<String> JAVA_BUILT_INS =
372343
Set.of(
373344
"String",
@@ -434,6 +405,129 @@ private TypeMirror unwrapListTypeMirror(TypeMirror typeMirror) {
434405
return typeMirror;
435406
}
436407

408+
private TypeAnnotationConfig resolveClassConfig(
409+
GraphqlSchema annotation,
410+
Map<String, TypeAnnotationConfig.FieldAnnotations> classFieldAnnotations) {
411+
var fqns = extractClassFqns(annotation::typeAnnotations);
412+
var rawAnnotations = annotation.rawTypeAnnotations();
413+
var usesFqns = extractClassFqns(annotation::uses);
414+
var nonNullFqns = extractClassFqns(annotation::nonNullTypeAnnotations);
415+
var nonNullRaw = annotation.nonNullRawTypeAnnotations();
416+
var config =
417+
TypeAnnotationConfig.resolve(
418+
fqns,
419+
rawAnnotations,
420+
annotation.useOptional(),
421+
classFieldAnnotations,
422+
nonNullFqns,
423+
nonNullRaw);
424+
if (usesFqns.isEmpty()) {
425+
return config;
426+
}
427+
var mergedImports = new TreeSet<>(config.imports());
428+
for (var fqn : usesFqns) {
429+
if (!fqn.startsWith("java.lang.")) {
430+
mergedImports.add(fqn);
431+
}
432+
}
433+
return new TypeAnnotationConfig(
434+
mergedImports,
435+
config.annotations(),
436+
config.useOptional(),
437+
config.fieldAnnotations(),
438+
config.nonNullAnnotations());
439+
}
440+
441+
private static List<String> extractClassFqns(Supplier<Class<?>[]> accessor) {
442+
try {
443+
var classes = accessor.get();
444+
return java.util.Arrays.stream(classes).map(Class::getCanonicalName).toList();
445+
} catch (MirroredTypesException e) {
446+
return e.getTypeMirrors().stream().map(TypeMirror::toString).toList();
447+
}
448+
}
449+
450+
private TypeAnnotationConfig resolveMethodConfig(
451+
ExecutableElement method, GraphqlQuery annotation, TypeAnnotationConfig classConfig) {
452+
var methodFqns = extractClassFqns(annotation::typeAnnotations);
453+
var methodRaw = annotation.rawTypeAnnotations();
454+
var methodToggle = annotation.useOptional();
455+
456+
var useOptional =
457+
methodToggle == Toggle.INHERIT ? classConfig.useOptional() : methodToggle == Toggle.TRUE;
458+
459+
var methodFieldAnnotations = extractFieldAnnotations(method);
460+
var fieldAnnotations =
461+
TypeAnnotationConfig.FieldAnnotations.merge(
462+
classConfig.fieldAnnotations(), methodFieldAnnotations);
463+
464+
var methodNonNullFqns = extractClassFqns(annotation::nonNullTypeAnnotations);
465+
var methodNonNullRaw = annotation.nonNullRawTypeAnnotations();
466+
boolean hasMethodNonNull = !methodNonNullFqns.isEmpty() || methodNonNullRaw.length > 0;
467+
var resolvedNonNull = hasMethodNonNull ? null : classConfig.nonNullAnnotations();
468+
469+
boolean hasMethodAnnotations = !methodFqns.isEmpty() || methodRaw.length > 0;
470+
if (!hasMethodAnnotations && !hasMethodNonNull) {
471+
if (useOptional == classConfig.useOptional()
472+
&& fieldAnnotations.equals(classConfig.fieldAnnotations())) {
473+
return classConfig;
474+
}
475+
var mergedImports = new TreeSet<>(classConfig.imports());
476+
for (var fa : fieldAnnotations.values()) {
477+
mergedImports.addAll(fa.imports());
478+
}
479+
return new TypeAnnotationConfig(
480+
mergedImports,
481+
classConfig.annotations(),
482+
useOptional,
483+
fieldAnnotations,
484+
classConfig.nonNullAnnotations());
485+
}
486+
487+
var nonNullFqns = hasMethodNonNull ? methodNonNullFqns : List.<String>of();
488+
var nonNullRaw = hasMethodNonNull ? methodNonNullRaw : new String[0];
489+
var config =
490+
TypeAnnotationConfig.resolve(
491+
methodFqns, methodRaw, useOptional, fieldAnnotations, nonNullFqns, nonNullRaw);
492+
493+
if (resolvedNonNull != null && !resolvedNonNull.isEmpty()) {
494+
var mergedImports = new TreeSet<>(config.imports());
495+
mergedImports.addAll(classConfig.imports());
496+
return new TypeAnnotationConfig(
497+
mergedImports, config.annotations(), useOptional, fieldAnnotations, resolvedNonNull);
498+
}
499+
500+
return config;
501+
}
502+
503+
private Map<String, TypeAnnotationConfig.FieldAnnotations> extractFieldAnnotations(
504+
Element method) {
505+
var fieldAnnotations = new HashMap<String, TypeAnnotationConfig.FieldAnnotations>();
506+
var graphqlFields = method.getAnnotationsByType(GraphqlField.class);
507+
for (var gf : graphqlFields) {
508+
var fqns = extractClassFqns(gf::typeAnnotations);
509+
var typeOverride = extractFieldTypeOverride(gf);
510+
var resolved =
511+
TypeAnnotationConfig.FieldAnnotations.resolve(
512+
fqns, gf.rawTypeAnnotations(), typeOverride);
513+
fieldAnnotations.put(gf.name(), resolved);
514+
}
515+
return fieldAnnotations;
516+
}
517+
518+
private String extractFieldTypeOverride(GraphqlField annotation) {
519+
try {
520+
var cls = annotation.type();
521+
if (cls == Void.class) {
522+
return null;
523+
}
524+
return cls.getCanonicalName();
525+
} catch (MirroredTypeException e) {
526+
var fqn = e.getTypeMirror().toString();
527+
return "java.lang.Void".equals(fqn) ? null : fqn;
528+
}
529+
}
530+
437531
private String getPackageName(TypeElement typeElement) {
438532
var enclosing = typeElement.getEnclosingElement();
439533
while (enclosing != null && !(enclosing instanceof PackageElement)) {

graphql-apt/src/main/java/feign/graphql/apt/GraphqlTypeMapper.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
import com.squareup.javapoet.ClassName;
1919
import com.squareup.javapoet.ParameterizedTypeName;
2020
import com.squareup.javapoet.TypeName;
21+
import graphql.language.FieldDefinition;
2122
import graphql.language.ListType;
2223
import graphql.language.NonNullType;
24+
import graphql.language.ObjectTypeDefinition;
2325
import graphql.language.Type;
2426
import java.util.HashMap;
2527
import java.util.List;
2628
import java.util.Map;
29+
import java.util.Optional;
2730

2831
public class GraphqlTypeMapper {
2932

@@ -44,11 +47,24 @@ public GraphqlTypeMapper(String targetPackage, Map<String, TypeName> customScala
4447
}
4548

4649
public TypeName map(Type<?> type) {
50+
return map(type, false);
51+
}
52+
53+
public TypeName map(Type<?> type, boolean useOptional) {
54+
boolean nullable = !(type instanceof NonNullType);
55+
var mapped = mapInner(type);
56+
if (useOptional && nullable) {
57+
return ParameterizedTypeName.get(ClassName.get(Optional.class), mapped);
58+
}
59+
return mapped;
60+
}
61+
62+
private TypeName mapInner(Type<?> type) {
4763
if (type instanceof NonNullType nullType) {
48-
return map(nullType.getType());
64+
return mapInner(nullType.getType());
4965
}
5066
if (type instanceof ListType listType) {
51-
var elementType = map(listType.getType());
67+
var elementType = mapInner(listType.getType());
5268
return ParameterizedTypeName.get(ClassName.get(List.class), elementType);
5369
}
5470
if (type instanceof graphql.language.TypeName name) {
@@ -72,4 +88,26 @@ private TypeName mapScalarOrNamed(String name) {
7288
public boolean isScalar(String name) {
7389
return BUILT_IN_SCALARS.containsKey(name) || customScalars.containsKey(name);
7490
}
91+
92+
static String unwrapTypeName(Type<?> type) {
93+
if (type instanceof NonNullType nullType) {
94+
return unwrapTypeName(nullType.getType());
95+
}
96+
if (type instanceof ListType listType) {
97+
return unwrapTypeName(listType.getType());
98+
}
99+
if (type instanceof graphql.language.TypeName name) {
100+
return name.getName();
101+
}
102+
return "String";
103+
}
104+
105+
static FieldDefinition findFieldDefinition(ObjectTypeDefinition typeDef, String fieldName) {
106+
for (var fd : typeDef.getFieldDefinitions()) {
107+
if (fd.getName().equals(fieldName)) {
108+
return fd;
109+
}
110+
}
111+
return null;
112+
}
75113
}

0 commit comments

Comments
 (0)