diff --git a/server/api/src/main/java/io/smallrye/graphql/api/ErrorExtensionProvider.java b/server/api/src/main/java/io/smallrye/graphql/api/ErrorExtensionProvider.java new file mode 100644 index 000000000..f0f11033c --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/ErrorExtensionProvider.java @@ -0,0 +1,13 @@ +package io.smallrye.graphql.api; + +import jakarta.json.JsonValue; + +/** + * To add you own GraphQL error extension fields, you can add your own implementations + * of this class via the {@link java.util.ServiceLoader ServiceLoader} mechanism. + */ +public interface ErrorExtensionProvider { + String getKey(); + + JsonValue mapValueFrom(Throwable exception); +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java index 566fe4b7d..7e20f1642 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java @@ -59,9 +59,7 @@ public JsonObject getExecutionResultAsJsonObject() { // Extensions returnObjectBuilder = addExtensionsToResponse(returnObjectBuilder, executionResult); - JsonObject jsonResponse = returnObjectBuilder.build(); - - return jsonResponse; + return returnObjectBuilder.build(); } public String getExecutionResultAsString() { @@ -75,10 +73,8 @@ private JsonObjectBuilder addErrorsToResponse(JsonObjectBuilder returnObjectBuil if (!jsonArray.isEmpty()) { returnObjectBuilder = returnObjectBuilder.add(ERRORS, jsonArray); } - return returnObjectBuilder; - } else { - return returnObjectBuilder; } + return returnObjectBuilder; } private JsonObjectBuilder addDataToResponse(JsonObjectBuilder returnObjectBuilder, ExecutionResult executionResult) { @@ -123,7 +119,7 @@ private JsonObject buildExtensions(final Map extensions) { * GraphQL returns a limited set of values ({@code Collection}, {@code Map}, {@code Number}, {@code Boolean}, {@code Enum}), * so the json value is build by hand. * Additionally, {@code JsonB} is used as a fallback if an different type is encountered. - * + * * @param pojo a java object, limited to {@code Collection}, {@code Map}, {@code Number}, {@code Boolean} and {@code Enum} * @return the json value */ diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java new file mode 100644 index 000000000..82c4fa531 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java @@ -0,0 +1,36 @@ +package io.smallrye.graphql.execution.error; + +import static java.util.Locale.ROOT; + +import jakarta.json.Json; +import jakarta.json.JsonValue; + +import io.smallrye.graphql.api.ErrorExtensionProvider; +import io.smallrye.graphql.schema.model.ErrorInfo; +import io.smallrye.graphql.spi.config.Config; + +public class ErrorCodeExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return Config.ERROR_EXTENSION_CODE; + } + + @Override + public JsonValue mapValueFrom(Throwable exception) { + return Json.createValue(errorCode(exception)); + } + + private String errorCode(Throwable exception) { + ErrorInfo errorInfo = ErrorInfoMap.getErrorInfo(exception.getClass().getName()); + if (errorInfo == null) { + return camelToKebab(exception.getClass().getSimpleName().replaceAll("Exception$", "")); + } else { + return errorInfo.getErrorCode(); + } + } + + private static String camelToKebab(String input) { + return String.join("-", input.split("(?=\\p{javaUpperCase})")) + .toLowerCase(ROOT); + } +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorInfoMap.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorInfoMap.java index c6677012f..38ab28b80 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorInfoMap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorInfoMap.java @@ -7,7 +7,7 @@ /** * Here we create a mapping of all error info that we know about - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class ErrorInfoMap { @@ -23,10 +23,6 @@ public static void register(Map map) { } } - public static boolean hasErrorInfo(String className) { - return errorInfoMap.containsKey(className); - } - public static ErrorInfo getErrorInfo(String className) { if (errorInfoMap.containsKey(className)) { return errorInfoMap.get(className); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java new file mode 100644 index 000000000..d3fc216e5 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java @@ -0,0 +1,19 @@ +package io.smallrye.graphql.execution.error; + +import jakarta.json.Json; +import jakarta.json.JsonString; + +import io.smallrye.graphql.api.ErrorExtensionProvider; +import io.smallrye.graphql.spi.config.Config; + +public class ExceptionNameErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return Config.ERROR_EXTENSION_EXCEPTION; + } + + @Override + public JsonString mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getName()); + } +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java index a41508247..71d60b5b3 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java @@ -1,11 +1,10 @@ package io.smallrye.graphql.execution.error; -import static java.util.Locale.UK; - import java.io.StringReader; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.ServiceLoader; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -15,6 +14,7 @@ import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonReader; import jakarta.json.JsonReaderFactory; +import jakarta.json.JsonValue; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; @@ -22,7 +22,7 @@ import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; import graphql.validation.ValidationError; -import io.smallrye.graphql.schema.model.ErrorInfo; +import io.smallrye.graphql.api.ErrorExtensionProvider; import io.smallrye.graphql.spi.config.Config; /** @@ -38,7 +38,7 @@ public class ExecutionErrorsService { .withNullValues(Boolean.TRUE) .withFormatting(Boolean.TRUE)); - private Config config = Config.get(); + private final Config config = Config.get(); public JsonArray toJsonErrors(List errors) { JsonArrayBuilder arrayBuilder = jsonBuilderFactory.createArrayBuilder(); @@ -71,7 +71,6 @@ private Optional getOptionalExtensions(GraphQLError error) { } private Optional getValidationExtensions(ValidationError error) { - if (config.getErrorExtensionFields().isPresent()) { JsonObjectBuilder objectBuilder = jsonBuilderFactory.createObjectBuilder(); addKeyValue(objectBuilder, Config.ERROR_EXTENSION_DESCRIPTION, error.getDescription()); @@ -90,9 +89,8 @@ private Optional getDataFetchingExtensions(ExceptionWhileDataFetchin Throwable exception = error.getException(); JsonObjectBuilder objectBuilder = jsonBuilderFactory.createObjectBuilder(); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_EXCEPTION, exception.getClass().getName()); addKeyValue(objectBuilder, Config.ERROR_EXTENSION_CLASSIFICATION, error.getErrorType().toString()); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_CODE, toErrorCode(exception)); + addErrorExtensions(objectBuilder, exception); Map extensions = error.getExtensions(); populateCustomExtensions(objectBuilder, extensions); @@ -101,24 +99,15 @@ private Optional getDataFetchingExtensions(ExceptionWhileDataFetchin return Optional.empty(); } - private String toErrorCode(Throwable exception) { - String exceptionClassName = exception.getClass().getName(); - if (ErrorInfoMap.hasErrorInfo(exceptionClassName)) { - ErrorInfo errorInfo = ErrorInfoMap.getErrorInfo(exceptionClassName); - return errorInfo.getErrorCode(); - } - return camelToKebab(exception.getClass().getSimpleName().replaceAll("Exception$", "")); - } - - private static String camelToKebab(String input) { - return String.join("-", input.split("(?=\\p{javaUpperCase})")) - .toLowerCase(UK); + private void addErrorExtensions(JsonObjectBuilder objectBuilder, Throwable exception) { + ServiceLoader.load(ErrorExtensionProvider.class) + .forEach(provider -> addKeyValue(objectBuilder, provider.getKey(), provider.mapValueFrom(exception))); } private void populateCustomExtensions(JsonObjectBuilder objectBuilder, Map extensions) { if (extensions != null) { for (Map.Entry entry : extensions.entrySet()) { - if (!config.getErrorExtensionFields().isPresent() + if (config.getErrorExtensionFields().isEmpty() || (config.getErrorExtensionFields().isPresent() && config.getErrorExtensionFields().get().contains(entry.getKey()))) { addKeyValue(objectBuilder, entry.getKey(), entry.getValue().toString()); @@ -139,7 +128,10 @@ private JsonArray toJsonArray(List list) { } private void addKeyValue(JsonObjectBuilder objectBuilder, String key, String value) { + addKeyValue(objectBuilder, key, Json.createValue(value)); + } + private void addKeyValue(JsonObjectBuilder objectBuilder, String key, JsonValue value) { if (config.getErrorExtensionFields().isPresent()) { List fieldsThatShouldBeIncluded = config.getErrorExtensionFields().get(); if (fieldsThatShouldBeIncluded.contains(key)) { diff --git a/server/implementation/src/main/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider b/server/implementation/src/main/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider new file mode 100644 index 000000000..f950454e8 --- /dev/null +++ b/server/implementation/src/main/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider @@ -0,0 +1,2 @@ +io.smallrye.graphql.execution.error.ErrorCodeExtensionProvider +io.smallrye.graphql.execution.error.ExceptionNameErrorExtensionProvider diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/MutinyTest.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/MutinyTest.java index 781262bfd..0e92f107f 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/execution/MutinyTest.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/MutinyTest.java @@ -11,6 +11,8 @@ import org.jboss.jandex.IndexView; import org.junit.jupiter.api.Test; +import io.smallrye.graphql.test.mutiny.CustomException; + public class MutinyTest extends ExecutionTestBase { protected IndexView getIndex() { @@ -37,10 +39,11 @@ public void testFailureQuery() { assertNotNull(errors); assertEquals(errors.size(), 1); - - String code = errors.get(0).asJsonObject().getJsonObject("extensions").getString("code"); - - assertEquals(code, "custom-error", "expected error code: custom-error"); + var extensions = errors.get(0).asJsonObject().getJsonObject("extensions"); + assertEquals("custom-error", extensions.getString("code"), "error code"); + assertEquals(CustomException.class.getName(), extensions.getString("exception"), "exception"); + assertEquals("DataFetchingException", extensions.getString("classification"), "classification"); + assertEquals(CustomException.class.getSimpleName().length(), extensions.getInt("test-extension"), "test extension"); } private static final String TEST_QUERY = "{\n" + diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java index 09c32f8f1..d5ac3d4d4 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java @@ -10,7 +10,7 @@ /** * Implements the config for testing - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class TestConfig implements Config { @@ -32,9 +32,9 @@ public LogPayloadOption logPayload() { @Override public Optional> getErrorExtensionFields() { - return Optional - .of(Arrays.asList(new String[] { "exception", "classification", "code", "description", - "validationErrorType", "queryPath" })); + return Optional.of(Arrays.asList( + "exception", "classification", "code", "description", + "validationErrorType", "queryPath", "test-extension")); } @Override @@ -45,6 +45,7 @@ public boolean isIncludeDirectivesInSchema() { @Override public T getConfigValue(String key, Class type, T defaultValue) { if (key.equals(TestEventingService.KEY)) { + //noinspection unchecked return (T) Boolean.TRUE; } return defaultValue; diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/TestErrorExtensionProvider.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestErrorExtensionProvider.java new file mode 100644 index 000000000..d33305d8c --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestErrorExtensionProvider.java @@ -0,0 +1,18 @@ +package io.smallrye.graphql.execution; + +import jakarta.json.Json; +import jakarta.json.JsonNumber; + +import io.smallrye.graphql.api.ErrorExtensionProvider; + +public class TestErrorExtensionProvider implements ErrorExtensionProvider { + @Override + public String getKey() { + return "test-extension"; + } + + @Override + public JsonNumber mapValueFrom(Throwable exception) { + return Json.createValue(exception.getClass().getSimpleName().length()); + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/mutiny/MutinyBookGraphQLApi.java b/server/implementation/src/test/java/io/smallrye/graphql/test/mutiny/MutinyBookGraphQLApi.java index 597a98e99..8acdfcd16 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/test/mutiny/MutinyBookGraphQLApi.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/mutiny/MutinyBookGraphQLApi.java @@ -21,11 +21,12 @@ public Uni getUniBook(String name) { } @Query("failedBook") - public Uni failedBook(String name) { + public Uni failedBook(@SuppressWarnings("unused") String name) { return Uni.createFrom().failure(new CustomException()); } - private static Map BOOKS = new HashMap<>(); + private static final Map BOOKS = new HashMap<>(); + static { Book book1 = new Book("0-571-05686-5", "Lord of the Flies", LocalDate.of(1954, Month.SEPTEMBER, 17), "William Golding"); BOOKS.put(book1.title, book1); diff --git a/server/implementation/src/test/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider b/server/implementation/src/test/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider new file mode 100644 index 000000000..19bcdd157 --- /dev/null +++ b/server/implementation/src/test/resources/META-INF/services/io.smallrye.graphql.api.ErrorExtensionProvider @@ -0,0 +1 @@ +io.smallrye.graphql.execution.TestErrorExtensionProvider