Skip to content

Commit

Permalink
Add ability to supply custom ObjectMapper to clients
Browse files Browse the repository at this point in the history
Add constructors / static factory methods to allow supplying a custom ObjectMapper
to the GraphQL clients, which can be useful when the user needs precise control
over serialization.

Additionally, remove deprecated DefaultGraphQLClient, fix various tests, and simplify
serialization of the request body to avoid needing the KotlinModule installed at all.
  • Loading branch information
kilink committed Aug 1, 2024
1 parent 093bb41 commit 3a8f6f5
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language

/**
* Blocking implementation of a GraphQL client.
* The user is responsible for doing the actual HTTP request, making this pluggable with any HTTP client.
* For a more convenient option, use [WebClientGraphQLClient] instead.
*/
class CustomGraphQLClient(private val url: String, private val requestExecutor: RequestExecutor) : GraphQLClient {
class CustomGraphQLClient(private val url: String, private val requestExecutor: RequestExecutor, private val mapper: ObjectMapper) : GraphQLClient {

constructor(url: String, requestExecutor: RequestExecutor) : this(url, requestExecutor, GraphQLClients.objectMapper)

override fun executeQuery(@Language("graphql") query: String): GraphQLResponse {
return executeQuery(query, emptyMap(), null)
}
Expand All @@ -40,12 +44,8 @@ class CustomGraphQLClient(private val url: String, private val requestExecutor:
variables: Map<String, Any>,
operationName: String?
): GraphQLResponse {
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
Request(
query,
variables,
operationName
)
val serializedRequest = mapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)

val response = requestExecutor.execute(url, GraphQLClients.defaultHeaders, serializedRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language
import reactor.core.publisher.Mono

Expand All @@ -26,8 +27,12 @@ import reactor.core.publisher.Mono
*/
class CustomMonoGraphQLClient(
private val url: String,
private val monoRequestExecutor: MonoRequestExecutor
private val monoRequestExecutor: MonoRequestExecutor,
private val mapper: ObjectMapper
) : MonoGraphQLClient {

constructor(url: String, monoRequestExecutor: MonoRequestExecutor) : this (url, monoRequestExecutor, GraphQLClients.objectMapper)

override fun reactiveExecuteQuery(@Language("graphql") query: String): Mono<GraphQLResponse> {
return reactiveExecuteQuery(query, emptyMap(), null)
}
Expand All @@ -41,12 +46,8 @@ class CustomMonoGraphQLClient(
variables: Map<String, Any>,
operationName: String?
): Mono<GraphQLResponse> {
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
Request(
query,
variables,
operationName
)
val serializedRequest = mapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)
return monoRequestExecutor.execute(url, GraphQLClients.defaultHeaders, serializedRequest).map { response ->
GraphQLClients.handleResponse(response, serializedRequest, url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ class DefaultGraphQLClient(private val url: String) : GraphQLClient, MonoGraphQL
operationName: String?,
requestExecutor: RequestExecutor
): GraphQLResponse {
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(Request(query, variables, operationName))
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)
val response = requestExecutor.execute(url, GraphQLClients.defaultHeaders, serializedRequest)
return GraphQLClients.handleResponse(response, serializedRequest, url)
}
Expand Down Expand Up @@ -155,8 +157,9 @@ class DefaultGraphQLClient(private val url: String) : GraphQLClient, MonoGraphQL
operationName: String?,
requestExecutor: MonoRequestExecutor
): Mono<GraphQLResponse> {
@Suppress("BlockingMethodInNonBlockingContext")
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(Request(query, variables, operationName))
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)
return requestExecutor.execute(url, GraphQLClients.defaultHeaders, serializedRequest).map { response ->
GraphQLClients.handleResponse(response, serializedRequest, url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language

/**
Expand Down Expand Up @@ -66,5 +67,8 @@ interface GraphQLClient {
companion object {
@JvmStatic
fun createCustom(url: String, requestExecutor: RequestExecutor) = CustomGraphQLClient(url, requestExecutor)

@JvmStatic
fun createCustom(url: String, requestExecutor: RequestExecutor, mapper: ObjectMapper) = CustomGraphQLClient(url, requestExecutor, mapper)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ internal object GraphQLClients {

return GraphQLResponse(body ?: "", headers)
}

internal fun toRequestMap(query: String, operationName: String?, variables: Map<String, Any?>): Map<String, Any?> {
return mapOf(
"query" to query,
"operationName" to operationName,
"variables" to variables
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language
import org.springframework.http.HttpHeaders
import org.springframework.web.reactive.function.client.WebClient
Expand Down Expand Up @@ -93,6 +94,9 @@ interface MonoGraphQLClient {
@JvmStatic
fun createWithWebClient(webClient: WebClient) = WebClientGraphQLClient(webClient)

@JvmStatic
fun createWithWebClient(webClient: WebClient, objectMapper: ObjectMapper) = WebClientGraphQLClient(webClient, objectMapper)

@JvmStatic
fun createWithWebClient(
webClient: WebClient,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language
import org.springframework.http.HttpHeaders
import org.springframework.web.client.RestClient
Expand All @@ -39,11 +40,16 @@ import java.util.function.Consumer
*/
class RestClientGraphQLClient(
private val restClient: RestClient,
private val headersConsumer: Consumer<HttpHeaders>
private val headersConsumer: Consumer<HttpHeaders>,
private val mapper: ObjectMapper
) : GraphQLClient {

constructor(restClient: RestClient) : this(restClient, Consumer { })

constructor(restClient: RestClient, headersConsumer: Consumer<HttpHeaders>) : this(restClient, headersConsumer, GraphQLClients.objectMapper)

constructor(restClient: RestClient, mapper: ObjectMapper) : this(restClient, Consumer { }, mapper)

/**
* @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query!
* @return A [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors.
Expand All @@ -68,12 +74,8 @@ class RestClientGraphQLClient(
* @return A [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors.
*/
override fun executeQuery(@Language("graphql") query: String, variables: Map<String, Any>, operationName: String?): GraphQLResponse {
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
Request(
query,
variables,
operationName
)
val serializedRequest = mapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)

val responseEntity = restClient.post()
Expand All @@ -84,9 +86,14 @@ class RestClientGraphQLClient(
.toEntity(String::class.java)

if (!responseEntity.statusCode.is2xxSuccessful) {
throw GraphQLClientException(responseEntity.statusCode.value(), restClient.toString(), responseEntity.body ?: "", serializedRequest)
throw GraphQLClientException(
statusCode = responseEntity.statusCode.value(),
url = restClient.toString(),
response = responseEntity.body ?: "",
request = serializedRequest
)
}

return GraphQLResponse(responseEntity.body ?: "", responseEntity.headers)
return GraphQLResponse(json = responseEntity.body ?: "", headers = responseEntity.headers)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

package com.netflix.graphql.dgs.client

import com.fasterxml.jackson.databind.ObjectMapper
import org.intellij.lang.annotations.Language
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec
import org.springframework.web.reactive.function.client.toEntity
Expand All @@ -41,11 +42,16 @@ import java.util.function.Consumer
*/
class WebClientGraphQLClient(
private val webclient: WebClient,
private val headersConsumer: Consumer<HttpHeaders>
private val headersConsumer: Consumer<HttpHeaders>,
private val mapper: ObjectMapper
) : MonoGraphQLClient {

constructor(webclient: WebClient) : this(webclient, Consumer {})

constructor(webclient: WebClient, headersConsumer: Consumer<HttpHeaders>) : this(webclient, headersConsumer, GraphQLClients.objectMapper)

constructor(webclient: WebClient, mapper: ObjectMapper) : this(webclient, Consumer {}, mapper)

/**
* @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query!
* @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors.
Expand Down Expand Up @@ -111,13 +117,8 @@ class WebClientGraphQLClient(
operationName: String?,
requestBodyUriCustomizer: RequestBodyUriCustomizer
): Mono<GraphQLResponse> {
@Suppress("BlockingMethodInNonBlockingContext")
val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
Request(
query,
variables,
operationName
)
val serializedRequest = mapper.writeValueAsString(
GraphQLClients.toRequestMap(query = query, operationName = operationName, variables = variables)
)

return requestBodyUriCustomizer.apply(webclient.post())
Expand All @@ -126,24 +127,20 @@ class WebClientGraphQLClient(
.bodyValue(serializedRequest)
.retrieve()
.toEntity<String>()
.map { response ->
HttpResponse(
statusCode = response.statusCode.value(),
body = response.body,
headers = response.headers
)
}
.map { httpResponse -> handleResponse(httpResponse, serializedRequest) }
}

private fun handleResponse(response: HttpResponse, requestBody: String): GraphQLResponse {
val (statusCode, body) = response
val headers = response.headers
if (!HttpStatus.valueOf(statusCode).is2xxSuccessful) {
throw GraphQLClientException(statusCode, webclient.toString(), body ?: "", requestBody)
private fun handleResponse(response: ResponseEntity<String>, requestBody: String): GraphQLResponse {
if (!response.statusCode.is2xxSuccessful) {
throw GraphQLClientException(
statusCode = response.statusCode.value(),
url = webclient.toString(),
response = response.body ?: "",
request = requestBody
)
}

return GraphQLResponse(body ?: "", headers)
return GraphQLResponse(json = response.body ?: "", headers = response.headers)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
import reactor.core.publisher.Mono;

import static java.util.Collections.emptyMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@SuppressWarnings("deprecation")
public class GraphQLResponseJavaTest {

private final String query = "query SubmitReview {" +
Expand All @@ -47,21 +47,14 @@ public class GraphQLResponseJavaTest {

String url = "http://localhost:8080/graphql";

DefaultGraphQLClient client = new DefaultGraphQLClient(url);

RequestExecutor requestExecutor = (url, headers, body) -> {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::addAll);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders),String.class);
return new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody());
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders), String.class);
return new HttpResponse(exchange.getStatusCode().value(), exchange.getBody(), exchange.getHeaders());
};

RequestExecutor requestExecutorWithResponseHeaders = (url, headers, body) -> {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::addAll);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders),String.class);
return new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody(), exchange.getHeaders());
};
CustomGraphQLClient client = new CustomGraphQLClient(url, requestExecutor);

@Test
public void responseWithoutHeaders() {
Expand All @@ -74,7 +67,7 @@ public void responseWithoutHeaders() {

GraphQLResponse graphQLResponse = client.executeQuery(
query,
emptyMap(), "SubmitReview", requestExecutor
emptyMap(), "SubmitReview"
);

String submittedBy = graphQLResponse.extractValueAsObject("submitReview.submittedBy", String.class);
Expand All @@ -97,11 +90,11 @@ public void responseWithHeaders() {
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON));

GraphQLResponse graphQLResponse = client.executeQuery(query, emptyMap(), requestExecutorWithResponseHeaders);
GraphQLResponse graphQLResponse = client.executeQuery(query, emptyMap());

String submittedBy = graphQLResponse.extractValueAsObject("submitReview.submittedBy", String.class);
assert(submittedBy).contentEquals("abc@netflix.com");
assert(graphQLResponse.getHeaders().get("Content-Type").get(0)).contentEquals("application/json");
assertThat(submittedBy).isEqualTo("abc@netflix.com");
assertThat(graphQLResponse.getHeaders().get("Content-Type").get(0)).isEqualTo("application/json");
server.verify();
}

Expand All @@ -116,7 +109,7 @@ public void testCustom() {
CustomGraphQLClient client = GraphQLClient.createCustom(url, requestExecutor);
GraphQLResponse graphQLResponse = client.executeQuery(query, emptyMap(), "SubmitReview");
String submittedBy = graphQLResponse.extractValueAsObject("submitReview.submittedBy", String.class);
assert(submittedBy).contentEquals("abc@netflix.com");
assertThat(submittedBy).isEqualTo("abc@netflix.com");
server.verify();
}

Expand All @@ -131,12 +124,12 @@ public void testCustomMono() {
CustomMonoGraphQLClient client = MonoGraphQLClient.createCustomReactive(url, (requestUrl, headers, body) -> {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::addAll);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders),String.class);
return Mono.just(new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody(), exchange.getHeaders()));
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders), String.class);
return Mono.just(new HttpResponse(exchange.getStatusCode().value(), exchange.getBody(), exchange.getHeaders()));
});
Mono<GraphQLResponse> graphQLResponse = client.reactiveExecuteQuery(query, emptyMap(), "SubmitReview");
String submittedBy = graphQLResponse.map(r -> r.extractValueAsObject("submitReview.submittedBy", String.class)).block();
assert(submittedBy).contentEquals("abc@netflix.com");
assertThat(submittedBy).isEqualTo("abc@netflix.com");
server.verify();
}
}
Loading

0 comments on commit 3a8f6f5

Please sign in to comment.