From e3e55c3a85136f309fcb45bb2c85ad73185888c9 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 2 Nov 2022 10:19:58 -0400 Subject: [PATCH] Add Java DSL for GraphQL (#3934) * Add Java DSL for GraphQL * GraphQl.outboundChannelAdapter() -> outboundGateway() * * Add `private` ctor to `GraphQl` * * Just `GraphQl.gateway()` - there is not going to be any other components to handle GraphQL operations. Also, the `outbound` in the name might confuse, where we, essentially, query a GraphQL data * Fix typo in the doc Co-authored-by: Gary Russell Co-authored-by: Gary Russell --- .../integration/graphql/dsl/GraphQl.java | 42 +++++++ .../dsl/GraphQlMessageHandlerSpec.java | 110 ++++++++++++++++++ .../outbound/GraphQlMessageHandler.java | 2 +- .../outbound/GraphQlMessageHandlerTests.java | 29 ++--- src/reference/asciidoc/graphql.adoc | 27 +++-- 5 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQl.java create mode 100644 spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQlMessageHandlerSpec.java diff --git a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQl.java b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQl.java new file mode 100644 index 0000000000..ba0c0aadc5 --- /dev/null +++ b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQl.java @@ -0,0 +1,42 @@ +/* + * Copyright 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.integration.graphql.dsl; + +import org.springframework.graphql.ExecutionGraphQlService; + +/** + * Factory class for GraphQL components DSL. + * + * @author Artem Bilan + * + * @since 6.0 + */ +public final class GraphQl { + + /** + * Create an instance of {@link GraphQlMessageHandlerSpec} for the provided {@link ExecutionGraphQlService}. + * @param graphQlService the {@link ExecutionGraphQlService} to use. + * @return the spec. + */ + public static GraphQlMessageHandlerSpec gateway(ExecutionGraphQlService graphQlService) { + return new GraphQlMessageHandlerSpec(graphQlService); + } + + private GraphQl() { + } + +} diff --git a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQlMessageHandlerSpec.java b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQlMessageHandlerSpec.java new file mode 100644 index 0000000000..52da3ac487 --- /dev/null +++ b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/dsl/GraphQlMessageHandlerSpec.java @@ -0,0 +1,110 @@ +/* + * Copyright 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.integration.graphql.dsl; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.expression.Expression; +import org.springframework.graphql.ExecutionGraphQlService; +import org.springframework.integration.dsl.MessageHandlerSpec; +import org.springframework.integration.expression.FunctionExpression; +import org.springframework.integration.graphql.outbound.GraphQlMessageHandler; +import org.springframework.messaging.Message; + +/** + * The {@link MessageHandlerSpec} for {@link GraphQlMessageHandler}. + * + * @author Artem Bilan + * + * @since 6.0 + */ +public class GraphQlMessageHandlerSpec extends MessageHandlerSpec { + + protected GraphQlMessageHandlerSpec(ExecutionGraphQlService graphQlService) { + this.target = new GraphQlMessageHandler(graphQlService); + } + + public GraphQlMessageHandlerSpec operation(String operation) { + this.target.setOperation(operation); + return this; + } + + public GraphQlMessageHandlerSpec operation(Function, String> operationFunction) { + return operationExpression(new FunctionExpression<>(operationFunction)); + } + + public GraphQlMessageHandlerSpec operationExpression(String operationExpression) { + return operationExpression(PARSER.parseExpression(operationExpression)); + } + + public GraphQlMessageHandlerSpec operationExpression(Expression operationExpression) { + this.target.setOperationExpression(operationExpression); + return this; + } + + public GraphQlMessageHandlerSpec operationName(String operationName) { + this.target.setOperationName(operationName); + return this; + } + + public GraphQlMessageHandlerSpec operationName(Function, String> operationNameFunction) { + return operationNameExpression(new FunctionExpression<>(operationNameFunction)); + } + + public GraphQlMessageHandlerSpec operationNameExpression(String operationNameExpression) { + return operationNameExpression(PARSER.parseExpression(operationNameExpression)); + } + + public GraphQlMessageHandlerSpec operationNameExpression(Expression operationNameExpression) { + this.target.setOperationNameExpression(operationNameExpression); + return this; + } + + public GraphQlMessageHandlerSpec variables(Function, Map> variablesFunction) { + return variablesExpression(new FunctionExpression<>(variablesFunction)); + } + + public GraphQlMessageHandlerSpec variablesExpression(String variablesExpression) { + return variablesExpression(PARSER.parseExpression(variablesExpression)); + } + + public GraphQlMessageHandlerSpec variablesExpression(Expression variablesExpression) { + this.target.setVariablesExpression(variablesExpression); + return this; + } + + public GraphQlMessageHandlerSpec locale(Locale locale) { + this.target.setLocale(locale); + return this; + } + + public GraphQlMessageHandlerSpec executionId(Function, String> executionIdExpression) { + return executionIdExpression(new FunctionExpression<>(executionIdExpression)); + } + + public GraphQlMessageHandlerSpec executionIdExpression(String executionIdExpression) { + return executionIdExpression(PARSER.parseExpression(executionIdExpression)); + } + + public GraphQlMessageHandlerSpec executionIdExpression(Expression executionIdExpression) { + this.target.setExecutionIdExpression(executionIdExpression); + return this; + } + +} diff --git a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java index 7aa156a96c..05b1fc5d2e 100644 --- a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java +++ b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java @@ -61,7 +61,7 @@ public class GraphQlMessageHandler extends AbstractReplyProducingMessageHandler private Expression executionIdExpression = new FunctionExpression>(message -> message.getHeaders().getId()); - public GraphQlMessageHandler(final ExecutionGraphQlService graphQlService) { + public GraphQlMessageHandler(ExecutionGraphQlService graphQlService) { Assert.notNull(graphQlService, "'graphQlService' must not be null"); this.graphQlService = graphQlService; setAsync(true); diff --git a/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java b/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java index 4ba410bbb7..3c6e4a31f0 100644 --- a/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java +++ b/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java @@ -45,6 +45,7 @@ import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.MessageChannels; +import org.springframework.integration.graphql.dsl.GraphQl; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.PollableChannel; @@ -121,8 +122,8 @@ public class GraphQlMessageHandlerTests { Locale locale = Locale.getDefault(); this.graphQlMessageHandler.setLocale(locale); - Mono resultMono = - (Mono) this.graphQlMessageHandler.handleRequestMessage(new GenericMessage<>(fakeQuery)); + var resultMono = (Mono) this.graphQlMessageHandler.handleRequestMessage( + new GenericMessage<>(fakeQuery)); StepVerifier.create(resultMono) .consumeNextWith(result -> { assertThat(result).isInstanceOf(ExecutionGraphQlResponse.class); @@ -243,18 +244,9 @@ public class GraphQlMessageHandlerTests { @SubscriptionMapping public Flux results() { - return Flux.just( - new QueryResult("test-data-01"), - new QueryResult("test-data-02"), - new QueryResult("test-data-03"), - new QueryResult("test-data-04"), - new QueryResult("test-data-05"), - new QueryResult("test-data-06"), - new QueryResult("test-data-07"), - new QueryResult("test-data-08"), - new QueryResult("test-data-09"), - new QueryResult("test-data-10") - ); + return Flux.range(1, 10) + .map(d -> String.format("test-data-%02d", d)) + .map(QueryResult::new); } } @@ -280,14 +272,9 @@ public class GraphQlMessageHandlerTests { static class TestConfig { @Bean - GraphQlMessageHandler handler(ExecutionGraphQlService graphQlService) { - return new GraphQlMessageHandler(graphQlService); - } - - @Bean - IntegrationFlow graphqlQueryMessageHandlerFlow(GraphQlMessageHandler handler) { + IntegrationFlow graphqlQueryMessageHandlerFlow(ExecutionGraphQlService graphQlService) { return IntegrationFlow.from(MessageChannels.flux("inputChannel")) - .handle(handler) + .handle(GraphQl.gateway(graphQlService)) .channel(c -> c.flux("resultChannel")) .get(); } diff --git a/src/reference/asciidoc/graphql.adoc b/src/reference/asciidoc/graphql.adoc index 592a01f74b..f5c17949dd 100644 --- a/src/reference/asciidoc/graphql.adoc +++ b/src/reference/asciidoc/graphql.adoc @@ -44,19 +44,18 @@ See documentation for the `ExecutionGraphQlResponse` how to process the GraphQL [source, java] ---- @Bean -GraphQlMessageHandler handler(ExecutionGraphQlService graphQlService) { - GraphQlMessageHandler graphQlMessageHandler = new GraphQlMessageHandler(graphQlService); - graphQlMessageHandler.setOperation(""" - query HeroNameAndFriends($episode: Episode) { - hero(episode: $episode) { - name - friends { - name - } - } - }"""); - graphQlMessageHandler.setVariablesExpression(new SpelExpressionParser().parseExpression("{episode:'JEDI'}")); - return graphQlMessageHandler; +GraphQlMessageHandlerSpec graphQlMessageHandlerSpec(ExecutionGraphQlService graphQlService) { + return GraphQl.gateway(graphQlService) + .operation(""" + query HeroNameAndFriends($episode: Episode) { + hero(episode: $episode) { + name + friends { + name + } + } + }""") + .variablesExpression("{episode:'JEDI'}"); } @Bean @@ -102,4 +101,4 @@ public SubscriptionPublisher obtainSubscriptionResult(RequestOutput output) { ==== Such an outbound gateway can be used not only for GraphQL request via HTTP, but from any upstream endpoint which produces or carries a GraphQL operation or its arguments in the message. -The result of the `GraphQlMessageHandler` handling can be produces as a reply to the upstream request or sent downstream for further processing in the integration flow. \ No newline at end of file +The result of the `GraphQlMessageHandler` handling can be produced as a reply to the upstream request or sent downstream for further processing in the integration flow.