From 2ecac0bd35c7d61bceb1feb5f5de8d2742a9c335 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 14 Jan 2022 18:44:54 +0000 Subject: [PATCH] Multiple WiringFactory registrations CombinedWiringFactory makes use of multiple WiringFactory instances possible, but RuntimeWiring.Builder does not use it by default. This change enables use of multiple WiringFactory instances through an extra callback on RuntimeWiringConfigurer that allows multiple parties to add their own WiringFactory. See gh-244 --- .../src/docs/asciidoc/index.adoc | 5 + .../DefaultGraphQlSourceBuilder.java | 28 +++- .../execution/RuntimeWiringConfigurer.java | 20 ++- .../DefaultGraphQlSourceBuilderTests.java | 138 ++++++++++++++++++ 4 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilderTests.java 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; + } + + } + +}