diff --git a/README.md b/README.md index 8c0913d2..f48ed9ee 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,9 @@ the classpath. Use the `schemaLocationPattern` property to customize this patter https://github.com/Enigmatis/graphql-java-annotations +The GraphQL Annotations library is used instead of GraphQL Java Tools if the `graphql-spring-boot-starter` +dependency is replaced by `graphql-kickstart-spring-boot-starter-graphql-annotations`. + The schema will be built using the GraphQL Annotations library in a code-first approach - instead of writing it manually, the schema will be constructed based on the Java code. Please see the documentation of the GraphQL Annotations library for a detailed documentation of the available diff --git a/build.gradle b/build.gradle index 68e22a84..128bd07a 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,7 @@ subprojects { jcenter() maven { url "https://dl.bintray.com/graphql-java-kickstart/releases" } maven { url "https://repo.spring.io/libs-milestone" } + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } maven { url "https://oss.jfrog.org/artifactory/oss-snapshot-local" } } @@ -88,6 +89,10 @@ subprojects { } } + jacoco { + toolVersion = "0.8.7-SNAPSHOT" + } + jacocoTestReport { reports { xml.enabled = true diff --git a/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLProperties.java b/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLProperties.java index 58f16e68..85b69d7a 100644 --- a/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLProperties.java +++ b/graphiql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/graphiql/boot/GraphiQLProperties.java @@ -32,12 +32,27 @@ static class CodeMirror { @Data static class Props { - private Variables variables = new Variables(); + private GraphiQLVariables variables = new GraphiQLVariables(); + /** + * See https://github.com/graphql/graphiql/tree/main/packages/graphiql#props + */ @Data - static class Variables { + static class GraphiQLVariables { + private String query; + private String variables; + private String headers; + private String operationName; + private String response; + private String defaultQuery; + private boolean defaultVariableEditorOpen; + private boolean defaultSecondaryEditorOpen; private String editorTheme; + private boolean readOnly; + private boolean docsExplorerOpen; + private boolean headerEditorEnabled; + private boolean shouldPersistHeaders; } } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java index a6b7fa7c..803c8feb 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java @@ -18,10 +18,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; @@ -187,11 +189,15 @@ private void registerGraphQLInterfaceImplementations( final Reflections reflections, final AnnotationsSchemaCreator.Builder builder ) { + Predicate> 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, diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java index 92c8d065..3de34e0e 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java @@ -40,4 +40,12 @@ public class GraphQLAnnotationsProperties { * If not configured the default suffix of the GraphQL-Java Annotations library is used. */ private String inputSuffix; + + /** + * If set to true abstract classes implementing a GraphQL interface will not be added to the schema. + * Defaults to false for backward compatibility. + */ + @Builder.Default + private boolean ignoreAbstractInterfaceImplementations = false; + } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java new file mode 100644 index 00000000..2c24b6bf --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java @@ -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 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"); + } +} + diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java index e7aaf680..9ed3039b 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java @@ -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; @@ -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 { @@ -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 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"); + } } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java new file mode 100644 index 00000000..dad3a501 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java @@ -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; +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java index 0f684229..5c836a5c 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java @@ -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; + } } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java index 9bc43003..f1f7fbf8 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java @@ -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 { /** diff --git a/playground-spring-boot-autoconfigure/src/main/resources/templates/playground.html b/playground-spring-boot-autoconfigure/src/main/resources/templates/playground.html index be52603b..b7586ce9 100644 --- a/playground-spring-boot-autoconfigure/src/main/resources/templates/playground.html +++ b/playground-spring-boot-autoconfigure/src/main/resources/templates/playground.html @@ -2,7 +2,7 @@ - +