Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit 1a1cf6e

Browse files
feat(#5): support GraphQL interfaces
1 parent a384799 commit 1a1cf6e

File tree

10 files changed

+212
-1
lines changed

10 files changed

+212
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,12 @@ demonstrates possible workarounds for this issue.
439439
`GraphQLDirectiveDefinition` and `GraphQLTypeExtension`-annotated classes are subject to the same limitation regarding
440440
dependency injection - but there can be any number of them.
441441
442+
### Interfaces
443+
444+
Interfaces in the configured package having at least one of their methods marked as `@GraphQLField` are considered a
445+
GraphQL interface, and their implementations are automatically added to the schema. Furthermore, you have to add the
446+
following annotation to GraphQL interfaces: `@GraphQLTypeResolver(GraphQLInterfaceTypeResolver.class)`
447+
442448
### Custom scalars and type functions
443449
444450
Custom scalars can be defined in the same way as in the case of using GraphQL Java Tools - just define the

graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.kickstart.graphql.annotations;
22

33
import graphql.annotations.AnnotationsSchemaCreator;
4+
import graphql.annotations.annotationTypes.GraphQLField;
45
import graphql.annotations.annotationTypes.GraphQLTypeExtension;
56
import graphql.annotations.annotationTypes.directives.definition.GraphQLDirectiveDefinition;
67
import graphql.annotations.processor.GraphQLAnnotations;
@@ -20,13 +21,17 @@
2021
import lombok.extern.slf4j.Slf4j;
2122
import org.reflections.Reflections;
2223
import org.reflections.ReflectionsException;
24+
import org.reflections.scanners.MethodAnnotationsScanner;
25+
import org.reflections.scanners.SubTypesScanner;
26+
import org.reflections.scanners.TypeAnnotationsScanner;
2327
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
2428
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2529
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2630
import org.springframework.context.annotation.Bean;
2731
import org.springframework.context.annotation.Configuration;
2832

2933
import java.lang.annotation.Annotation;
34+
import java.lang.reflect.Method;
3035
import java.util.Collections;
3136
import java.util.List;
3237
import java.util.Optional;
@@ -46,6 +51,11 @@ public class GraphQLAnnotationsAutoConfiguration {
4651
private final List<TypeFunction> typeFunctions;
4752
private final List<GraphQLScalarType> customScalarTypes;
4853

54+
@Bean
55+
public GraphQLInterfaceTypeResolver graphQLInterfaceTypeResolver() {
56+
return new GraphQLInterfaceTypeResolver();
57+
}
58+
4959
@Bean
5060
@ConditionalOnMissingBean
5161
public GraphQLAnnotations graphQLAnnotations() {
@@ -58,7 +68,8 @@ public GraphQLSchema graphQLSchema(final GraphQLAnnotations graphQLAnnotations)
5868
log.info("GraphQL classes are searched in the following package (including subpackages): {}",
5969
graphQLAnnotationsProperties.getBasePackage());
6070
final AnnotationsSchemaCreator.Builder builder = newAnnotationsSchema();
61-
final Reflections reflections = new Reflections(graphQLAnnotationsProperties.getBasePackage());
71+
final Reflections reflections = new Reflections(graphQLAnnotationsProperties.getBasePackage(),
72+
new MethodAnnotationsScanner(), new SubTypesScanner(), new TypeAnnotationsScanner());
6273
builder.setAlwaysPrettify(graphQLAnnotationsProperties.isAlwaysPrettify());
6374
setQueryResolverClass(builder, reflections);
6475
setMutationResolverClass(builder, reflections);
@@ -88,6 +99,7 @@ public GraphQLSchema graphQLSchema(final GraphQLAnnotations graphQLAnnotations)
8899
log.info("Registering relay {}", r.getClass());
89100
builder.setRelay(r);
90101
});
102+
registerGraphQLInterfaceImplementations(reflections, builder);
91103
return builder.build();
92104
}
93105

