Add GraphQlClient and GraphQlTransport for HTTP
See gh-10
This commit is contained in:
@@ -12,6 +12,7 @@ dependencies {
|
||||
compileOnly 'org.springframework:spring-webmvc'
|
||||
compileOnly 'org.springframework:spring-websocket'
|
||||
compileOnly 'javax.servlet:javax.servlet-api'
|
||||
compileOnly 'javax.validation:validation-api'
|
||||
|
||||
compileOnly 'org.springframework.security:spring-security-core'
|
||||
|
||||
@@ -22,7 +23,8 @@ dependencies {
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-stdlib'
|
||||
compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
|
||||
|
||||
compileOnly 'javax.validation:validation-api'
|
||||
compileOnly 'com.jayway.jsonpath:json-path'
|
||||
compileOnly 'com.fasterxml.jackson.core:jackson-databind'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
@@ -47,6 +49,7 @@ dependencies {
|
||||
testImplementation 'com.querydsl:querydsl-core'
|
||||
testImplementation 'com.querydsl:querydsl-collections'
|
||||
testImplementation 'javax.servlet:javax.servlet-api'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.14.9'
|
||||
testImplementation 'javax.validation:validation-api'
|
||||
testImplementation 'com.jayway.jsonpath:json-path'
|
||||
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GraphQlClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final GraphQlTransport transport;
|
||||
|
||||
private final Configuration jsonPathConfig;
|
||||
|
||||
private final OperationContentLoader operationContentLoader;
|
||||
|
||||
|
||||
public DefaultGraphQlClient(GraphQlTransport transport, Configuration jsonPathConfig,
|
||||
OperationContentLoader operationContentLoader) {
|
||||
|
||||
Assert.notNull(transport, "GraphQlTransport is required");
|
||||
Assert.notNull(jsonPathConfig, "'jsonPathConfig' is required");
|
||||
Assert.notNull(operationContentLoader, "RequestNameResolver is required");
|
||||
this.transport = transport;
|
||||
this.jsonPathConfig = jsonPathConfig;
|
||||
this.operationContentLoader = operationContentLoader;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T extends GraphQlTransport> T getTransport(Class<T> requiredType) {
|
||||
return (requiredType.isInstance(this.transport) ? (T) this.transport : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec operation(String operation) {
|
||||
return new DefaultRequestSpec(operation, this.transport, this.jsonPathConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec loadOperationContent(String key) {
|
||||
String requestContent = this.operationContentLoader.loadOperation(key);
|
||||
Assert.notNull(requestContent, "Failed to load operation content for key: " + key);
|
||||
return new DefaultRequestSpec(requestContent, this.transport, this.jsonPathConfig);
|
||||
}
|
||||
|
||||
|
||||
private static final class DefaultRequestSpec implements RequestSpec {
|
||||
|
||||
private final String operation;
|
||||
|
||||
@Nullable
|
||||
private String operationName;
|
||||
|
||||
private final Map<String, Object> variables = new LinkedHashMap<>();
|
||||
|
||||
private final GraphQlTransport transport;
|
||||
|
||||
private final Configuration jsonPathConfig;
|
||||
|
||||
DefaultRequestSpec(String operation, GraphQlTransport transport, Configuration jsonPathConfig) {
|
||||
Assert.hasText(operation, "'operation' is required");
|
||||
this.operation = operation;
|
||||
this.transport = transport;
|
||||
this.jsonPathConfig = jsonPathConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestSpec operationName(@Nullable String name) {
|
||||
this.operationName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestSpec variable(String name, Object value) {
|
||||
this.variables.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseSpec> execute() {
|
||||
return this.transport.execute(initRequestInput())
|
||||
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ResponseSpec> executeSubscription() {
|
||||
return this.transport.executeSubscription(initRequestInput())
|
||||
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
|
||||
}
|
||||
|
||||
private RequestInput initRequestInput() {
|
||||
return new RequestInput(this.operation, this.operationName, this.variables, null, "1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final DocumentContext documentContext;
|
||||
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
private DefaultResponseSpec(ExecutionResult result, Configuration jsonPathConfig) {
|
||||
this.documentContext = JsonPath.parse(result.toSpecification(), jsonPathConfig);
|
||||
this.errors = result.getErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(String path, Class<D> entityType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(String path, ParameterizedTypeReference<D> entityType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(String path, Class<D> elementType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(String path, ParameterizedTypeReference<D> elementType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
}
|
||||
|
||||
private static JsonPath initJsonPath(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
path = "$.data";
|
||||
}
|
||||
else if (!path.startsWith("$") && !path.startsWith("data.")) {
|
||||
path = "$.data." + path;
|
||||
}
|
||||
return JsonPath.compile(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GraphQLError> errors() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
|
||||
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GraphQlClient.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = DefaultGraphQlClientBuilder.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
}
|
||||
|
||||
|
||||
private final GraphQlTransport transport;
|
||||
|
||||
@Nullable
|
||||
private Configuration jsonPathConfig;
|
||||
|
||||
@Nullable
|
||||
private OperationContentLoader operationContentLoader;
|
||||
|
||||
|
||||
DefaultGraphQlClientBuilder(GraphQlTransport transport) {
|
||||
Assert.notNull(transport, "GraphQlTransport is required");
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GraphQlClient.Builder jsonPathConfig(@Nullable Configuration config) {
|
||||
this.jsonPathConfig = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient.Builder operationContentLoader(@Nullable OperationContentLoader contentLoader) {
|
||||
this.operationContentLoader = contentLoader;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient build() {
|
||||
return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initRequestNameResolver());
|
||||
}
|
||||
|
||||
private Configuration initJsonPathConfig() {
|
||||
if (this.jsonPathConfig != null) {
|
||||
return this.jsonPathConfig;
|
||||
}
|
||||
else if (jackson2Present) {
|
||||
return Jackson2Configuration.create();
|
||||
}
|
||||
else {
|
||||
return Configuration.builder().build();
|
||||
}
|
||||
}
|
||||
|
||||
private OperationContentLoader initRequestNameResolver() {
|
||||
return (this.operationContentLoader == null ?
|
||||
new ResourceOperationContentLoader() : this.operationContentLoader);
|
||||
}
|
||||
|
||||
|
||||
private static class Jackson2Configuration {
|
||||
|
||||
static Configuration create() {
|
||||
return Configuration.builder()
|
||||
.jsonProvider(new JacksonJsonProvider())
|
||||
.mappingProvider(new JacksonMappingProvider())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import graphql.GraphQLError;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Defines a workflow to prepare and execute GraphQL requests and to decode and
|
||||
* handle responses.
|
||||
*
|
||||
* <p>To create a {@link GraphQlClient}, use the builder in this class, and see
|
||||
* examples in {@link HttpGraphQlTransport} and {@link WebSocketGraphQlTransport}
|
||||
* for initializing both the client and the transport it runs over.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
* @see HttpGraphQlTransport
|
||||
* @see WebSocketGraphQlTransport
|
||||
*/
|
||||
public interface GraphQlClient {
|
||||
|
||||
/**
|
||||
* Return the underlying transport or {@code null} if the required type
|
||||
* does not match the transport type. See {@link GraphQlTransport}
|
||||
*/
|
||||
@Nullable
|
||||
<T extends GraphQlTransport> T getTransport(Class<T> requiredType);
|
||||
|
||||
/**
|
||||
* Start a new request with the given GraphQL operation, which can be a
|
||||
* query, a mutation, or subscription.
|
||||
* @param operation the operation to perform
|
||||
* @return spec to further define and execute the request
|
||||
*/
|
||||
RequestSpec operation(String operation);
|
||||
|
||||
/**
|
||||
* Variant of {@link #operation(String)} that loads the content of the
|
||||
* operation from the given key through the configured
|
||||
* {@link OperationContentLoader}.
|
||||
* @throws IllegalArgumentException if the content could not be loaded
|
||||
*/
|
||||
RequestSpec loadOperationContent(String key);
|
||||
|
||||
|
||||
/**
|
||||
* Return a builder to initialize a {@link GraphQlClient} instance.
|
||||
* @param transport the transport for executing requests over
|
||||
* @see HttpGraphQlTransport
|
||||
* @see WebSocketGraphQlTransport
|
||||
*/
|
||||
static Builder builder(GraphQlTransport transport) {
|
||||
return new DefaultGraphQlClientBuilder(transport);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder for creating {@link GraphQlClient} instances.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Provide JSONPath configuration settings.
|
||||
* <p>By default, the Jackson JSON library is used if present.
|
||||
*/
|
||||
Builder jsonPathConfig(@Nullable Configuration config);
|
||||
|
||||
/**
|
||||
* Configure an {@link OperationContentLoader} to use with
|
||||
* {@link #loadOperationContent(String) GraphQlClient#loadOperationContent}.
|
||||
* <p>By default, {@link ResourceOperationContentLoader} is used.
|
||||
*/
|
||||
Builder operationContentLoader(@Nullable OperationContentLoader contentLoader);
|
||||
|
||||
/**
|
||||
* Build the {@code GraphQlClient} instance.
|
||||
*/
|
||||
GraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Declare options to execute a request.
|
||||
*/
|
||||
interface ExecuteSpec {
|
||||
|
||||
/**
|
||||
* Execute as a single-response operation such as "query" or "mutation".
|
||||
* @return a {@code Mono} with a {@code ResponseSpec} for further
|
||||
* decoding of the response. The {@code Mono} may end wth an error due
|
||||
* to transport level issues.
|
||||
*/
|
||||
Mono<ResponseSpec> execute();
|
||||
|
||||
/**
|
||||
* Execute a "subscription" request and stream a stream of responses.
|
||||
* @return a {@code Flux} with a {@code ResponseSpec} for further
|
||||
* decoding of the response. The {@code Flux} may terminate as follows:
|
||||
* <ul>
|
||||
* <li>Completes if the subscription completes before the connection is closed.
|
||||
* <li>{@link SubscriptionErrorException} if the subscription ends with an error.
|
||||
* <li>{@link IllegalStateException} if the connection is closed or lost
|
||||
* before the stream terminates.
|
||||
* <li>Exception for connection and GraphQL session initialization issues.
|
||||
* </ul>
|
||||
* <p>The {@code Flux} may be cancelled to notify the server to end the
|
||||
* subscription stream.
|
||||
*/
|
||||
Flux<ResponseSpec> executeSubscription();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Declare options to gather input for a GraphQL request and execute it.
|
||||
*/
|
||||
interface RequestSpec extends ExecuteSpec {
|
||||
|
||||
/**
|
||||
* Set the operation name.
|
||||
* @param name the operation name
|
||||
* @return this request spec
|
||||
*/
|
||||
RequestSpec operationName(@Nullable String name);
|
||||
|
||||
/**
|
||||
* Add a variable.
|
||||
* @param name the variable name
|
||||
* @param value the variable value
|
||||
* @return this request spec
|
||||
*/
|
||||
RequestSpec variable(String name, Object value);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Declare options to decode a response.
|
||||
*/
|
||||
interface ResponseSpec {
|
||||
|
||||
/**
|
||||
* Switch to the given the "data" path of the GraphQL response and
|
||||
* convert the data to the target type. The path can be an operation
|
||||
* root type name, e.g. "book", or a nested path such as "book.name",
|
||||
* or any <a href="https://github.com/jayway/JsonPath">JsonPath</a>
|
||||
* relative to the "data" key of the response.
|
||||
* @param path a JSON path to the data of interest
|
||||
* @param entityType the type to convert to
|
||||
* @param <D> the target entity type
|
||||
* @return the entity resulting from the conversion
|
||||
*/
|
||||
<D> D toEntity(String path, Class<D> entityType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(String, Class)} for entity classes with
|
||||
* generic types.
|
||||
* @param path a JSON path to the data of interest
|
||||
* @param entityType the type to convert to
|
||||
* @param <D> the target entity type
|
||||
* @return the entity resulting from the conversion
|
||||
*/
|
||||
<D> D toEntity(String path, ParameterizedTypeReference<D> entityType);
|
||||
|
||||
/**
|
||||
* Switch to the given the "data" path of the GraphQL response and
|
||||
* convert the data to a List with the given element type.
|
||||
* The path can be an operation root type name, e.g. "book", or a
|
||||
* nested path such as "book.name", or any
|
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a>
|
||||
* relative to the "data" key of the response.
|
||||
* @param path a JSON path to the data of interest
|
||||
* @param elementType the type of element to convert to
|
||||
* @param <D> the target entity type
|
||||
* @return the list of entities resulting from the conversion
|
||||
*/
|
||||
<D> List<D> toEntityList(String path, Class<D> elementType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntityList(String, Class)} for entity classes
|
||||
* with generic types.
|
||||
* @param path a JSON path to the data of interest
|
||||
* @param elementType the type to convert to
|
||||
* @param <D> the target entity type
|
||||
* @return the list of entities resulting from the conversion
|
||||
*/
|
||||
<D> List<D> toEntityList(String path, ParameterizedTypeReference<D> elementType);
|
||||
|
||||
/**
|
||||
* Return the errors from the response or an empty list.
|
||||
*/
|
||||
List<GraphQLError> errors();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.client;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
|
||||
/**
|
||||
* Contract for a transport, over which to execute GraphQL requests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface GraphQlTransport {
|
||||
|
||||
/**
|
||||
* Execute a single-response operation such as "query" or "mutation".
|
||||
* @param input the request to execute
|
||||
* @return a {@code Mono} with the {@code ExecutionResult} for the response.
|
||||
* The {@code Mono} may end wth an error due to transport or other issues
|
||||
* such as failures to encode the request or decode the response.
|
||||
* </ul>
|
||||
*/
|
||||
Mono<ExecutionResult> execute(RequestInput input);
|
||||
|
||||
/**
|
||||
* Execute a "subscription" request and stream the responses.
|
||||
* @param input the request to execute
|
||||
* @return a {@code Flux} with an {@code ExecutionResult} for each response.
|
||||
* The {@code Flux} may terminate as follows:
|
||||
* <ul>
|
||||
* <li>Completes if the subscription completes before the connection is closed.
|
||||
* <li>{@link SubscriptionErrorException} if the subscription ends with an error.
|
||||
* <li>{@link IllegalStateException} if the connection is closed or lost
|
||||
* before the stream terminates.
|
||||
* <li>Exception for connection and GraphQL session initialization issues.
|
||||
* </ul>
|
||||
* <p>The {@code Flux} may be cancelled to notify the server to end the
|
||||
* subscription stream.
|
||||
*/
|
||||
Flux<ExecutionResult> executeSubscription(RequestInput input);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Transport to execute GraphQL requests over HTTP via {@link WebClient}.
|
||||
* Single-response requests are performed over HTTP POST while subscriptions
|
||||
* over HTTP are not supported.
|
||||
*
|
||||
* <p>Use the builder to initialize the transport and the {@link GraphQlClient}
|
||||
* in a single chain:
|
||||
*
|
||||
* <pre style="class">
|
||||
* GraphQlClient client = HttpGraphQlTransport.builder(webClient).buildClient();
|
||||
* </pre>
|
||||
*
|
||||
* <p>Or build the transport and the client separately:
|
||||
*
|
||||
* <pre style="class">
|
||||
* HttpGraphQlTransport transport = HttpGraphQlTransport.create(webClient);
|
||||
* GraphQlClient client = GraphQlClient.create(transport);
|
||||
* </pre>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class HttpGraphQlTransport implements GraphQlTransport {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
|
||||
private HttpGraphQlTransport(WebClient webClient) {
|
||||
Assert.notNull(webClient, "WebClient is required");
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying {@code WebClient}.
|
||||
*/
|
||||
public WebClient getWebClient() {
|
||||
return this.webClient;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ExecutionResult> execute(RequestInput requestInput) {
|
||||
return this.webClient.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestInput.toMap())
|
||||
.retrieve()
|
||||
.bodyToMono(MAP_TYPE)
|
||||
.map(MapExecutionResult::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput requestInput) {
|
||||
throw new UnsupportedOperationException("Subscriptions not supported over HTTP");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method with a {@code WebClient} to use.
|
||||
*/
|
||||
public static HttpGraphQlTransport create(WebClient webClient) {
|
||||
return new HttpGraphQlTransport(webClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to obtain a {@code Builder}.
|
||||
*/
|
||||
public static Builder builder(WebClient webClient) {
|
||||
return new Builder(webClient);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builder for {@link HttpGraphQlTransport} or a {@link GraphQlClient}
|
||||
* configured with the transport.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
private Builder(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@code WebClient} to use.
|
||||
*/
|
||||
public Builder webClient(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@code HttpGraphQlTransport} instance.
|
||||
*/
|
||||
public HttpGraphQlTransport build() {
|
||||
return new HttpGraphQlTransport(this.webClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue on to build a {@link GraphQlClient} configured with the
|
||||
* transport configured here so far.
|
||||
*/
|
||||
public GraphQlClient.Builder configureClient() {
|
||||
return GraphQlClient.builder(build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to build a {@link GraphQlClient} configured with the
|
||||
* transport configured here.
|
||||
*/
|
||||
public GraphQlClient buildClient() {
|
||||
return GraphQlClient.builder(build()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.ExecutionResultImpl;
|
||||
import graphql.GraphQLError;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ExecutionResult} backed by a {@link Map}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class MapExecutionResult implements ExecutionResult {
|
||||
|
||||
private final Map<String, Object> map;
|
||||
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
|
||||
MapExecutionResult(@Nullable Map<String, Object> map) {
|
||||
this.map = (map != null ? map : Collections.emptyMap());
|
||||
this.errors = MapGraphQlError.fromResultMap(map);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<GraphQLError> getErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getData() {
|
||||
return (T) this.map.get("data");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataPresent() {
|
||||
return (this.map.get("data") != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Map<Object, Object> getExtensions() {
|
||||
return (Map<Object, Object>) this.map.get("extensions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toSpecification() {
|
||||
return ExecutionResultImpl.newExecutionResult().from(this).build().toSpecification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof MapExecutionResult && this.map.equals(((MapExecutionResult) other).map));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.map.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.map.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method to create an instance of this class.
|
||||
*/
|
||||
public static ExecutionResult forData(@Nullable Map<String, Object> map) {
|
||||
return new MapExecutionResult(Collections.singletonMap("data", map));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import graphql.ErrorClassification;
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorHelper;
|
||||
import graphql.language.SourceLocation;
|
||||
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link GraphQLError} backed by a {@link Map}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
final class MapGraphQlError implements GraphQLError {
|
||||
|
||||
private final Map<String, Object> errorMap;
|
||||
|
||||
private final List<SourceLocation> locations;
|
||||
|
||||
|
||||
private MapGraphQlError(Map<String, Object> errorMap) {
|
||||
this.errorMap = errorMap;
|
||||
this.locations = initLocations(errorMap);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<SourceLocation> initLocations(Map<String, Object> errorMap) {
|
||||
List<Map<String, Object>> locations = (List<Map<String, Object>>) errorMap.get("locations");
|
||||
if (locations == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return locations.stream()
|
||||
.map(map -> new SourceLocation(
|
||||
(int) map.getOrDefault("line", 0),
|
||||
(int) map.getOrDefault("column", 0),
|
||||
(String) map.get("sourceName")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessage() {
|
||||
return (String) errorMap.get("message");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceLocation> getLocations() {
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ErrorClassification getErrorType() {
|
||||
// Attempt the reverse of how errorType is serialized in GraphqlErrorHelper.toSpecification.
|
||||
// However, we can only do that for ErrorClassification enums that we know of.
|
||||
String value = (getExtensions() != null ? (String) getExtensions().get("classification") : null);
|
||||
if (value != null) {
|
||||
try {
|
||||
return graphql.ErrorType.valueOf(value);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
return ErrorType.valueOf(value);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@Nullable
|
||||
public List<Object> getPath() {
|
||||
return (List<Object>) this.errorMap.get("path");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@Nullable
|
||||
public Map<String, Object> getExtensions() {
|
||||
return (Map<String, Object>) this.errorMap.get("extensions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toSpecification() {
|
||||
return GraphqlErrorHelper.toSpecification(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return GraphqlErrorHelper.equals(this, other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return GraphqlErrorHelper.hashCode(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toSpecification().toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method to create an instance from a list of maps, each
|
||||
* containing an error.
|
||||
*/
|
||||
public static List<GraphQLError> fromMapList(@Nullable List<Map<String, Object>> errorMaps) {
|
||||
if (CollectionUtils.isEmpty(errorMaps)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return errorMaps.stream().map(MapGraphQlError::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create an instance from an
|
||||
* {@link graphql.ExecutionResult} map.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<GraphQLError> fromResultMap(@Nullable Map<String, Object> map) {
|
||||
if (map == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return MapGraphQlError.fromMapList((List<Map<String, Object>>) map.get("errors"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Strategy to load the content of a GraphQL operation from a key.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface OperationContentLoader {
|
||||
|
||||
/**
|
||||
* Return the operation for the given key.
|
||||
* @param key the key to look up the operation content
|
||||
* @return the content of the operation, if found
|
||||
*/
|
||||
@Nullable
|
||||
String loadOperation(String key);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* {@link OperationContentLoader} that looks for resources relative to a list of
|
||||
* {@link Resource} locations with a list of extensions.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ResourceOperationContentLoader implements OperationContentLoader {
|
||||
|
||||
private static final List<String> FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql");
|
||||
|
||||
|
||||
private final List<Resource> locations;
|
||||
|
||||
private final List<String> extensions;
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor to look under {@code graphql/} on the classpath for
|
||||
* resources with extensions ".graphql" and ".gql".
|
||||
*/
|
||||
public ResourceOperationContentLoader() {
|
||||
this(Collections.singletonList(new ClassPathResource("graphql/")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with custom locations with extensions ".graphql" and ".gql".
|
||||
*/
|
||||
public ResourceOperationContentLoader(List<Resource> locations) {
|
||||
this(locations, FILE_EXTENSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with given locations and extensions.
|
||||
*/
|
||||
public ResourceOperationContentLoader(List<Resource> locations, List<String> extensions) {
|
||||
this.locations = new ArrayList<>(locations);
|
||||
this.extensions = new ArrayList<>(extensions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured locations.
|
||||
*/
|
||||
public List<Resource> getLocations() {
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured extensions.
|
||||
*/
|
||||
public List<String> getExtensions() {
|
||||
return this.extensions;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String loadOperation(String key) {
|
||||
return this.locations.stream()
|
||||
.flatMap(location -> this.extensions.stream().map(ext -> getRelativeResource(location, key, ext)))
|
||||
.filter(Resource::exists)
|
||||
.findFirst()
|
||||
.map(resource -> {
|
||||
try {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
FileCopyUtils.copy(resource.getInputStream(), out);
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Found resource: " + resource.getDescription() + " but failed to read it", ex);
|
||||
}
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Resource getRelativeResource(Resource location, String name, String ext) {
|
||||
try {
|
||||
return location.createRelative(name + ext);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.jayway.jsonpath.TypeRef;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Adapt a JSONPath {@link TypeRef} to {@link ParameterizedTypeReference} and
|
||||
* {@link ResolvableType} for classes with generics.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TypeRefAdapter<T> extends TypeRef<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
TypeRefAdapter(Class<T> clazz) {
|
||||
this.type = clazz;
|
||||
}
|
||||
|
||||
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
|
||||
this.type = typeReference.getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* GraphQL client.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.ExecutionResultImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DefaultGraphQlClientTests {
|
||||
|
||||
@Test
|
||||
void executeQuery() {
|
||||
String query = "{" +
|
||||
" project(slug: \"spring-framework\") {" +
|
||||
" slug" +
|
||||
" name" +
|
||||
" repositoryUrl" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
Project expectedProject = new Project(
|
||||
"spring-framework", "Spring Framework", "https://github.com/spring-projects/spring-framework");
|
||||
|
||||
ExecutionResultImpl result = ExecutionResultImpl.newExecutionResult()
|
||||
.data(Collections.singletonMap("project", expectedProject.toMap()))
|
||||
.build();
|
||||
|
||||
TestTransport transport = new TestTransport(result);
|
||||
|
||||
Project project = GraphQlClient.builder(transport).build()
|
||||
.operation(query)
|
||||
.execute()
|
||||
.map(spec -> spec.toEntity("project", Project.class))
|
||||
.block();
|
||||
|
||||
assertThat(project).isNotNull();
|
||||
assertThat(project.getSlug()).isEqualTo(expectedProject.getSlug());
|
||||
assertThat(project.getName()).isEqualTo(expectedProject.getName());
|
||||
assertThat(project.getRepositoryUrl()).isEqualTo(expectedProject.getRepositoryUrl());
|
||||
}
|
||||
|
||||
|
||||
private static class TestTransport implements GraphQlTransport {
|
||||
|
||||
private final Mono<ExecutionResult> response;
|
||||
|
||||
@Nullable
|
||||
private RequestInput savedRequestInput;
|
||||
|
||||
public TestTransport(ExecutionResult response) {
|
||||
this(Mono.just(response));
|
||||
}
|
||||
|
||||
public TestTransport(Mono<ExecutionResult> response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public RequestInput getSavedRequestInput() {
|
||||
Assert.notNull(this.savedRequestInput, "No saved RequestInput");
|
||||
return this.savedRequestInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ExecutionResult> execute(RequestInput input) {
|
||||
this.savedRequestInput = input;
|
||||
return this.response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput input) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class Project {
|
||||
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
private String repositoryUrl;
|
||||
|
||||
public Project() {
|
||||
}
|
||||
|
||||
public Project(String slug, String name, String repositoryUrl) {
|
||||
this.slug = slug;
|
||||
this.name = name;
|
||||
this.repositoryUrl = repositoryUrl;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getRepositoryUrl() {
|
||||
return this.repositoryUrl;
|
||||
}
|
||||
|
||||
public void setRepositoryUrl(String repositoryUrl) {
|
||||
this.repositoryUrl = repositoryUrl;
|
||||
}
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("slug", getSlug());
|
||||
map.put("name", getName());
|
||||
map.put("repositoryUrl", getRepositoryUrl());
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user