Refine GraphQlResponseField getError()

Instead of a simple check, looking for an associated field error at or
above the field, this method now more focused on finding the reason for
a failure when the field has no value.

This allows performing a more thorough search including cases when the
field error is at, above, or even below (e.g. non-null nested field
that bubbled up), in the end falling back on request errors (e.g.
failed response without any field errors).

Also, rename ResponseField to GraphQlResponseField and move to a
top-level class.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-18 17:21:06 +00:00
parent 827b70b71d
commit cdd8b67fed
10 changed files with 284 additions and 214 deletions

View File

@@ -135,7 +135,7 @@ which is a strategy for loading the document for a request by file name.
Once you have a <<client-graphqlclient>>, you can begin to perform requests via
<<client-requests-retrieve, retrieve()>> or <<client-requests-execute, execute()>>
where the former is merely a shortcut for the latter.
where the former is only a shortcut for the latter.
@@ -159,25 +159,21 @@ The below retrieves and decodes the data for a query:
.retrieve("project") <2>
.toEntity(Project.class); <3>
----
<1> The operation to perform
<2> Specify a path under the "data" key in the response map
<3> Decode the data at the path to the target type
<1> The operation to perform.
<2> The path under the "data" key in the response map to decode from.
<3> Decode the data at the path to the target type.
The document is a `String` that could be a literal or produced through a code generated
request object. You can also define documents in files and use a
The input document is a `String` that could be a literal or produced through a code
generated request object. You can also define documents in files and use a
<<client-requests-document-source>> to resole them by file name.
The path is relative to the "data" key and uses a simple dot (".") separated notation
for nested fields with optional array indices for list elements, e.g. `"project.name"`,
`"project.releases[0].version"`, and so on.
for nested fields with optional array indices for list elements, e.g. `"project.name"`
or `"project.releases[0].version"`.
Decoding can fail with `FieldAccessException` if the given path is not present in the
response map, or when the value is `null` and there is an error for the field.
By default, `FieldAccessException` is also raised on `retrieve` for partial data where
the field value exists but nested fields may be `null` with a field error. In such
cases, you can handle the exception to examine the errors and decide whether or how to
decode the partial data:
Decoding can result in `FieldAccessException` if the given path is not present, or the
field value is `null` and has an error. `FieldAccessException` provides access to the
response and the field:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -185,24 +181,20 @@ decode the partial data:
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, ex -> {
ResponseField field = ex.getField();
// Use field to check nested field errors and/or decode
// Return Mono with Project or an error
ClientGraphQlResponse response = ex.getResponse();
// ...
GraphQlResponseField field = ex.getField();
// ...
});
----
TIP: The GraphQL spec considers a partial response or a partial field to be valid, and
it may be feasible to decode them. By contrast, a failed field (i.e. value not present or
is `null` with field error) or a failed response (no "data" key) are not valid and
attempts to decode those are always rejected.
[[client-requests-execute]]
=== Execute
The `retrieve` method is only a shortcut to decode from a single path to a higher level
object. For more control and access to the response, use the `execute` method.
`retrieve` is only a shortcut to decode from a single path in the response map. For more
control, use the `execute` method and handle the response:
For example:
@@ -212,17 +204,27 @@ For example:
Mono<Project> projectMono = graphQlClient.document(document)
.execute()
.map(response -> {
// Check response.isValid(), getErrors()
if (!response.isValid()) {
// Request failure... <1>
}
ResponseField field = response.field("project");
// Check field.hasValue(), getError()
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure... <2>
}
else {
// Optional field set to null... <3>
}
}
return field.toEntity(Project.class)
return field.toEntity(Project.class); <4>
});
----
You can use `execute` to check response errors, obtain different fields, check their
field errors and nested field errors, and/or decode their values.
<1> The response does not have data, only errors
<2> Field that is `null` and has an associated error
<3> Field that was set to `null` by its `DataFetcher`
<4> Decode the data at the given path
@@ -272,7 +274,7 @@ You can use the `GraphQlClient` <<client-graphqlclient-builder>> to customize th
== Subscriptions
For a subscription operation, call `retrieveSubscription` instead of `retrieve` to
obtain a stream of responses rather than a single response:
obtain a stream of responses, each decoded to a target object:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -281,29 +283,34 @@ obtain a stream of responses rather than a single response:
.toEntity(String.class);
----
Similar to the <<client-requests-retrieve>> vs <<client-requests-execute>> choice
for requests with a single response, the same choice is also available for subscriptions.
For example, for more control over each response, use `executeSubscription` instead of
`retrieveSubscription`:
Similar to the <<client-requests-retrieve, retrieve>> vs <<client-requests-execute, execute>>
alternatives for single response requests, the same is also available for subscriptions.
For more control over each response, use `executeSubscription`:
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map(response -> {
// Check response.isValid(), getErrors()
if (!response.isValid()) {
// Request failure...
}
ResponseField field = response.field("greeting");
// Check field.isValid(), getError()
ResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure...
}
else {
// Optional field set to null... <3>
}
}
return field.toEntity(String.class)
});
----
Subscriptions are supported only with the <<client-websocketgraphqlclient,
WebSocketGraphQlClient>> extension.
NOTE: Subscriptions are supported only over <<client-websocketgraphqlclient, WebSocket>>.