@@ -176,4 +188,30 @@ private <T> Set<Class<? extends T>> getSubTypesOf(
176188
return Collections.emptySet();
177189
}
178190
}
191+
192+
/**
193+
* This is required, because normally implementations of interfaces are not explicitly returned by any resolver
194+
* method, and therefor not added to the schema automatically.
195+
*
196+
* All interfaces are considered GraphQL interfaces if they are declared in the configured package and
197+
* have at least one {@link GraphQLField}-annotated methods.
198+
*
199+
* @param reflections the reflections instance.
200+
* @param builder the schema builder instance.
201+
*/
202+
private void registerGraphQLInterfaceImplementations(
203+
final Reflections reflections,
204+
final AnnotationsSchemaCreator.Builder builder
205+
) {
206+
reflections.getMethodsAnnotatedWith(GraphQLField.class).stream()
207+
.map(Method::getDeclaringClass)
208+
.filter(Class::isInterface)
209+
.forEach(graphQLInterface ->
210+
reflections.getSubTypesOf(graphQLInterface)
211+
.forEach(implementation -> {
212+
log.info("Registering {} as an implementation of GraphQL interface {}", implementation,
213+
graphQLInterface);
214+
builder.additionalType(implementation);
215+
}));
216+
}
179217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import graphql.TypeResolutionEnvironment;
4+
import graphql.annotations.processor.GraphQLAnnotations;
5+
import graphql.schema.GraphQLObjectType;
6+
import graphql.schema.TypeResolver;
7+
import org.springframework.beans.BeansException;
8+
import org.springframework.context.ApplicationContext;
9+
import org.springframework.context.ApplicationContextAware;
10+
11+
/**
12+
* Type resolver for GraphQL interfaces.
13+
* @see <a href="https://github.com/Enigmatis/graphql-java-annotations/issues/100">Issue with workaround.</a>
14+
*
15+
* Apply this interface to GraphQL interfaces using the {@link graphql.annotations.annotationTypes.GraphQLTypeResolver}
16+
* annotation.
17+
*/
18+
public class GraphQLInterfaceTypeResolver implements TypeResolver, ApplicationContextAware {
19+
20+
private static GraphQLAnnotations graphQLAnnotations;
21+
22+
@Override
23+
public GraphQLObjectType getType(final TypeResolutionEnvironment env) {
24+
return graphQLAnnotations.object(env.getObject().getClass());
25+
}
26+
27+
@Override
28+
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
29+
graphQLAnnotations = applicationContext.getBean(GraphQLAnnotations.class);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import com.graphql.spring.boot.test.GraphQLResponse;
4+
import com.graphql.spring.boot.test.GraphQLTestTemplate;
5+
import graphql.kickstart.graphql.annotations.test.interfaces.Car;
6+
import graphql.kickstart.graphql.annotations.test.interfaces.Truck;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.test.context.ActiveProfiles;
12+
13+
import java.io.IOException;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
@DisplayName("Testing interface handling.")
18+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
19+
@ActiveProfiles({"test", "interface-test"})
20+
public class GraphQLInterfaceQueryTest {
21+
22+
@Autowired
23+
private GraphQLTestTemplate graphQLTestTemplate;
24+
25+
@Test
26+
@DisplayName("Assert that GraphQL interfaces and their implementations are registered correctly.")
27+
void testInterfaceQuery() throws IOException {
28+
// WHEN
29+
final GraphQLResponse actual = graphQLTestTemplate.postForResource("queries/test-interface-query.graphql");
30+
// THEN
31+
assertThat(actual.get("$.data.vehicles[0]", Car.class))
32+
.usingRecursiveComparison().ignoringAllOverriddenEquals()
33+
.isEqualTo(Car.builder().numberOfSeats(4).registrationNumber("ABC-123").build());
34+
assertThat(actual.get("$.data.vehicles[1]", Truck.class))
35+
.usingRecursiveComparison().ignoringAllOverriddenEquals()
36+
.isEqualTo(Truck.builder().cargoWeightCapacity(12).registrationNumber("CBA-321").build());
37+
}
38+
}
39+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package graphql.kickstart.graphql.annotations.test.interfaces;
2+
3+
import graphql.annotations.annotationTypes.GraphQLField;
4+
import graphql.annotations.annotationTypes.GraphQLNonNull;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class Car implements Vehicle {
15+
16+
/**
17+
* Note that you have to repeat the annotations from the interface method!
18+
*/
19+
@GraphQLField
20+
@GraphQLNonNull
21+
private String registrationNumber;
22+
23+
@GraphQLField
24+
@GraphQLNonNull
25+
private int numberOfSeats;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package graphql.kickstart.graphql.annotations.test.interfaces;
2+
3+
import graphql.annotations.annotationTypes.GraphQLDescription;
4+
import graphql.annotations.annotationTypes.GraphQLField;
5+
import graphql.annotations.annotationTypes.GraphQLNonNull;
6+
import graphql.kickstart.tools.GraphQLQueryResolver;
7+
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
public class InterfaceQuery implements GraphQLQueryResolver {
12+
13+
@GraphQLField
14+
@GraphQLNonNull
15+
@GraphQLDescription("Returns vehicles")
16+
public static List<Vehicle> vehicles() {
17+
return Arrays.asList(new Car("ABC-123", 4), new Truck("CBA-321", 12));
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package graphql.kickstart.graphql.annotations.test.interfaces;
2+
3+
import graphql.annotations.annotationTypes.GraphQLField;
4+
import graphql.annotations.annotationTypes.GraphQLNonNull;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class Truck implements Vehicle {
15+
16+
/**
17+
* Note that you have to repeat the annotations from the interface method!
18+
*/
19+
@GraphQLField
20+
@GraphQLNonNull
21+
private String registrationNumber;
22+
23+
@GraphQLField
24+
@GraphQLNonNull
25+
private int cargoWeightCapacity;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package graphql.kickstart.graphql.annotations.test.interfaces;
2+
3+
import graphql.annotations.annotationTypes.GraphQLField;
4+
import graphql.annotations.annotationTypes.GraphQLNonNull;
5+
import graphql.annotations.annotationTypes.GraphQLTypeResolver;
6+
import graphql.kickstart.graphql.annotations.GraphQLInterfaceTypeResolver;
7+
8+
@GraphQLTypeResolver(GraphQLInterfaceTypeResolver.class)
9+
public interface Vehicle {
10+
11+
@GraphQLField
12+
@GraphQLNonNull
13+
String getRegistrationNumber();
14+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
graphql.annotations.base-package: graphql.kickstart.graphql.annotations.test.interfaces
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
query {
2+
vehicles {
3+
registrationNumber
4+
... on Car {
5+
numberOfSeats
6+
}
7+
... on Truck {
8+
cargoWeightCapacity
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)