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
This commit is contained in:
rstoyanchev
2022-01-14 18:44:54 +00:00
parent be74ba2b59
commit 2ecac0bd35
4 changed files with 183 additions and 8 deletions

View File

@@ -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<WiringFactory>`. 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`.

View File

@@ -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<WiringFactory> 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);
}
}

View File

@@ -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<WiringFactory> container) {
// no-op
}
}

View File

@@ -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<WiringFactory> 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<WiringFactory> 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;
}
}
}