Auto-configure Spring GraphQL base infrastructure

This commit adds the auto-configuration for setting up the base Spring
GraphQL infrastructure. Because GraphQL doesn't depend on any particular
transport, we must have a separate configuration for creating:

* the `GraphQlSource`, which holds the schema and the `GraphQL` instance
* the `GraphQlService` for executing incoming requests
* the `BatchLoaderRegistry` for batch loading support
* the `AnnotatedControllerConfigurer` for supporting the annotated
  controllers programming model

This comes with a starting point for the `"spring.graphql.*"`
configuration properties; we can now configure the locations and file
extensions of GraphQL schema files we should load and configure at
startup.

See gh-29140
pull/29177/head
Brian Clozel 3 years ago
parent e5e157528b
commit de808834f5

@ -174,6 +174,7 @@ public class DocumentConfigurationProperties extends DefaultTask {
}
private void webPrefixes(Config prefix) {
prefix.accept("spring.graphql");
prefix.accept("spring.hateoas");
prefix.accept("spring.http");
prefix.accept("spring.servlet");

@ -187,6 +187,7 @@ dependencies {
optional("org.springframework.data:spring-data-neo4j")
optional("org.springframework.data:spring-data-r2dbc")
optional("org.springframework.data:spring-data-redis")
optional("org.springframework.graphql:spring-graphql")
optional("org.springframework.hateoas:spring-hateoas")
optional("org.springframework.security:spring-security-acl")
optional("org.springframework.security:spring-security-config")

@ -0,0 +1,130 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.execution.instrumentation.Instrumentation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.MissingSchemaException;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base
* infrastructure.
*
* @author Brian Clozel
* @since 2.7.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
@EnableConfigurationProperties(GraphQlProperties.class)
public class GraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
private final BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
@Bean
@ConditionalOnMissingBean
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
ObjectProvider<Instrumentation> instrumentationsProvider,
ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers,
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers) {
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver,
properties.getSchema().getLocations(), properties.getSchema().getFileExtensions());
GraphQlSource.Builder builder = GraphQlSource.builder()
.schemaResources(schemaResources.toArray(new Resource[0]))
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
try {
return builder.build();
}
catch (MissingSchemaException exc) {
throw new InvalidSchemaLocationsException(properties.getSchema().getLocations(), resourcePatternResolver,
exc);
}
}
@Bean
@ConditionalOnMissingBean
public BatchLoaderRegistry batchLoaderRegistry() {
return this.batchLoaderRegistry;
}
@Bean
@ConditionalOnMissingBean
public GraphQlService graphQlService(GraphQlSource graphQlSource) {
ExecutionGraphQlService service = new ExecutionGraphQlService(graphQlSource);
service.addDataLoaderRegistrar(this.batchLoaderRegistry);
return service;
}
@Bean
@ConditionalOnMissingBean
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
annotatedControllerConfigurer.setConversionService(new DefaultFormattingConversionService());
return annotatedControllerConfigurer;
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] schemaLocations,
String[] fileExtensions) {
List<Resource> schemaResources = new ArrayList<>();
for (String location : schemaLocations) {
for (String extension : fileExtensions) {
String resourcePattern = location + "*" + extension;
try {
schemaResources.addAll(Arrays.asList(resolver.getResources(resourcePattern)));
}
catch (IOException ex) {
logger.debug("Could not resolve schema location: '" + resourcePattern + "'", ex);
}
}
}
return schemaResources;
}
}

@ -0,0 +1,73 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for Spring GraphQL.
*
* @author Brian Clozel
* @since 2.7.0
*/
@ConfigurationProperties(prefix = "spring.graphql")
public class GraphQlProperties {
private final Schema schema = new Schema();
public Schema getSchema() {
return this.schema;
}
public static class Schema {
/**
* Locations of GraphQL schema files.
*/
private String[] locations = new String[] { "classpath:graphql/**/" };
/**
* File extensions for GraphQL schema files.
*/
private String[] fileExtensions = new String[] { ".graphqls", ".gqls" };
public String[] getLocations() {
return this.locations;
}
public void setLocations(String[] locations) {
this.locations = appendSlashIfNecessary(locations);
}
public String[] getFileExtensions() {
return this.fileExtensions;
}
public void setFileExtensions(String[] fileExtensions) {
this.fileExtensions = fileExtensions;
}
private String[] appendSlashIfNecessary(String[] locations) {
return Arrays.stream(locations).map((location) -> location.endsWith("/") ? location : location + "/")
.toArray(String[]::new);
}
}
}

