Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add @GraphqlField for field-level annotations/type overrides, non-nul…
…l field annotations, and uses for imports

Signed-off-by: Marvin Froeder <[email protected]>
  • Loading branch information
velo committed Mar 16, 2026
commit d44b76edb2089622d1e70ceb43a2f844ff4d45ed
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.google.auto.service.AutoService;
import com.squareup.javapoet.TypeName;
import feign.graphql.GraphqlField;
import feign.graphql.GraphqlQuery;
import feign.graphql.GraphqlSchema;
import feign.graphql.Scalar;
Expand All @@ -41,6 +42,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
Expand All @@ -50,10 +52,12 @@
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
Expand Down Expand Up @@ -120,7 +124,8 @@ private void processInterface(TypeElement typeElement) {
var validator = new QueryValidator(messager);
var generator = new TypeGenerator(filer, messager, registry, typeMapper, targetPackage);

var classConfig = resolveClassConfig(schemaAnnotation);
var classFieldAnnotations = extractFieldAnnotations(typeElement);
var classConfig = resolveClassConfig(schemaAnnotation, classFieldAnnotations);

for (var enclosed : typeElement.getEnclosedElements()) {
if (!(enclosed instanceof ExecutableElement method)) {
Expand All @@ -131,7 +136,7 @@ private void processInterface(TypeElement typeElement) {
continue;
}

var methodConfig = resolveMethodConfig(queryAnnotation, classConfig);
var methodConfig = resolveMethodConfig(method, queryAnnotation, classConfig);
generator.setAnnotationConfig(methodConfig);

processMethod(
Expand Down Expand Up @@ -442,31 +447,170 @@ private TypeMirror unwrapListTypeMirror(TypeMirror typeMirror) {
return typeMirror;
}

private TypeAnnotationConfig resolveClassConfig(GraphqlSchema annotation) {
private TypeAnnotationConfig resolveClassConfig(
GraphqlSchema annotation,
Map<String, TypeAnnotationConfig.FieldAnnotations> classFieldAnnotations) {
var fqns = extractTypeAnnotationFqns(annotation);
var rawAnnotations = annotation.rawTypeAnnotations();
return TypeAnnotationConfig.resolve(fqns, rawAnnotations, annotation.useOptional());
var usesFqns = extractUsesFqns(annotation);
var nonNullFqns = extractNonNullTypeAnnotationFqns(annotation);
var nonNullRaw = annotation.nonNullRawTypeAnnotations();
var config =
TypeAnnotationConfig.resolve(
fqns,
rawAnnotations,
annotation.useOptional(),
classFieldAnnotations,
nonNullFqns,
nonNullRaw);
if (usesFqns.isEmpty()) {
return config;
}
var mergedImports = new TreeSet<>(config.imports());
for (var fqn : usesFqns) {
if (!fqn.startsWith("java.lang.")) {
mergedImports.add(fqn);
}
}
return new TypeAnnotationConfig(
mergedImports,
config.annotations(),
config.useOptional(),
config.fieldAnnotations(),
config.nonNullAnnotations());
}

private List<String> extractNonNullTypeAnnotationFqns(GraphqlSchema annotation) {
try {
var classes = annotation.nonNullTypeAnnotations();
var result = new ArrayList<String>(classes.length);
for (var cls : classes) {
result.add(cls.getCanonicalName());
}
return result;
} catch (MirroredTypesException e) {
return e.getTypeMirrors().stream().map(TypeMirror::toString).toList();
}
}

private List<String> extractNonNullTypeAnnotationFqns(GraphqlQuery annotation) {
try {
var classes = annotation.nonNullTypeAnnotations();
var result = new ArrayList<String>(classes.length);
for (var cls : classes) {
result.add(cls.getCanonicalName());
}
return result;
} catch (MirroredTypesException e) {
return e.getTypeMirrors().stream().map(TypeMirror::toString).toList();
}
}

private List<String> extractUsesFqns(GraphqlSchema annotation) {
try {
var classes = annotation.uses();
var result = new ArrayList<String>(classes.length);
for (var cls : classes) {
result.add(cls.getCanonicalName());
}
return result;
} catch (MirroredTypesException e) {
return e.getTypeMirrors().stream().map(TypeMirror::toString).toList();
}
}

private TypeAnnotationConfig resolveMethodConfig(
GraphqlQuery annotation, TypeAnnotationConfig classConfig) {
ExecutableElement method, GraphqlQuery annotation, TypeAnnotationConfig classConfig) {
var methodFqns = extractTypeAnnotationFqns(annotation);
var methodRaw = annotation.rawTypeAnnotations();
var methodToggle = annotation.useOptional();

var useOptional =
methodToggle == Toggle.INHERIT ? classConfig.useOptional() : methodToggle == Toggle.TRUE;

var methodFieldAnnotations = extractFieldAnnotations(method);
var fieldAnnotations =
TypeAnnotationConfig.FieldAnnotations.merge(
classConfig.fieldAnnotations(), methodFieldAnnotations);

var methodNonNullFqns = extractNonNullTypeAnnotationFqns(annotation);
var methodNonNullRaw = annotation.nonNullRawTypeAnnotations();
boolean hasMethodNonNull = !methodNonNullFqns.isEmpty() || methodNonNullRaw.length > 0;
var resolvedNonNull = hasMethodNonNull ? null : classConfig.nonNullAnnotations();

boolean hasMethodAnnotations = !methodFqns.isEmpty() || methodRaw.length > 0;
if (!hasMethodAnnotations) {
if (useOptional == classConfig.useOptional()) {
if (!hasMethodAnnotations && !hasMethodNonNull) {
if (useOptional == classConfig.useOptional()
&& fieldAnnotations.equals(classConfig.fieldAnnotations())) {
return classConfig;
}
var mergedImports = new TreeSet<>(classConfig.imports());
for (var fa : fieldAnnotations.values()) {
mergedImports.addAll(fa.imports());
}
return new TypeAnnotationConfig(
mergedImports,
classConfig.annotations(),
useOptional,
fieldAnnotations,
classConfig.nonNullAnnotations());
}

var nonNullFqns = hasMethodNonNull ? methodNonNullFqns : List.<String>of();
var nonNullRaw = hasMethodNonNull ? methodNonNullRaw : new String[0];
var config =
TypeAnnotationConfig.resolve(
methodFqns, methodRaw, useOptional, fieldAnnotations, nonNullFqns, nonNullRaw);

if (resolvedNonNull != null && !resolvedNonNull.isEmpty()) {
var mergedImports = new TreeSet<>(config.imports());
mergedImports.addAll(classConfig.imports());
return new TypeAnnotationConfig(
classConfig.imports(), classConfig.annotations(), useOptional);
mergedImports, config.annotations(), useOptional, fieldAnnotations, resolvedNonNull);
}

return config;
}

private Map<String, TypeAnnotationConfig.FieldAnnotations> extractFieldAnnotations(
Element method) {
var fieldAnnotations = new HashMap<String, TypeAnnotationConfig.FieldAnnotations>();
var graphqlFields = method.getAnnotationsByType(GraphqlField.class);
for (var gf : graphqlFields) {
var fqns = extractFieldTypeAnnotationFqns(gf);
var typeOverride = extractFieldTypeOverride(gf);
var resolved =
TypeAnnotationConfig.FieldAnnotations.resolve(
fqns, gf.rawTypeAnnotations(), typeOverride);
fieldAnnotations.put(gf.name(), resolved);
}
return fieldAnnotations;
}

return TypeAnnotationConfig.resolve(methodFqns, methodRaw, useOptional);
private String extractFieldTypeOverride(GraphqlField annotation) {
try {
var cls = annotation.type();
if (cls == Void.class) {
return null;
}
return cls.getCanonicalName();
} catch (MirroredTypeException e) {
var fqn = e.getTypeMirror().toString();
return "java.lang.Void".equals(fqn) ? null : fqn;
}
}

private List<String> extractFieldTypeAnnotationFqns(GraphqlField annotation) {
try {
var classes = annotation.typeAnnotations();
var result = new ArrayList<String>(classes.length);
for (var cls : classes) {
result.add(cls.getCanonicalName());
}
return result;
} catch (MirroredTypesException e) {
return e.getTypeMirrors().stream().map(TypeMirror::toString).toList();
}
}

private List<String> extractTypeAnnotationFqns(GraphqlSchema annotation) {
Expand Down
127 changes: 124 additions & 3 deletions graphql-apt/src/main/java/feign/graphql/apt/TypeAnnotationConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,50 @@
package feign.graphql.apt;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

record TypeAnnotationConfig(Set<String> imports, List<String> annotations, boolean useOptional) {
record TypeAnnotationConfig(
Set<String> imports,
List<String> annotations,
boolean useOptional,
Map<String, FieldAnnotations> fieldAnnotations,
List<String> nonNullAnnotations) {

static final TypeAnnotationConfig EMPTY = new TypeAnnotationConfig(Set.of(), List.of(), false);
static final TypeAnnotationConfig EMPTY =
new TypeAnnotationConfig(Set.of(), List.of(), false, Map.of(), List.of());

static TypeAnnotationConfig resolve(
List<String> typeAnnotationFqns, String[] rawTypeAnnotations, boolean useOptional) {
return resolve(
typeAnnotationFqns, rawTypeAnnotations, useOptional, Map.of(), List.of(), new String[0]);
}

static TypeAnnotationConfig resolve(
List<String> typeAnnotationFqns,
String[] rawTypeAnnotations,
boolean useOptional,
Map<String, FieldAnnotations> fieldAnnotations) {
return resolve(
typeAnnotationFqns,
rawTypeAnnotations,
useOptional,
fieldAnnotations,
List.of(),
new String[0]);
}

static TypeAnnotationConfig resolve(
List<String> typeAnnotationFqns,
String[] rawTypeAnnotations,
boolean useOptional,
Map<String, FieldAnnotations> fieldAnnotations,
List<String> nonNullFqns,
String[] nonNullRawAnnotations) {

var rawSimpleNames = new HashSet<String>();
for (var raw : rawTypeAnnotations) {
Expand All @@ -52,6 +85,94 @@ static TypeAnnotationConfig resolve(
annotations.add(raw.startsWith("@") ? raw : "@" + raw);
}

return new TypeAnnotationConfig(imports, annotations, useOptional);
for (var fa : fieldAnnotations.values()) {
imports.addAll(fa.imports());
}

var nonNullResolved = resolveAnnotationList(nonNullFqns, nonNullRawAnnotations, imports);

return new TypeAnnotationConfig(
imports, annotations, useOptional, fieldAnnotations, nonNullResolved);
}

private static List<String> resolveAnnotationList(
List<String> fqns, String[] rawAnnotations, Set<String> imports) {
var rawSimpleNames = new HashSet<String>();
for (var raw : rawAnnotations) {
var stripped = raw.startsWith("@") ? raw.substring(1) : raw;
var parenIdx = stripped.indexOf('(');
rawSimpleNames.add(parenIdx > 0 ? stripped.substring(0, parenIdx).trim() : stripped.trim());
}

var annotations = new ArrayList<String>();

for (var fqn : fqns) {
var simpleName = fqn.substring(fqn.lastIndexOf('.') + 1);
if (!fqn.startsWith("java.lang.")) {
imports.add(fqn);
}
if (!rawSimpleNames.contains(simpleName)) {
annotations.add("@" + simpleName);
}
}

for (var raw : rawAnnotations) {
annotations.add(raw.startsWith("@") ? raw : "@" + raw);
}

return annotations;
}

record FieldAnnotations(Set<String> imports, List<String> annotations, String typeOverride) {

static FieldAnnotations resolve(
List<String> fqns, String[] rawAnnotations, String typeOverride) {
var rawSimpleNames = new HashSet<String>();
for (var raw : rawAnnotations) {
var stripped = raw.startsWith("@") ? raw.substring(1) : raw;
var parenIdx = stripped.indexOf('(');
rawSimpleNames.add(parenIdx > 0 ? stripped.substring(0, parenIdx).trim() : stripped.trim());
}

var imports = new TreeSet<String>();
var annotations = new ArrayList<String>();

for (var fqn : fqns) {
var simpleName = fqn.substring(fqn.lastIndexOf('.') + 1);
if (!fqn.startsWith("java.lang.")) {
imports.add(fqn);
}
if (!rawSimpleNames.contains(simpleName)) {
annotations.add("@" + simpleName);
}
}

for (var raw : rawAnnotations) {
annotations.add(raw.startsWith("@") ? raw : "@" + raw);
}

if (typeOverride != null) {
var simpleName = typeOverride.substring(typeOverride.lastIndexOf('.') + 1);
if (!typeOverride.startsWith("java.lang.")) {
imports.add(typeOverride);
}
return new FieldAnnotations(imports, annotations, simpleName);
}

return new FieldAnnotations(imports, annotations, null);
}

static Map<String, FieldAnnotations> merge(
Map<String, FieldAnnotations> classLevel, Map<String, FieldAnnotations> methodLevel) {
if (methodLevel.isEmpty()) {
return classLevel;
}
if (classLevel.isEmpty()) {
return methodLevel;
}
var merged = new HashMap<>(classLevel);
merged.putAll(methodLevel);
return merged;
}
}
}
Loading