diff --git a/graphql-webclient/build.gradle b/graphql-webclient/build.gradle index 273d0ee..0427820 100644 --- a/graphql-webclient/build.gradle +++ b/graphql-webclient/build.gradle @@ -1,5 +1,6 @@ dependencies { api "org.springframework.boot:spring-boot-starter-webflux" + implementation "com.jayway.jsonpath:json-path:2.8.0" testImplementation "org.springframework.boot:spring-boot-starter-webflux" testImplementation "org.springframework.boot:spring-boot-starter-test" diff --git a/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponse.java b/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponse.java index 976bb5e..72d9bee 100644 --- a/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponse.java +++ b/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponse.java @@ -6,6 +6,12 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.JsonPathException; +import com.jayway.jsonpath.ReadContext; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; + import java.util.Collections; import java.util.List; import java.util.Optional; @@ -20,6 +26,8 @@ public class GraphQLResponse { private final List errors; private final ObjectMapper objectMapper; + private ReadContext readContext; + GraphQLResponse(String rawResponse, ObjectMapper objectMapper) { this.objectMapper = objectMapper; @@ -58,6 +66,14 @@ public T get(String fieldName, Class type) { return null; } + public T getAt(String path, Class type) throws GraphQLResponseReadException { + try { + return getReadContext().read(path, type); + } catch (JsonPathException e) { + throw new GraphQLResponseReadException("Failed to read part of GraphQL response.", e); + } + } + public T getFirst(Class type) { return getFirstDataEntry().map(it -> objectMapper.convertValue(it, type)).orElse(null); } @@ -76,6 +92,10 @@ public List getList(String fieldName, Class type) { return emptyList(); } + public List getListAt(String path, Class itemType) throws GraphQLResponseReadException { + return objectMapper.convertValue(getAt(path), constructListType(itemType)); + } + @SuppressWarnings("unchecked") public List getFirstList(Class type) { return getFirstDataEntry() @@ -84,10 +104,28 @@ public List getFirstList(Class type) { .orElseGet(Collections::emptyList); } + public T getAt(String path) throws GraphQLResponseReadException { + try { + return getReadContext().read(path); + } catch (JsonPathException e) { + throw new GraphQLResponseReadException("Failed to read part of GraphQL response.", e); + } + } + public void validateNoErrors() { if (!errors.isEmpty()) { throw new GraphQLErrorsException(errors); } } + public ReadContext getReadContext() { + if (readContext == null) { + Configuration.builder() + .mappingProvider(new JacksonMappingProvider(objectMapper)) + .build(); + readContext = JsonPath.parse(data.toString()); + } + return readContext; + } + } diff --git a/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseReadException.java b/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseReadException.java new file mode 100644 index 0000000..4017556 --- /dev/null +++ b/graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseReadException.java @@ -0,0 +1,7 @@ +package graphql.kickstart.spring.webclient.boot; + +import lombok.experimental.StandardException; + +@StandardException +public class GraphQLResponseReadException extends RuntimeException { +} diff --git a/graphql-webclient/src/test/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseTest.java b/graphql-webclient/src/test/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseTest.java index 63530d2..6c0b0a6 100644 --- a/graphql-webclient/src/test/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseTest.java +++ b/graphql-webclient/src/test/java/graphql/kickstart/spring/webclient/boot/GraphQLResponseTest.java @@ -1,6 +1,7 @@ package graphql.kickstart.spring.webclient.boot; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,7 +13,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.PathNotFoundException; + import java.util.List; +import java.util.Map; + import org.junit.jupiter.api.Test; class GraphQLResponseTest { @@ -95,4 +100,83 @@ void getList_dataFieldExists_returnsList() { assertEquals("value", values.get(0)); } + @Test + void getAt_dataFieldExists_returnsValue() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": \"value\" } } }"); + String value = response.getAt("field.innerField", String.class); + assertEquals("value", value); + } + + @Test + void getAt_noDataFieldExists_throwsException() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { } } }"); + GraphQLResponseReadException ex = assertThrows(GraphQLResponseReadException.class, () -> response.getAt("field.innerField", String.class)); + assertInstanceOf(PathNotFoundException.class, ex.getCause()); + } + + @Test + void getAt_dataIsNull_returnsNull() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": null } } }"); + assertNull(response.getAt("field.innerField", String.class)); + } + + @Test + void getListAt_dataFieldExists_returnsList() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": [\"value\"] } } }"); + List values = response.getListAt("field.innerField", String.class); + assertEquals(1, values.size()); + assertEquals("value", values.get(0)); + } + + @Test + void getListAt_dataIsNull_returnsNull() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": null } } }"); + assertNull(response.getListAt("field.innerField", String.class)); + } + + @Test + void getListAt_noDataFieldExists_throwsException() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { } } }"); + GraphQLResponseReadException ex = assertThrows(GraphQLResponseReadException.class, () -> response.getListAt("field.innerField", String.class)); + assertInstanceOf(PathNotFoundException.class, ex.getCause()); + } + + @Test + void getAtAutoCast_dataFieldExists_returnsMap() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": { \"blah\": \"value\" } } } }"); + Map value = response.getAt("field.innerField"); + assertEquals(1, value.size()); + assertEquals("value", value.get("blah")); + } + + @Test + void getAtAutoCast_dataFieldExists_returnsString() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": \"value\" } } }"); + String value = response.getAt("field.innerField"); + assertEquals("value", value); + } + + @Test + void getAtAutoCast_dataFieldExists_returnsInt() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": 42 } } }"); + Integer value = response.getAt("field.innerField"); + assertEquals(42, value); + } + + @Test + void getAtAutoCast_dataFieldExists_returnsDouble() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": 42.5 } } }"); + Double value = response.getAt("field.innerField"); + assertEquals(42.5, value); + } + + @Test + void getAtAutoCast_dataFieldExists_returnsList() { + GraphQLResponse response = constructResponse("{ \"data\": { \"field\": { \"innerField\": [ \"value\", 42 ] } } }"); + List values = response.getAt("field.innerField"); + assertEquals(2, values.size()); + assertEquals("value", values.get(0)); + assertEquals(42, values.get(1)); + } + }