diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index e36b857d..d39b2928 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -213,6 +213,11 @@ You can use `RuntimeWiringConfigurer` to register: `AnnotatedControllerConfigurer`, which detects annotated, `DataFetcher` handler methods. The Spring Boot starter adds the `AnnotatedControllerConfigurer` by default. +If you need to add a `WiringFactory`, e.g. to make registrations that take into account +schema definitions, implement the alternative `configure` method that accepts both the +`RuntimeWiring.Builder` and an output `List`. This allows you to add any +number of factories that are then invoked in sequence. + The Spring for GraphQL Boot starter detects beans of type `RuntimeWiringConfigurer`. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index 2d492754..5a3b2185 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -36,10 +36,13 @@ import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeVisitor; import graphql.schema.SchemaTraverser; import graphql.schema.TypeResolver; +import graphql.schema.idl.CombinedWiringFactory; +import graphql.schema.idl.NoopWiringFactory; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.schema.idl.WiringFactory; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; @@ -130,9 +133,7 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { .map(this::parseSchemaResource).reduce(TypeDefinitionRegistry::merge) .orElseThrow(MissingSchemaException::new); - RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); - this.runtimeWiringConfigurers.forEach(configurer -> configurer.configure(runtimeWiringBuilder)); - RuntimeWiring runtimeWiring = runtimeWiringBuilder.build(); + RuntimeWiring runtimeWiring = initRuntimeWiring(); registerDefaultTypeResolver(registry, runtimeWiring); @@ -154,6 +155,23 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { return new CachedGraphQlSource(graphQl, schema); } + private RuntimeWiring initRuntimeWiring() { + RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring(); + this.runtimeWiringConfigurers.forEach(configurer -> configurer.configure(builder)); + + List factories = new ArrayList<>(); + WiringFactory factory = builder.build().getWiringFactory(); + if (!factory.getClass().equals(NoopWiringFactory.class)) { + factories.add(factory); + } + this.runtimeWiringConfigurers.forEach(configurer -> configurer.configure(builder, factories)); + if (!factories.isEmpty()) { + builder.wiringFactory(new CombinedWiringFactory(factories)); + } + + return builder.build(); + } + private void registerDefaultTypeResolver(TypeDefinitionRegistry registry, RuntimeWiring runtimeWiring) { TypeResolver typeResolver = (this.defaultTypeResolver != null ? this.defaultTypeResolver : new ClassNameTypeResolver()); @@ -171,7 +189,7 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { } } catch (IOException ex) { - throw new IllegalArgumentException("Failed to load schema resource: " + schemaResource.toString()); + throw new IllegalArgumentException("Failed to load schema resource: " + schemaResource); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/RuntimeWiringConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/RuntimeWiringConfigurer.java index b379f61c..e45ce931 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/RuntimeWiringConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/RuntimeWiringConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -15,11 +15,14 @@ */ package org.springframework.graphql.execution; +import java.util.List; + import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.WiringFactory; /** - * Component used to apply changes to the {@link RuntimeWiring.Builder} instance - * used in {@link GraphQlSource.Builder}. + * Callbacks that allow applying changes to the {@link RuntimeWiring.Builder} + * in {@link GraphQlSource.Builder}. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -33,4 +36,15 @@ public interface RuntimeWiringConfigurer { */ void configure(RuntimeWiring.Builder builder); + /** + * Variant of {@link #configure(RuntimeWiring.Builder)} that also collects + * {@link WiringFactory} instances that are then combined as one via + * {@link graphql.schema.idl.CombinedWiringFactory}. + * @param builder the builder to configure + * @param container the list of configured factories to add or insert into + */ + default void configure(RuntimeWiring.Builder builder, List container) { + // no-op + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilderTests.java new file mode 100644 index 00000000..19fd6a30 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilderTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2022 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.graphql.execution; + +import java.util.List; + +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.FieldWiringEnvironment; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.WiringFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.graphql.GraphQlSetup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link DefaultGraphQlSourceBuilder}. + * + * @author Rossen Stoyanchev + */ +public class DefaultGraphQlSourceBuilderTests { + + @Test + void wiringFactoryList() { + + String schemaContent = "type Query {" + + " q1: String" + + " q2: String" + + "}"; + + DataFetcher dataFetcher1 = mock(DataFetcher.class); + DataFetcher dataFetcher2 = mock(DataFetcher.class); + + RuntimeWiringConfigurer configurer = new RuntimeWiringConfigurer() { + + @Override + public void configure(RuntimeWiring.Builder builder) { + } + + @Override + public void configure(RuntimeWiring.Builder builder, List container) { + container.add(new DataFetcherWiringFactory("q1", dataFetcher1)); + container.add(new DataFetcherWiringFactory("q2", dataFetcher2)); + } + }; + + GraphQLSchema schema = GraphQlSetup.schemaContent(schemaContent) + .runtimeWiring(configurer) + .toGraphQlSource() + .schema(); + + assertThat(getDataFetcherForQuery(schema, "q1")).isSameAs(dataFetcher1); + assertThat(getDataFetcherForQuery(schema, "q2")).isSameAs(dataFetcher2); + } + + @Test + void wiringFactoryListAndBuilderWiringFactory() { + + String schemaContent = "type Query {" + + " q1: String" + + " q2: String" + + "}"; + + DataFetcher dataFetcher1 = mock(DataFetcher.class); + DataFetcher dataFetcher2 = mock(DataFetcher.class); + + RuntimeWiringConfigurer configurer = new RuntimeWiringConfigurer() { + + @Override + public void configure(RuntimeWiring.Builder builder) { + builder.wiringFactory(new DataFetcherWiringFactory("q1", dataFetcher1)); + } + + @Override + public void configure(RuntimeWiring.Builder builder, List container) { + container.add(new DataFetcherWiringFactory("q2", dataFetcher2)); + } + }; + + GraphQLSchema schema = GraphQlSetup.schemaContent(schemaContent) + .runtimeWiring(configurer) + .toGraphQlSource() + .schema(); + + assertThat(getDataFetcherForQuery(schema, "q1")).isSameAs(dataFetcher1); + assertThat(getDataFetcherForQuery(schema, "q2")).isSameAs(dataFetcher2); + } + + private DataFetcher getDataFetcherForQuery(GraphQLSchema schema, String query) { + FieldCoordinates coordinates = FieldCoordinates.coordinates("Query", query); + GraphQLFieldDefinition fieldDefinition = schema.getFieldDefinition(coordinates); + return schema.getCodeRegistry().getDataFetcher(coordinates, fieldDefinition); + } + + + private static class DataFetcherWiringFactory implements WiringFactory { + + private final String queryName; + + private final DataFetcher dataFetcher; + + DataFetcherWiringFactory(String queryName, DataFetcher dataFetcher) { + this.queryName = queryName; + this.dataFetcher = dataFetcher; + } + + @Override + public boolean providesDataFetcher(FieldWiringEnvironment environment) { + return (environment.getParentType().getName().equals("Query") && + environment.getFieldDefinition().getName().equals(this.queryName)); + } + + @Override + public DataFetcher getDataFetcher(FieldWiringEnvironment environment) { + return this.dataFetcher; + } + + } + +}