From 7f2aa00e21be8f63e83b6aed370c4a96749f9edd Mon Sep 17 00:00:00 2001 From: minux Date: Mon, 3 Jun 2024 10:03:10 +0900 Subject: [PATCH] Add GraphQL setter to GraphqlServiceBuilder (#5269) Motivation: A user might want to set `GraphQL` directly to the `GraphqlServiceBuilder` which could be shared between Expedia `GraphQLRequestHandler` and Armeria's `GraphqlService`. Modification: - Add the setter method for `GraphQL`. Result: - You can now set `GraphQL` to the `GraphqlServiceBuilder`. --- .../server/graphql/GraphqlConfigurator.java | 3 + .../server/graphql/GraphqlServiceBuilder.java | 255 +++++++++++++----- .../graphql/GraphqlServiceBuilderTest.java | 11 + 3 files changed, 200 insertions(+), 69 deletions(-) diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlConfigurator.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlConfigurator.java index 4f51697f7c7..1b2636eec0c 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlConfigurator.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlConfigurator.java @@ -22,9 +22,12 @@ /** * Interface used to configure the {@link GraphQL}. + * + * @deprecated Use {@link GraphQL.Builder} directly. */ @UnstableApi @FunctionalInterface +@Deprecated public interface GraphqlConfigurator { /** diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilder.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilder.java index b19bdf86fa2..d34f05c96b2 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilder.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilder.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.server.graphql; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static graphql.com.google.common.base.Preconditions.checkArgument; @@ -47,6 +48,7 @@ import com.linecorp.armeria.server.websocket.WebSocketServiceBuilder; import graphql.GraphQL; +import graphql.execution.ExecutionIdProvider; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; import graphql.schema.GraphQLSchema; @@ -67,29 +69,38 @@ public final class GraphqlServiceBuilder { private static final List DEFAULT_SCHEMA_FILE_NAMES = ImmutableList.of("schema.graphqls", "schema.graphql"); - private final ImmutableList.Builder schemaUrls = ImmutableList.builder(); - - private final ImmutableList.Builder runtimeWiringConfigurators = - ImmutableList.builder(); - private final ImmutableList.Builder typeVisitors = ImmutableList.builder(); - private final ImmutableList.Builder instrumentations = ImmutableList.builder(); - private final ImmutableList.Builder graphqlBuilderConsumers = - ImmutableList.builder(); @Nullable - private ImmutableList.Builder> dataLoaderRegistryConsumers; - private boolean useBlockingTaskExecutor; + private GraphQL graphql; + // Fields for building a graphql @Nullable - private Function dataLoaderRegistryFactory; + private ExecutionIdGenerator executionIdGenerator; + @Nullable + private ImmutableList.Builder instrumentations; + @Nullable + private ImmutableList.Builder graphqlBuilderConsumers; + // Fields for building a schema in a graphql @Nullable private GraphQLSchema schema; + @Nullable + private ImmutableList.Builder schemaUrls; + @Nullable + private ImmutableList.Builder runtimeWiringConfigurators; + @Nullable + private ImmutableList.Builder typeVisitors; + // Fields for building a DataLoaderRegistry @Nullable - private GraphqlErrorHandler errorHandler; + private ImmutableList.Builder> dataLoaderRegistryConsumers; + @Nullable + private Function dataLoaderRegistryFactory; - private ExecutionIdGenerator executionIdGenerator = ExecutionIdGenerator.of(); + // Others + private boolean useBlockingTaskExecutor; + @Nullable + private GraphqlErrorHandler errorHandler; private boolean enableWebSocket; @@ -98,10 +109,28 @@ public final class GraphqlServiceBuilder { GraphqlServiceBuilder() {} + /** + * Sets the {@link GraphQL}. + */ + public GraphqlServiceBuilder graphql(GraphQL graphql) { + checkState(executionIdGenerator == null && instrumentations == null && + graphqlBuilderConsumers == null && schema == null && + schemaUrls == null && runtimeWiringConfigurators == null && + typeVisitors == null, + "graphql() and setting properties for a GraphQL are mutually exclusive."); + this.graphql = requireNonNull(graphql, "graphql"); + return this; + } + /** * Adds the schema {@link File}s. * If not set, the {@code schema.graphql} or {@code schema.graphqls} will be imported from the resource. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can create a {@link TypeDefinitionRegistry} + * using {@link SchemaParser}, and then create {@link GraphQLSchema} using a + * {@link SchemaGenerator}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder schemaFile(File... schemaFiles) { return schemaFile(ImmutableList.copyOf(requireNonNull(schemaFiles, "schemaFiles"))); } @@ -109,7 +138,12 @@ public GraphqlServiceBuilder schemaFile(File... schemaFiles) { /** * Adds the schema {@link File}s. * If not set, the {@code schema.graphql} or {@code schema.graphqls} will be imported from the resource. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can create a {@link TypeDefinitionRegistry} + * using {@link SchemaParser}, and then create {@link GraphQLSchema} using a + * {@link SchemaGenerator}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder schemaFile(Iterable schemaFiles) { requireNonNull(schemaFiles, "schemaFiles"); return schemaUrls0(Streams.stream(schemaFiles) @@ -125,7 +159,12 @@ public GraphqlServiceBuilder schemaFile(Iterable schemaFiles) { /** * Adds the schema loaded from the given URLs. * If not set, the {@code schema.graphql} or {@code schema.graphqls} will be imported from the resource. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can create a {@link TypeDefinitionRegistry} + * using {@link SchemaParser}, and then create {@link GraphQLSchema} using a + * {@link SchemaGenerator}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder schemaUrls(String... schemaUrls) { return schemaUrls(ImmutableList.copyOf(requireNonNull(schemaUrls, "schemaUrls"))); } @@ -133,7 +172,12 @@ public GraphqlServiceBuilder schemaUrls(String... schemaUrls) { /** * Adds the schema loaded from the given URLs. * If not set, the {@code schema.graphql} or {@code schema.graphqls} will be imported from the resource. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can create a {@link TypeDefinitionRegistry} + * using {@link SchemaParser}, and then create {@link GraphQLSchema} using a + * {@link SchemaGenerator}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder schemaUrls(Iterable schemaUrls) { requireNonNull(schemaUrls, "schemaUrls"); return schemaUrls0(Streams.stream(schemaUrls) @@ -147,14 +191,23 @@ public GraphqlServiceBuilder schemaUrls(Iterable schemaUrls) { } private GraphqlServiceBuilder schemaUrls0(Iterable schemaUrls) { + checkState(graphql == null, "graphql() and schemaUrls() are mutually exclusive."); + if (this.schemaUrls == null) { + this.schemaUrls = ImmutableList.builder(); + } this.schemaUrls.addAll(requireNonNull(schemaUrls, "schemaUrls")); return this; } /** * Sets the {@link GraphQLSchema}. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. Set the {@link GraphQLSchema} to a + * {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder schema(GraphQLSchema schema) { + checkState(graphql == null, "graphql() and schema() are mutually exclusive."); this.schema = requireNonNull(schema, "schema"); return this; } @@ -201,7 +254,12 @@ public GraphqlServiceBuilder configureDataLoaderRegistry( /** * Adds the {@link RuntimeWiringConfigurator}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can specify a {@link RuntimeWiringConfigurator} + * to {@link SchemaGenerator#makeExecutableSchema(TypeDefinitionRegistry, RuntimeWiring)} + * when creating a {@link GraphQLSchema}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder runtimeWiring(RuntimeWiringConfigurator... runtimeWiringConfigurators) { requireNonNull(runtimeWiringConfigurators, "runtimeWiringConfigurators"); return runtimeWiring(ImmutableList.copyOf(runtimeWiringConfigurators)); @@ -209,30 +267,57 @@ public GraphqlServiceBuilder runtimeWiring(RuntimeWiringConfigurator... runtimeW /** * Adds the {@link RuntimeWiringConfigurator}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can specify a {@link RuntimeWiringConfigurator} + * to {@link SchemaGenerator#makeExecutableSchema(TypeDefinitionRegistry, RuntimeWiring)} + * when creating a {@link GraphQLSchema}. Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder runtimeWiring(Iterable configurators) { + checkState(graphql == null, "graphql() and runtimeWiring() are mutually exclusive."); + if (runtimeWiringConfigurators == null) { + runtimeWiringConfigurators = ImmutableList.builder(); + } runtimeWiringConfigurators.addAll(requireNonNull(configurators, "configurators")); return this; } /** * Adds the {@link GraphQLTypeVisitor}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can transform a {@link GraphQLSchema} + * using {@link SchemaTransformer#transformSchema(GraphQLSchema, GraphQLTypeVisitor)}. + * Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder typeVisitors(GraphQLTypeVisitor... typeVisitors) { return typeVisitors(ImmutableList.copyOf(requireNonNull(typeVisitors, "typeVisitors"))); } /** * Adds the {@link GraphQLTypeVisitor}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can transform a {@link GraphQLSchema} + * using {@link SchemaTransformer#transformSchema(GraphQLSchema, GraphQLTypeVisitor)}. + * Then, set it to a {@link GraphQL.Builder}. */ + @Deprecated public GraphqlServiceBuilder typeVisitors(Iterable typeVisitors) { + checkState(graphql == null, "graphql() and typeVisitors() are mutually exclusive."); + if (this.typeVisitors == null) { + this.typeVisitors = ImmutableList.builder(); + } this.typeVisitors.addAll(requireNonNull(typeVisitors, "typeVisitors")); return this; } /** * Adds the {@link Instrumentation}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can specify a {@link Instrumentation} to + * {@link GraphQL.Builder#instrumentation(Instrumentation)}. */ + @Deprecated public GraphqlServiceBuilder instrumentation(Instrumentation... instrumentations) { requireNonNull(instrumentations, "instrumentations"); return instrumentation(ImmutableList.copyOf(instrumentations)); @@ -240,25 +325,45 @@ public GraphqlServiceBuilder instrumentation(Instrumentation... instrumentations /** * Adds the {@link Instrumentation}s. + * + * @deprecated Use {@link #graphql(GraphQL)} instead. You can specify a {@link Instrumentation} to + * {@link GraphQL.Builder#instrumentation(Instrumentation)}. */ + @Deprecated public GraphqlServiceBuilder instrumentation(Iterable instrumentations) { - this.instrumentations.addAll(requireNonNull(instrumentations, "instrumentations")); + checkState(graphql == null, "graphql() and instrumentation() are mutually exclusive."); + requireNonNull(instrumentations, "instrumentations"); + if (this.instrumentations == null) { + this.instrumentations = ImmutableList.builder(); + } + this.instrumentations.addAll(instrumentations); return this; } /** * Adds the {@link GraphqlConfigurator} consumers. + * + * @deprecated Use {@link GraphQL.Builder} directly. */ + @Deprecated public GraphqlServiceBuilder configureGraphql(GraphqlConfigurator... configurers) { return configureGraphql(ImmutableList.copyOf(requireNonNull(configurers, "configurers"))); } /** * Adds the {@link GraphqlConfigurator} consumers. + * + * @deprecated Use {@link GraphQL.Builder} directly. */ + @Deprecated public GraphqlServiceBuilder configureGraphql( Iterable configurers) { - graphqlBuilderConsumers.addAll(requireNonNull(configurers, "configurers")); + checkState(graphql == null, "graphql() and configureGraphql() are mutually exclusive."); + requireNonNull(configurers, "configurers"); + if (graphqlBuilderConsumers == null) { + graphqlBuilderConsumers = ImmutableList.builder(); + } + graphqlBuilderConsumers.addAll(configurers); return this; } @@ -309,6 +414,7 @@ public GraphqlServiceBuilder errorHandler(GraphqlErrorHandler errorHandler) { * If not specified, {@link ExecutionIdGenerator#of()} is used by default. */ public GraphqlServiceBuilder executionIdGenerator(ExecutionIdGenerator executionIdGenerator) { + checkState(graphql == null, "graphql() and executionIdGenerator() are mutually exclusive."); this.executionIdGenerator = requireNonNull(executionIdGenerator, "executionIdGenerator"); return this; } @@ -319,33 +425,9 @@ public GraphqlServiceBuilder executionIdGenerator(ExecutionIdGenerator execution public GraphqlService build() { checkArgument(enableWebSocket || webSocketServiceCustomizer == null, "enableWebSocket must be true to customize WebSocketServiceBuilder"); - - final GraphQLSchema schema = buildSchema(); - GraphQL.Builder builder = GraphQL.newGraphQL(schema) - .executionIdProvider(executionIdGenerator.asExecutionProvider()); - final List instrumentations = this.instrumentations.build(); - if (!instrumentations.isEmpty()) { - builder = builder.instrumentation(new ChainedInstrumentation(instrumentations)); - } - - final List graphqlBuilders = graphqlBuilderConsumers.build(); - for (GraphqlConfigurator configurer : graphqlBuilders) { - configurer.configure(builder); - } - - Function dataLoaderRegistryFactory = null; - if (this.dataLoaderRegistryFactory != null) { - dataLoaderRegistryFactory = this.dataLoaderRegistryFactory; - } else if (dataLoaderRegistryConsumers != null) { - final DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); - for (Consumer configurer : dataLoaderRegistryConsumers.build()) { - configurer.accept(dataLoaderRegistry); - } - dataLoaderRegistryFactory = ctx -> dataLoaderRegistry; - } else { - assert dataLoaderRegistryFactory == null && dataLoaderRegistryConsumers == null; - dataLoaderRegistryFactory = ctx -> new DataLoaderRegistry(); - } + final GraphQL graphql = buildGraphql(); + final Function dataLoaderRegistryFactory = + buildDataLoaderRegistry(); final GraphqlErrorHandler errorHandler; if (this.errorHandler == null) { @@ -353,8 +435,7 @@ public GraphqlService build() { } else { errorHandler = this.errorHandler.orElse(GraphqlErrorHandler.of()); } - - final DefaultGraphqlService graphqlService = new DefaultGraphqlService(builder.build(), + final DefaultGraphqlService graphqlService = new DefaultGraphqlService(graphql, dataLoaderRegistryFactory, useBlockingTaskExecutor, errorHandler); @@ -366,41 +447,54 @@ public GraphqlService build() { } } - private GraphQLSchema buildSchema() { - final List schemaUrls = this.schemaUrls.build(); - final List runtimeWiringConfigurators = - this.runtimeWiringConfigurators.build(); - final List typeVisitors = this.typeVisitors.build(); + private GraphQL buildGraphql() { + if (graphql != null) { + return graphql; + } + final GraphQLSchema schema = buildSchema(); + final ExecutionIdProvider executionProvider = + firstNonNull(executionIdGenerator, ExecutionIdGenerator.of()).asExecutionProvider(); + + final GraphQL.Builder builder = GraphQL.newGraphQL(schema) + .executionIdProvider(executionProvider); + if (instrumentations != null) { + final List instrumentations = this.instrumentations.build(); + builder.instrumentation(new ChainedInstrumentation(instrumentations)); + } + if (graphqlBuilderConsumers != null) { + final List graphqlBuilders = graphqlBuilderConsumers.build(); + for (GraphqlConfigurator configurer : graphqlBuilders) { + configurer.configure(builder); + } + } + return builder.build(); + } + private GraphQLSchema buildSchema() { if (schema != null) { - checkState(schemaUrls.isEmpty() && runtimeWiringConfigurators.isEmpty() && - typeVisitors.isEmpty(), + checkState(schemaUrls == null && runtimeWiringConfigurators == null && + typeVisitors == null, "Cannot add schemaUrl(or File), runtimeWiringConfigurator and " + "typeVisitor when GraphqlSchema is specified."); return schema; } - final TypeDefinitionRegistry registry = typeDefinitionRegistry(schemaUrls); - final RuntimeWiring runtimeWiring = buildRuntimeWiring(runtimeWiringConfigurators); + final TypeDefinitionRegistry registry = typeDefinitionRegistry(); + final RuntimeWiring runtimeWiring = buildRuntimeWiring(); GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(registry, runtimeWiring); - for (GraphQLTypeVisitor typeVisitor : typeVisitors) { - schema = SchemaTransformer.transformSchema(schema, typeVisitor); + if (typeVisitors != null) { + for (GraphQLTypeVisitor typeVisitor : typeVisitors.build()) { + schema = SchemaTransformer.transformSchema(schema, typeVisitor); + } } return schema; } - private static TypeDefinitionRegistry typeDefinitionRegistry(List schemaUrls) { + private TypeDefinitionRegistry typeDefinitionRegistry() { final TypeDefinitionRegistry registry = new TypeDefinitionRegistry(); final SchemaParser parser = new SchemaParser(); - if (schemaUrls.isEmpty()) { - schemaUrls = defaultSchemaUrls(); - } - if (schemaUrls.isEmpty()) { - throw new IllegalStateException("Not found schema file(s)"); - } - - logger.info("Found schema files: {}", schemaUrls); - schemaUrls.forEach(url -> { + final List schemaUrlList = schemaUrls != null ? schemaUrls.build() : defaultSchemaUrls(); + schemaUrlList.forEach(url -> { try (InputStream inputStream = url.openStream()) { registry.merge(parser.parse(inputStream)); } catch (IOException e) { @@ -410,19 +504,42 @@ private static TypeDefinitionRegistry typeDefinitionRegistry(List schemaUrl return registry; } - private static RuntimeWiring buildRuntimeWiring( - List runtimeWiringConfigurators) { + private RuntimeWiring buildRuntimeWiring() { final RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); - runtimeWiringConfigurators.forEach(it -> it.configure(runtimeWiringBuilder)); + if (runtimeWiringConfigurators != null) { + runtimeWiringConfigurators.build().forEach(it -> it.configure(runtimeWiringBuilder)); + } return runtimeWiringBuilder.build(); } private static List defaultSchemaUrls() { final ClassLoader classLoader = GraphqlServiceBuilder.class.getClassLoader(); - return DEFAULT_SCHEMA_FILE_NAMES + final List schemaFiles = DEFAULT_SCHEMA_FILE_NAMES .stream() .map(classLoader::getResource) .filter(Objects::nonNull) .collect(toImmutableList()); + + if (schemaFiles.isEmpty()) { + throw new IllegalStateException("Not found schema file(s)"); + } + logger.info("Found schema files: {}", schemaFiles); + return schemaFiles; + } + + private Function buildDataLoaderRegistry() { + final Function dataLoaderRegistryFactory; + if (this.dataLoaderRegistryFactory != null) { + dataLoaderRegistryFactory = this.dataLoaderRegistryFactory; + } else if (dataLoaderRegistryConsumers != null) { + final DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + for (Consumer configurer : dataLoaderRegistryConsumers.build()) { + configurer.accept(dataLoaderRegistry); + } + dataLoaderRegistryFactory = ctx -> dataLoaderRegistry; + } else { + dataLoaderRegistryFactory = ctx -> new DataLoaderRegistry(); + } + return dataLoaderRegistryFactory; } } diff --git a/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilderTest.java b/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilderTest.java index e4944d51e39..369c28e4891 100644 --- a/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilderTest.java +++ b/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlServiceBuilderTest.java @@ -36,6 +36,8 @@ import com.google.common.collect.ImmutableList; +import graphql.GraphQL; +import graphql.GraphQL.Builder; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeVisitor; @@ -166,4 +168,13 @@ void bothDataLoaderConfig() { .isInstanceOf(IllegalStateException.class) .hasMessage("configureDataLoaderRegistry() and dataLoaderRegistry() are mutually exclusive."); } + + @Test + void graphqlAndSchemaCannotSetTogether() throws URISyntaxException { + final GraphQLSchema schema = makeGraphQLSchema(); + final GraphQL graphQL = new Builder(schema).build(); + assertThatThrownBy(() -> GraphqlService.builder().graphql(graphQL).schema(schema)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("are mutually exclusive."); + } }