diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java index de902c87413..504ab540b26 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java @@ -41,6 +41,8 @@ public class GraphQlProperties { private final Websocket websocket = new Websocket(); + private final Rsocket rsocket = new Rsocket(); + public Graphiql getGraphiql() { return this.graphiql; } @@ -61,6 +63,10 @@ public class GraphQlProperties { return this.websocket; } + public Rsocket getRsocket() { + return this.rsocket; + } + public static class Schema { /** @@ -204,4 +210,21 @@ public class GraphQlProperties { } + public static class Rsocket { + + /** + * Mapping of the RSocket message handler. + */ + private String mapping; + + public String getMapping() { + return this.mapping; + } + + public void setMapping(String mapping) { + this.mapping = mapping; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfiguration.java new file mode 100644 index 00000000000..fc449947701 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.rsocket; + +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.GraphQL; +import io.rsocket.core.RSocketServer; +import reactor.netty.http.server.HttpServer; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.graphql.ExecutionGraphQlService; +import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; +import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.server.GraphQlRSocketHandler; +import org.springframework.graphql.server.RSocketGraphQlInterceptor; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over + * RSocket. + * + * @author Brian Clozel + * @since 2.7.0 + */ +@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class }) +@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class }) +@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class }) +@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping") +public class GraphQlRSocketAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService, + ObjectProvider interceptorsProvider, ObjectMapper objectMapper) { + List interceptors = interceptorsProvider.orderedStream() + .collect(Collectors.toList()); + return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper)); + } + + @Bean + @ConditionalOnMissingBean + public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) { + return new GraphQlRSocketController(handler); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketController.java new file mode 100644 index 00000000000..6a416901d22 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketController.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.rsocket; + +import java.util.Map; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.server.GraphQlRSocketHandler; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; + +@Controller +class GraphQlRSocketController { + + private final GraphQlRSocketHandler handler; + + GraphQlRSocketController(GraphQlRSocketHandler handler) { + this.handler = handler; + } + + @MessageMapping("${spring.graphql.rsocket.mapping}") + Mono> handle(Map payload) { + return this.handler.handle(payload); + } + + @MessageMapping("${spring.graphql.rsocket.mapping}") + Flux> handleSubscription(Map payload) { + return this.handler.handleSubscription(payload); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/package-info.java new file mode 100644 index 00000000000..0beb3257113 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020-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. + */ + +/** + * Auto-configuration classes for RSocket integration with GraphQL. + */ +package org.springframework.boot.autoconfigure.graphql.rsocket; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfigurationTests.java new file mode 100644 index 00000000000..827a47e4955 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfigurationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.rsocket; + +import java.net.URI; +import java.time.Duration; +import java.util.function.Consumer; + +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; +import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.embedded.netty.NettyRouteProvider; +import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.client.RSocketGraphQlClient; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.server.GraphQlRSocketHandler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GraphQlRSocketAutoConfiguration} + * + * @author Brian Clozel + */ +class GraphQlRSocketAutoConfigurationTests { + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class, + GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class) + .withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql"); + + @Test + void shouldContributeDefaultBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class) + .hasSingleBean(GraphQlRSocketController.class)); + } + + @Test + void simpleQueryShouldWorkWithTcpServer() { + testWithRSocketTcp(this::assertThatSimpleQueryWorks); + } + + @Test + void simpleQueryShouldWorkWithWebSocketServer() { + testWithRSocketWebSocket(this::assertThatSimpleQueryWorks); + } + + private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) { + String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }"; + String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class) + .block(Duration.ofSeconds(5)); + assertThat(bookName).isEqualTo("GraphQL for beginners"); + } + + private void testWithRSocketTcp(Consumer consumer) { + ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class, + GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues( + "spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql"); + contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer()) + .withPropertyValues("spring.rsocket.server.port=0").run((context) -> { + String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port"); + RSocketGraphQlClient client = RSocketGraphQlClient.builder() + .tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build(); + consumer.accept(client); + }); + } + + private void testWithRSocketWebSocket(Consumer consumer) { + ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner( + AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration( + AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, + ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, + JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class, + GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class)) + .withInitializer(new ServerPortInfoApplicationContextInitializer()) + .withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class) + .withPropertyValues("spring.main.web-application-type=reactive", "server.port=0", + "spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket"); + contextRunner.run((context) -> { + String serverPort = context.getEnvironment().getProperty("local.server.port"); + RSocketGraphQlClient client = RSocketGraphQlClient.builder() + .webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build(); + consumer.accept(client); + }); + } + + @Configuration(proxyBeanMethods = false) + static class NettyServerConfiguration { + + @Bean + NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) { + NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0); + serverFactory.addRouteProviders(routeProvider); + return serverFactory; + } + + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + RuntimeWiringConfigurer bookDataFetcher() { + return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", + GraphQlTestDataFetchers.getBookByIdDataFetcher())); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc index b71ec6de2c3..451ac235091 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc @@ -1,7 +1,7 @@ [[web.graphql]] -== Spring GraphQL -If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring GraphQL]. -The Spring GraphQL project is based on https://github.com/graphql-java/graphql-java[GraphQL Java]. +== Spring for GraphQL +If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring for GraphQL]. +The Spring for GraphQL project is based on https://github.com/graphql-java/graphql-java[GraphQL Java]. You'll need the `spring-boot-starter-graphql` starter at a minimum. Because GraphQL is transport-agnostic, you'll also need to have one or more additional starters in your application to expose your GraphQL API over the web: @@ -22,6 +22,9 @@ Because GraphQL is transport-agnostic, you'll also need to have one or more addi | HTTP, WebSocket | Spring WebFlux +| `spring-boot-starter-rsocket` +| TCP, WebSocket +| Spring WebFlux on Reactor Netty |=== @@ -74,9 +77,11 @@ Spring Data repositories annotated with `@GraphQlRepository` and extending one o are detected by Spring Boot and considered as candidates for `DataFetcher` for matching top-level queries. +[[web.graphql.transports]] +=== Transports -[[web.graphql.web-endpoints]] -=== Web Endpoints +[[web.graphql.transports.http-websocket]] +==== HTTP and WebSocket The GraphQL HTTP endpoint is at HTTP POST "/graphql" by default. The path can be customized with configprop:spring.graphql.path[]. @@ -91,10 +96,6 @@ This is quite useful for retrieving information from an HTTP request header and With Spring Boot, you can declare a `WebInterceptor` bean to have it registered with the web transport. - -[[web.graphql.cors]] -=== CORS - {spring-framework-docs}/web.html#mvc-cors[Spring MVC] and {spring-framework-docs}/web-reactive.html#webflux-cors[Spring WebFlux] support CORS (Cross-Origin Resource Sharing) requests. CORS is a critical part of the web config for GraphQL applications that are accessed from browsers using different domains. @@ -111,6 +112,19 @@ Spring Boot supports many configuration properties under the `spring.graphql.cor ---- +[[web.graphql.transports.rsocket]] +==== RSocket + +RSocket is also supported as a transport, on top of WebSocket or TCP. +Once the <>, we can configure our GraphQL handler on a particular route using configprop:spring.graphql.rsocket.mapping[]. +For example, configuring that mapping as `"graphql"` means we can use the `RSocketGraphQlClient` as follows. + +For RSocket over TCP: +include::code:RSocketGraphQlClientExample[tag=tcp] + +For RSocket over WebSocket: +include::code:RSocketGraphQlClientExample[tag=websocket] + [[web.graphql.exception-handling]] === Exceptions Handling @@ -126,7 +140,7 @@ Spring Boot will automatically detect `DataFetcherExceptionResolver` beans and r Spring GraphQL offers infrastructure for helping developers when consuming or developing a GraphQL API. -Spring GraphQL ships with a default https://github.com/graphql/graphiql[GraphiQL] page that is exposed at "/graphiql" by default. +Spring GraphQL ships with a default https://github.com/graphql/graphiql[GraphiQL] page that is exposed at `"/graphiql"` by default. This page is disabled by default and can be turned on with the configprop:spring.graphql.graphiql.enabled[] property. Many applications exposing such a page will prefer a custom build. A default implementation is very useful during development, this is why it is exposed automatically with <> during development. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.java new file mode 100644 index 00000000000..c656be28c12 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-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.boot.docs.web.graphql.transports.rsocket; + +import java.net.URI; +import java.time.Duration; + +import reactor.core.publisher.Mono; + +import org.springframework.graphql.client.RSocketGraphQlClient; + +public class RSocketGraphQlClientExample { + + public void rsocketOverTcp() { + // tag::tcp[] + RSocketGraphQlClient client = RSocketGraphQlClient.builder().tcp("example.spring.io", 8181).route("graphql") + .build(); + Mono book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }") + .retrieve("bookById").toEntity(Book.class); + // end::tcp[] + book.block(Duration.ofSeconds(5)); + } + + public void rsocketOverWebSocket() { + // tag::websocket[] + RSocketGraphQlClient client = RSocketGraphQlClient.builder() + .webSocket(URI.create("wss://example.spring.io/rsocket")).route("graphql").build(); + Mono book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }") + .retrieve("bookById").toEntity(Book.class); + // end::websocket[] + book.block(Duration.ofSeconds(5)); + } + + static class Book { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.kt new file mode 100644 index 00000000000..7a674b7fa3a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2012-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.boot.docs.web.graphql.transports.rsocket + +import org.springframework.graphql.client.RSocketGraphQlClient +import java.net.URI +import java.time.Duration + + +class RSocketGraphQlClientExample { + + fun rsocketOverTcp() { + // tag::tcp[] + val client = RSocketGraphQlClient.builder() + .tcp("example.spring.io", 8181) + .route("graphql") + .build() + val book = client.document( + """ + { + bookById(id: "book-1"){ + id + name + pageCount + author + } + } + """) + .retrieve("bookById").toEntity(Book::class.java) + // end::tcp[] + book.block(Duration.ofSeconds(5)) + } + + fun rsocketOverWebSocket() { + // tag::websocket[] + val client = RSocketGraphQlClient.builder() + .webSocket(URI.create("wss://example.spring.io/rsocket")) + .route("graphql") + .build() + val book = client.document( + """ + { + bookById(id: "book-1"){ + id + name + pageCount + author + } + } + """) + .retrieve("bookById").toEntity(Book::class.java) + // end::websocket[] + book.block(Duration.ofSeconds(5)) + } + + internal class Book +} \ No newline at end of file