@ -0,0 +1,38 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import org.springframework.graphql.execution.GraphQlSource;
/**
* Callback interface that can be implemented by beans wishing to customize properties of
* {@link org.springframework.graphql.execution.GraphQlSource.Builder} whilst retaining
* default auto-configuration.
*
* @author Rossen Stoyanchev
* @since 2.7.0
*/
@FunctionalInterface
public interface GraphQlSourceBuilderCustomizer {
/**
* Customize the {@link GraphQlSource.Builder} instance.
* @param builder builder the builder to customize
*/
void customize(GraphQlSource.Builder builder);
}

@ -0,0 +1,101 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.NestedRuntimeException;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
/**
* {@link InvalidSchemaLocationsException} thrown when no schema file could be found in
* the provided locations.
*
* @author Brian Clozel
* @since 2.7.0
*/
public class InvalidSchemaLocationsException extends NestedRuntimeException {
private final List<SchemaLocation> schemaLocations;
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver) {
this(locations, resolver, null);
}
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver, Throwable cause) {
super("No schema file could be found in the provided locations.", cause);
Assert.notEmpty(locations, "locations should not be empty");
Assert.notNull(resolver, "resolver should not be null");
List<SchemaLocation> providedLocations = new ArrayList<>();
for (String location : locations) {
try {
String uri = resolver.getResource(location).getURI().toASCIIString();
providedLocations.add(new SchemaLocation(location, uri));
}
catch (IOException ex) {
providedLocations.add(new SchemaLocation(location, ""));
}
}
this.schemaLocations = Collections.unmodifiableList(providedLocations);
}
/**
* Return the list of provided locations where to look for schemas.
* @return the list of locations
*/
public List<SchemaLocation> getSchemaLocations() {
return this.schemaLocations;
}
/**
* The location where to look for schemas.
*/
public static class SchemaLocation {
private final String location;
private final String uri;
SchemaLocation(String location, String uri) {
this.location = location;
this.uri = uri;
}
/**
* Return the location String to be resolved by a {@link ResourcePatternResolver}.
* @return the location
*/
public String getLocation() {
return this.location;
}
/**
* Return the resolved URI String for this location, an empty String if resolution
* failed.
* @return the resolved location or an empty String
*/
public String getUri() {
return this.uri;
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql;

@ -951,6 +951,14 @@
"level": "error"
}
},
{
"name": "spring.graphql.schema.locations",
"defaultValue": "classpath:graphql/**/"
},
{
"name": "spring.graphql.schema.file-extensions",
"defaultValue": ".graphqls,.gqls"
},
{
"name": "spring.groovy.template.prefix",
"defaultValue": ""

@ -68,6 +68,7 @@ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\

@ -0,0 +1,230 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import graphql.GraphQL;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.MissingSchemaException;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlAutoConfiguration}.
*/
class GraphQlAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class));
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context).hasSingleBean(GraphQlService.class);
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
});
}
@Test
void schemaShouldScanNestedFolders() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getObjectType("Book")).isNotNull();
});
}
@Test
void shouldFailWhenSchemaFileIsMissing() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/").run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().getRootCause().isInstanceOf(MissingSchemaException.class);
});
}
@Test
void shouldUseProgrammaticallyDefinedBuilder() {
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
assertThat(context).hasBean("customGraphQlSourceBuilder");
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
});
}
@Test
void shouldScanLocationsWithCustomExtension() {
this.contextRunner.withPropertyValues("spring.graphql.schema.file-extensions:.graphqls,.custom")
.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getObjectType("Book")).isNotNull();
assertThat(schema.getObjectType("Person")).isNotNull();
});
}
@Test
void shouldBackOffWithCustomGraphQlSource() {
this.contextRunner.withUserConfiguration(CustomGraphQlSourceConfiguration.class).run((context) -> {
assertThat(context).getBeanNames(GraphQlSource.class).containsOnly("customGraphQlSource");
assertThat(context).hasSingleBean(GraphQlProperties.class);
});
}
@Test
void shouldConfigureDataFetcherExceptionResolvers() {
this.contextRunner.withUserConfiguration(DataFetcherExceptionResolverConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQL graphQL = graphQlSource.graphQl();
assertThat(graphQL.getQueryStrategy()).extracting("dataFetcherExceptionHandler")
.satisfies((exceptionHandler) -> assertThat(exceptionHandler.getClass().getName())
.endsWith("ExceptionResolversExceptionHandler"));
});
}
@Test
void shouldConfigureInstrumentation() {
this.contextRunner.withUserConfiguration(InstrumentationConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
Instrumentation customInstrumentation = context.getBean("customInstrumentation", Instrumentation.class);
GraphQL graphQL = graphQlSource.graphQl();
assertThat(graphQL).extracting("instrumentation").isInstanceOf(ChainedInstrumentation.class)
.extracting("instrumentations", InstanceOfAssertFactories.iterable(Instrumentation.class))
.contains(customInstrumentation);
});
}
@Test
void shouldApplyRuntimeWiringConfigurers() {
this.contextRunner.withUserConfiguration(RuntimeWiringConfigurerConfiguration.class).run((context) -> {
RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer configurer = context
.getBean(RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer.class);
assertThat(configurer.applied).isTrue();
});
}
@Test
void shouldApplyGraphQlSourceBuilderCustomizer() {
this.contextRunner.withUserConfiguration(GraphQlSourceBuilderCustomizerConfiguration.class).run((context) -> {
GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer customizer = context
.getBean(GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer.class);
assertThat(customizer.applied).isTrue();
});
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {
@Bean
GraphQlSource.Builder customGraphQlSourceBuilder() {
return GraphQlSource.builder().schemaResources(new ClassPathResource("graphql/schema.graphqls"),
new ClassPathResource("graphql/types/book.graphqls"));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlSourceConfiguration {
@Bean
GraphQlSource customGraphQlSource() {
return mock(GraphQlSource.class);
}
}
@Configuration(proxyBeanMethods = false)
static class DataFetcherExceptionResolverConfiguration {
@Bean
DataFetcherExceptionResolver customDataFetcherExceptionResolver() {
return mock(DataFetcherExceptionResolver.class);
}
}
@Configuration(proxyBeanMethods = false)
static class InstrumentationConfiguration {
@Bean
Instrumentation customInstrumentation() {
return mock(Instrumentation.class);
}
}
@Configuration(proxyBeanMethods = false)
static class RuntimeWiringConfigurerConfiguration {
@Bean
CustomRuntimeWiringConfigurer customRuntimeWiringConfigurer() {
return new CustomRuntimeWiringConfigurer();
}
public static class CustomRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
public boolean applied = false;
@Override
public void configure(RuntimeWiring.Builder builder) {
this.applied = true;
}
}
}
static class GraphQlSourceBuilderCustomizerConfiguration {
@Bean
CustomGraphQlSourceBuilderCustomizer customGraphQlSourceBuilderCustomizer() {
return new CustomGraphQlSourceBuilderCustomizer();
}
public static class CustomGraphQlSourceBuilderCustomizer implements GraphQlSourceBuilderCustomizer {
public boolean applied = false;
@Override
public void customize(GraphQlSource.Builder builder) {
this.applied = true;
}
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link InvalidSchemaLocationsException}.
*
* @author Brian Clozel
*/
class InvalidSchemaLocationsExceptionTests {
private final String schemaFolder = "graphql/";
private final String[] locations = new String[] { "classpath:" + this.schemaFolder };
@Test
void shouldRejectEmptyLocations() {
assertThatIllegalArgumentException().isThrownBy(
() -> new InvalidSchemaLocationsException(new String[] {}, new PathMatchingResourcePatternResolver()))
.isInstanceOf(IllegalArgumentException.class).withMessage("locations should not be empty");
}
@Test
void shouldRejectNullResolver() {
assertThatIllegalArgumentException().isThrownBy(() -> new InvalidSchemaLocationsException(this.locations, null))
.isInstanceOf(IllegalArgumentException.class).withMessage("resolver should not be null");
}
@Test
void shouldExposeConfiguredLocations() {
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.locations,
new PathMatchingResourcePatternResolver());
assertThat(exception.getSchemaLocations()).hasSize(1);
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
assertThat(schemaLocation.getLocation()).isEqualTo(this.locations[0]);
assertThat(schemaLocation.getUri()).endsWith(this.schemaFolder);
}
@Test
void shouldNotFailWithUnresolvableLocations() {
String unresolved = "classpath:unresolved/";
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(new String[] { unresolved },
new PathMatchingResourcePatternResolver());
assertThat(exception.getSchemaLocations()).hasSize(1);
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
assertThat(schemaLocation.getLocation()).isEqualTo(unresolved);
assertThat(schemaLocation.getUri()).isEmpty();
}
}

@ -0,0 +1,4 @@
type Query {
greeting(name: String! = "Spring"): String!
bookById(id: ID): Book
}

@ -0,0 +1,6 @@
type Book {
id: ID
name: String
pageCount: Int
author: String
}
Loading…
Cancel
Save