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

Ignore abstract classes when detecting GraphQL interface implementations #502 #504

Merged
merged 1 commit into from
Jan 4, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import graphql.schema.GraphQLSchema;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
Expand Down Expand Up @@ -179,11 +181,15 @@ private void registerGraphQLInterfaceImplementations(
final Reflections reflections,
final AnnotationsSchemaCreator.Builder builder
) {
Predicate<Class<?>> implementationQualifiesForInclusion =
type -> !(graphQLAnnotationsProperties.isIgnoreAbstractInterfaceImplementations()
&& Modifier.isAbstract(type.getModifiers()));
reflections.getMethodsAnnotatedWith(GraphQLField.class).stream()
.map(Method::getDeclaringClass)
.filter(Class::isInterface)
.forEach(graphQLInterface ->
reflections.getSubTypesOf(graphQLInterface)
reflections.getSubTypesOf(graphQLInterface).stream()
.filter(implementationQualifiesForInclusion)
.forEach(implementation -> {
log.info("Registering {} as an implementation of GraphQL interface {}",
implementation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ public class GraphQLAnnotationsProperties {
*/
@Builder.Default
private boolean alwaysPrettify = true;

/**
* If set to <code>true</code> abstract classes implementing a GraphQL interface will not be added to the schema.
* Defaults to <code>false</code> for backward compatibility.
*/
@Builder.Default
private boolean ignoreAbstractInterfaceImplementations = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package graphql.kickstart.graphql.annotations;

import com.graphql.spring.boot.test.GraphQLResponse;
import com.graphql.spring.boot.test.GraphQLTestTemplate;
import graphql.kickstart.graphql.annotations.test.interfaces.Car;
import graphql.kickstart.graphql.annotations.test.interfaces.Truck;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("Testing interface handling (ignore abstract implementations).")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "graphql.annotations.ignore-abstract-interface-implementations=true")
@ActiveProfiles({"test", "interface-test"})
class GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest {

@Autowired
private GraphQLTestTemplate graphQLTestTemplate;

@Autowired
private GraphQLSchema graphQLSchema;

@Test
@DisplayName("Assert that GraphQL interfaces and their implementations are registered correctly.")
void testInterfaceQuery() throws IOException {
// WHEN
final GraphQLResponse actual = graphQLTestTemplate
.postForResource("queries/test-interface-query.graphql");
// THEN
assertThat(actual.get("$.data.vehicles[0]", Car.class))
.usingRecursiveComparison().ignoringAllOverriddenEquals()
.isEqualTo(Car.builder().numberOfSeats(4).registrationNumber("ABC-123").build());
assertThat(actual.get("$.data.vehicles[1]", Truck.class))
.usingRecursiveComparison().ignoringAllOverriddenEquals()
.isEqualTo(Truck.builder().cargoWeightCapacity(12).registrationNumber("CBA-321").build());
}

@Test
@DisplayName("Assert that abstract GraphQL interface implementations are excluded from the schema.")
void testInterfaceImplementationDetection() {
// THEN
Set<String> vehicleDomainTypes = graphQLSchema.getAllTypesAsList().stream()
.filter(type -> !(type instanceof GraphQLScalarType))
.map(GraphQLNamedType::getName)
.filter(name -> !name.startsWith("__"))
.filter(name -> !"PageInfo".equals(name))
.collect(Collectors.toSet());
// Must not contain "AbstractVehicle"
assertThat(vehicleDomainTypes)
.containsExactlyInAnyOrder("InterfaceQuery", "Vehicle", "Car", "Truck");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import graphql.kickstart.graphql.annotations.test.interfaces.Car;
import graphql.kickstart.graphql.annotations.test.interfaces.Truck;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -21,6 +26,9 @@ class GraphQLInterfaceQueryTest {
@Autowired
private GraphQLTestTemplate graphQLTestTemplate;

@Autowired
private GraphQLSchema graphQLSchema;

@Test
@DisplayName("Assert that GraphQL interfaces and their implementations are registered correctly.")
void testInterfaceQuery() throws IOException {
Expand All @@ -35,5 +43,20 @@ void testInterfaceQuery() throws IOException {
.usingRecursiveComparison().ignoringAllOverriddenEquals()
.isEqualTo(Truck.builder().cargoWeightCapacity(12).registrationNumber("CBA-321").build());
}

@Test
@DisplayName("Assert that abstract GraphQL interface implementations are added to the schema.")
void testInterfaceImplementationDetection() {
// THEN
Set<String> vehicleDomainTypes = graphQLSchema.getAllTypesAsList().stream()
.filter(type -> !(type instanceof GraphQLScalarType))
.map(GraphQLNamedType::getName)
.filter(name -> !name.startsWith("__"))
.filter(name -> !"PageInfo".equals(name))
.collect(Collectors.toSet());
// Should contain "AbstractVehicle"
assertThat(vehicleDomainTypes)
.containsExactlyInAnyOrder("InterfaceQuery", "Vehicle", "AbstractVehicle", "Car", "Truck");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package graphql.kickstart.graphql.annotations.test.interfaces;

import graphql.annotations.annotationTypes.GraphQLField;
import graphql.annotations.annotationTypes.GraphQLNonNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public abstract class AbstractVehicle implements Vehicle {

/**
* Note that you have to repeat the annotations from the interface method!
*/
@GraphQLField
@GraphQLNonNull
private String registrationNumber;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@
import graphql.annotations.annotationTypes.GraphQLField;
import graphql.annotations.annotationTypes.GraphQLNonNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@Builder
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class Car implements Vehicle {

/**
* Note that you have to repeat the annotations from the interface method!
*/
@GraphQLField
@GraphQLNonNull
private String registrationNumber;
@EqualsAndHashCode(callSuper = true)
// “implements Vehicle” has to be repeated here although already inherited from AbstractVehicle
// because otherwise GraphQL-Java Annotations would not find this class.
public class Car extends AbstractVehicle implements Vehicle {

@GraphQLField
@GraphQLNonNull
private int numberOfSeats;

public Car(String registrationNumber, int numberOfSeats) {
super(registrationNumber);
this.numberOfSeats = numberOfSeats;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
// Truck intentionally does not extend AbstractVehicle in order to have one inheritance
// hierarchy free from abstract classes.
public class Truck implements Vehicle {

/**
Expand Down