diff --git a/README.md b/README.md index 2ed63b3c..19fb9799 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ and join the team! **Table of Contents** -- [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x) + - [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x) - [Using Gradle](#using-gradle) - [Using Maven](#using-maven) - [Documentation](#documentation) @@ -26,21 +26,22 @@ and join the team! - [Enable Graph*i*QL](#enable-graphiql) - [Enable Altair](#enable-altair) - [Enable GraphQL Playground](#enable-graphql-playground) - - [Basic settings](#basic-settings) - - [CDN](#cdn) - - [Custom static resources](#custom-static-resources) - - [Customizing GraphQL Playground](#customizing-graphql-playground) - - [Tabs](#tabs) + - [Basic settings](#basic-settings) + - [CDN](#cdn) + - [Custom static resources](#custom-static-resources) + - [Customizing GraphQL Playground](#customizing-graphql-playground) + - [Tabs](#tabs) - [Supported GraphQL-Java Libraries](#supported-graphql-java-libraries) - - [GraphQL Java Tools](#graphql-java-tools) - - [GraphQL Annotations](#graphql-annotations) - - [Configuration](#configuration) - - [Root resolvers, directives, type extensions](#root-resolvers-directives-type-extensions) - - [Interfaces](#interfaces) - - [Custom scalars and type functions](#custom-scalars-and-type-functions) - - [Custom Relay and GraphQL Annotation Processor](#custom-relay-and-graphql-annotation-processor) + - [GraphQL Java Tools](#graphql-java-tools) + - [GraphQL Annotations](#graphql-annotations) + - [Configuration](#configuration) + - [Root resolvers, directives, type extensions](#root-resolvers-directives-type-extensions) + - [Interfaces](#interfaces) + - [Custom scalars and type functions](#custom-scalars-and-type-functions) + - [Custom Relay and GraphQL Annotation Processor](#custom-relay-and-graphql-annotation-processor) + - [Extended scalars](#extended-scalars) - [Tracing and Metrics](#tracing-and-metrics) - - [Usage](#usage) + - [Usage](#usage) - [Contributions](#contributions) - [Licenses](#licenses) @@ -537,6 +538,31 @@ It is possible to define a bean implementing `Relay` and/or `GraphQLAnnotations` will be passed to the schema builder. Spring dependency injection works as usual. Note that GraphQL Annotations provides default implementation for these which should be sufficient is most cases. +## Extended scalars + +[Extended scalars](https://github.com/graphql-java/graphql-java-extended-scalars) can be enabled by using the +`graphql.extended-scalars` configuration property, e. g.: + +```yaml +graphql: + extended-scalars: BigDecimal, Date +``` + +The available scalars are the following: `BigDecimal`, `BigInteger`, `Byte`, `Char`, `Date`, `DateTime`, `JSON`, +`Locale`, `Long`, `NegativeFloat`, `NegativeInt`, `NonNegativeFloat`, `NonNegativeInt`, `NonPositiveFloat`, +`NonPositiveInt`, `Object`, `PositiveFloat`, `PositiveInt`, `Short`, `Time`, `Url`. + +This setting works with both the [GraphQL Java Tools](#graphql-java-tools) and the +[GraphQL Annotations](#graphql-annotations) integration. + +When using the [GraphQL Java Tools](#graphql-java-tools) integration, the scalars must also be declared in the GraphQL +Schema: + +```graphql +scalar BigDecimal +scalar Date +``` + # Tracing and Metrics [Apollo style tracing](https://github.com/apollographql/apollo-tracing) along with two levels of diff --git a/example-graphql-subscription/src/main/resources/application.yml b/example-graphql-subscription/src/main/resources/application.yml index d252806d..0be7d619 100644 --- a/example-graphql-subscription/src/main/resources/application.yml +++ b/example-graphql-subscription/src/main/resources/application.yml @@ -3,3 +3,6 @@ spring: name: graphql-subscription-example server: port: 9001 + +graphql: + extended-scalars: BigDecimal diff --git a/example-graphql-subscription/src/main/resources/subscription.graphqls b/example-graphql-subscription/src/main/resources/subscription.graphqls index a5e14677..961cbb76 100644 --- a/example-graphql-subscription/src/main/resources/subscription.graphqls +++ b/example-graphql-subscription/src/main/resources/subscription.graphqls @@ -6,6 +6,8 @@ schema { subscription : Subscription } +scalar BigDecimal + type Query { hello : String } @@ -17,6 +19,6 @@ type Subscription { type StockPriceUpdate { dateTime : String stockCode : String - stockPrice : Float - stockPriceChange : Float! + stockPrice : BigDecimal + stockPriceChange : BigDecimal! } diff --git a/gradle.properties b/gradle.properties index 5a67df83..6f2a8201 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,6 +32,7 @@ SOURCE_COMPATIBILITY=1.8 TARGET_COMPATIBILITY=1.8 ### Dependencies LIB_GRAPHQL_JAVA_VER=16.1 +LIB_EXTENDED_SCALARS_VERSION=16.0.0 LIB_SPRING_BOOT_VER=2.4.2 LIB_GRAPHQL_SERVLET_VER=11.0.0 LIB_GRAPHQL_JAVA_TOOLS_VER=11.0.0 diff --git a/graphql-spring-boot-autoconfigure/build.gradle b/graphql-spring-boot-autoconfigure/build.gradle index 56c3b7bc..04e59ca9 100644 --- a/graphql-spring-boot-autoconfigure/build.gradle +++ b/graphql-spring-boot-autoconfigure/build.gradle @@ -21,6 +21,7 @@ dependencies { api(project(":graphql-kickstart-spring-boot-starter-tools")) api(project(":graphql-kickstart-spring-support")) implementation "org.springframework.boot:spring-boot-autoconfigure" + api "com.graphql-java:graphql-java-extended-scalars:$LIB_EXTENDED_SCALARS_VERSION" api "com.graphql-java-kickstart:graphql-java-kickstart:$LIB_GRAPHQL_SERVLET_VER" api "com.graphql-java-kickstart:graphql-java-servlet:$LIB_GRAPHQL_SERVLET_VER" api "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER" diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLExtendedScalarsInitializer.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLExtendedScalarsInitializer.java new file mode 100644 index 00000000..3d9eb64d --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLExtendedScalarsInitializer.java @@ -0,0 +1,68 @@ +package graphql.kickstart.spring.web.boot; + +import graphql.scalars.ExtendedScalars; +import graphql.schema.GraphQLScalarType; +import lombok.NoArgsConstructor; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +@NoArgsConstructor +public class GraphQLExtendedScalarsInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(final GenericApplicationContext applicationContext) { + final Collection enabledExtendedScalars = getEnabledExtendedScalars(applicationContext); + final Collection validScalarNames = new HashSet<>(); + ReflectionUtils.doWithFields(ExtendedScalars.class, scalarField -> { + if (Modifier.isPublic(scalarField.getModifiers()) && Modifier.isStatic(scalarField.getModifiers()) + && scalarField.getType().equals(GraphQLScalarType.class)) { + final GraphQLScalarType graphQLScalarType = (GraphQLScalarType) scalarField.get(null); + if (enabledExtendedScalars.contains(graphQLScalarType.getName())) { + applicationContext.registerBean( + graphQLScalarType.getName(), + GraphQLScalarType.class, + () -> graphQLScalarType + ); + } + validScalarNames.add(graphQLScalarType.getName()); + } + }); + verifyEnabledScalars(enabledExtendedScalars, validScalarNames); + } + + private void verifyEnabledScalars( + final Collection enabledExtendedScalars, + final Collection validScalarNames + ) { + final Collection invalidScalarNames = new HashSet<>(enabledExtendedScalars); + invalidScalarNames.removeAll(validScalarNames); + if (!invalidScalarNames.isEmpty()) { + throw new ApplicationContextException(String.format( + "Invalid extended scalar name(s) found: %s. Valid names are: %s.", + joinNames(invalidScalarNames), + joinNames(validScalarNames) + ) + ); + } + } + + private String joinNames(final Collection names) { + return names.stream().sorted().collect(Collectors.joining(", ")); + } + + @SuppressWarnings("unchecked") + private Set getEnabledExtendedScalars(final GenericApplicationContext applicationContext) { + return (Set) applicationContext.getEnvironment() + .getProperty("graphql.extended-scalars", Collection.class, Collections.emptySet()) + .stream().map(String::valueOf).collect(Collectors.toSet()); + } +} diff --git a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 24abccf4..ee1b8468 100644 --- a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -5,5 +5,10 @@ } ], "properties": [ + { + "name": "graphql.extended-scalars", + "type": "java.util.Set", + "description": "List of extended scalars to be used." + } ] } diff --git a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 8757f2b3..c92ca286 100644 --- a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,3 +1,5 @@ +org.springframework.context.ApplicationContextInitializer=\ +graphql.kickstart.spring.web.boot.GraphQLExtendedScalarsInitializer org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ graphql.kickstart.spring.web.boot.GraphQLWebAutoConfiguration,\ graphql.kickstart.spring.web.boot.GraphQLWebsocketAutoConfiguration,\ diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarAutoConfigurationTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarAutoConfigurationTest.java new file mode 100644 index 00000000..bcbfecc5 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarAutoConfigurationTest.java @@ -0,0 +1,38 @@ +package graphql.kickstart.spring.web.boot.test.extendedscalars; + +import graphql.scalars.ExtendedScalars; +import graphql.schema.GraphQLScalarType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.TestPropertySource; + +import java.util.AbstractMap; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = ExtendedScalarAutoConfigurationTest.ExtendedScalarsTestApplication.class +) +@TestPropertySource(properties = "graphql.extended-scalars=BigDecimal") +@DisplayName("Testing extended scalars auto configuration") +public class ExtendedScalarAutoConfigurationTest { + + @Autowired + private ApplicationContext applicationContext; + + @Test + @DisplayName("The extended scalars initializer should be properly picked up by Spring auto configuration.") + void testAutoConfiguration() { + assertThat(applicationContext.getBeansOfType(GraphQLScalarType.class)) + .containsOnly(new AbstractMap.SimpleEntry<>("BigDecimal", ExtendedScalars.GraphQLBigDecimal)); + } + + @SpringBootApplication + public static class ExtendedScalarsTestApplication { + } +} diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarsTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarsTest.java new file mode 100644 index 00000000..782210e0 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/web/boot/test/extendedscalars/ExtendedScalarsTest.java @@ -0,0 +1,75 @@ +package graphql.kickstart.spring.web.boot.test.extendedscalars; + +import graphql.kickstart.spring.web.boot.GraphQLExtendedScalarsInitializer; +import graphql.scalars.ExtendedScalars; +import graphql.schema.GraphQLScalarType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; + +import java.util.AbstractMap; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@DisplayName("Testing extended scalars configuration") +public class ExtendedScalarsTest { + + @Test + @DisplayName("Should throw exception at context initialization when invalid extended scalar name is provided.") + void shouldThrowErrorOnStartupIfExtendedScalarDoesNotExists() { + // GIVEN + final SpringApplication application = setupTestApplication("Long,Short,Datee,BadDecimal"); + // THEN + assertThatExceptionOfType(ApplicationContextException.class) + .isThrownBy(application::run) + .withMessage("Invalid extended scalar name(s) found: BadDecimal, Datee. Valid names are: BigDecimal, " + + "BigInteger, Byte, Char, Date, DateTime, JSON, Locale, Long, NegativeFloat, NegativeInt, " + + "NonNegativeFloat, NonNegativeInt, NonPositiveFloat, NonPositiveInt, Object, PositiveFloat, " + + "PositiveInt, Short, Time, Url."); + } + + @Test + @DisplayName("Should not create any extended scalars by default.") + void shouldNotDeclareAnyExtendedScalarsByDefault() { + // GIVEN + final SpringApplication application = setupTestApplication(null); + // WHEN + final ConfigurableApplicationContext context = application.run(); + // THEN + assertThat(context.getBeansOfType(GraphQLScalarType.class)).isEmpty(); + } + + @Test + @DisplayName("Should declare the configured extended scalars.") + void shouldDeclareTheConfiguredScalars() { + // GIVEN + final SpringApplication application = setupTestApplication("Long,Short,BigDecimal,Date"); + // WHEN + final ConfigurableApplicationContext context = application.run(); + // THEN + assertThat(context.getBeansOfType(GraphQLScalarType.class)) + .containsOnly( + new AbstractMap.SimpleEntry<>("Long", ExtendedScalars.GraphQLLong), + new AbstractMap.SimpleEntry<>("Short", ExtendedScalars.GraphQLShort), + new AbstractMap.SimpleEntry<>("BigDecimal", ExtendedScalars.GraphQLBigDecimal), + new AbstractMap.SimpleEntry<>("Date", ExtendedScalars.Date) + ); + } + + private SpringApplication setupTestApplication(final String extendedScalarValue) { + final StandardEnvironment standardEnvironment = new StandardEnvironment(); + standardEnvironment.getPropertySources().addFirst(new MapPropertySource("testProperties", + Collections.singletonMap("graphql.extended-scalars", extendedScalarValue))); + final SpringApplication application = new SpringApplication(GraphQLExtendedScalarsInitializer.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setEnvironment(standardEnvironment); + return application; + } +}