diff --git a/README.md b/README.md index 60acdad..bf4964c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ Production-oriented JSON-RPC 2.0 server library for Java, with optional Spring W - Spring WebMVC transport adapter and Spring Boot auto-configuration - Multiple registration styles (annotation/manual/typed) - Explicit extension points (parser/validator/invoker/exception mapping/interceptors/metrics) -- Response-side protocol utilities for bidirectional transports (classifier/parser/validator) +- Response-side protocol utilities for bidirectional transports (envelope classifier, error-code classifier, parser, + validator) - Focused dependency surface (no direct Guava, Commons Lang3, or Jakarta Validation dependency) ## Specification diff --git a/docs/protocol-and-compliance.md b/docs/protocol-and-compliance.md index 62a8deb..dc072c1 100644 --- a/docs/protocol-and-compliance.md +++ b/docs/protocol-and-compliance.md @@ -51,6 +51,7 @@ Implementation constants are in `JsonRpcErrorCode` and messages in `JsonRpcConst `jsonrpc-core` also provides response-side protocol utilities: - `JsonRpcEnvelopeClassifier` +- `JsonRpcErrorClassifier` - `JsonRpcResponseParser` - `JsonRpcResponseValidator` - `JsonRpcResponseValidationOptions` @@ -61,6 +62,15 @@ members during raw JSON parsing. These APIs are transport-agnostic and useful for bidirectional channels (for example WebSocket) where request/response envelopes may arrive on the same connection. +`JsonRpcErrorClassifier` interprets integer `error.code` values as: + +- `STANDARD`: `-32700`, `-32600`, `-32601`, `-32602`, `-32603` +- `SERVER_RESERVED_RANGE`: `-32099..-32000` +- `CUSTOM`: any other integer value + +`CUSTOM` does not mean the code is invalid. It only means the code is outside the standard set and the reserved +server-error range. + ### Default Validation Rules (RFC MUST) By default, `JsonRpcResponseValidationOptions.defaults()` enforces: diff --git a/docs/pure-java-guide.md b/docs/pure-java-guide.md index 68a5324..2003ce6 100644 --- a/docs/pure-java-guide.md +++ b/docs/pure-java-guide.md @@ -350,9 +350,12 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import com.limehee.jsonrpc.core.DefaultJsonRpcEnvelopeClassifier; +import com.limehee.jsonrpc.core.DefaultJsonRpcErrorClassifier; import com.limehee.jsonrpc.core.DefaultJsonRpcResponseParser; import com.limehee.jsonrpc.core.DefaultJsonRpcResponseValidator; import com.limehee.jsonrpc.core.JsonRpcEnvelopeClassifier; +import com.limehee.jsonrpc.core.JsonRpcErrorClassifier; +import com.limehee.jsonrpc.core.JsonRpcErrorCodeCategory; import com.limehee.jsonrpc.core.JsonRpcEnvelopeType; import com.limehee.jsonrpc.core.JsonRpcIncomingResponse; import com.limehee.jsonrpc.core.JsonRpcIncomingResponseEnvelope; @@ -361,6 +364,7 @@ import com.limehee.jsonrpc.core.JsonRpcResponseValidator; ObjectMapper mapper = JsonMapper.builder().build(); JsonRpcEnvelopeClassifier classifier = new DefaultJsonRpcEnvelopeClassifier(); +JsonRpcErrorClassifier errorClassifier = new DefaultJsonRpcErrorClassifier(); JsonRpcResponseParser responseParser = new DefaultJsonRpcResponseParser(); JsonRpcResponseValidator responseValidator = new DefaultJsonRpcResponseValidator(); @@ -373,6 +377,11 @@ if (envelopeType == JsonRpcEnvelopeType.REQUEST) { JsonRpcIncomingResponseEnvelope envelope = responseParser.parse(payload); for (JsonRpcIncomingResponse response : envelope.responses()) { responseValidator.validate(response); + if (response.errorPresent() && response.error() != null && response.error().has("code")) { + int code = response.error().get("code").asInt(); + JsonRpcErrorCodeCategory category = errorClassifier.classify(code); + System.out.println("error.code category = " + category); + } // route by response.id() to pending-call registry } } else { @@ -383,6 +392,12 @@ if (envelopeType == JsonRpcEnvelopeType.REQUEST) { For policy tuning, customize `JsonRpcResponseValidationOptions` and pass it into `DefaultJsonRpcResponseValidator`. +`JsonRpcErrorClassifier` categories: + +- `STANDARD`: one of `-32700`, `-32600`, `-32601`, `-32602`, `-32603` +- `SERVER_RESERVED_RANGE`: inside `-32099..-32000` +- `CUSTOM`: any other integer code + ## 10. Concurrency Notes - `JsonRpcDispatcher` invocation path is stateless per request except method registry lookups. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifier.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifier.java new file mode 100644 index 0000000..32cfedb --- /dev/null +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifier.java @@ -0,0 +1,25 @@ +package com.limehee.jsonrpc.core; + +/** + * Default classifier for integer JSON-RPC {@code error.code} values. + */ +public class DefaultJsonRpcErrorClassifier implements JsonRpcErrorClassifier { + + /** + * {@inheritDoc} + */ + @Override + public JsonRpcErrorCodeCategory classify(int code) { + if (code == JsonRpcErrorCode.PARSE_ERROR + || code == JsonRpcErrorCode.INVALID_REQUEST + || code == JsonRpcErrorCode.METHOD_NOT_FOUND + || code == JsonRpcErrorCode.INVALID_PARAMS + || code == JsonRpcErrorCode.INTERNAL_ERROR) { + return JsonRpcErrorCodeCategory.STANDARD; + } + if (code >= -32099 && code <= -32000) { + return JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE; + } + return JsonRpcErrorCodeCategory.CUSTOM; + } +} diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidator.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidator.java index 96cf6a3..0f95a1e 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidator.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidator.java @@ -6,9 +6,16 @@ /** * Default validator for parsed incoming JSON-RPC responses. + *

+ * Integer {@code error.code} validation uses {@link JsonRpcErrorClassifier} for standard and server-range policies. + * {@link JsonRpcResponseErrorCodePolicy#CUSTOM_RANGE} remains a direct numeric bounds check and does not depend on the + * classifier's category result. + *

*/ public class DefaultJsonRpcResponseValidator implements JsonRpcResponseValidator { + private static final JsonRpcErrorClassifier ERROR_CLASSIFIER = new DefaultJsonRpcErrorClassifier(); + private final JsonRpcResponseValidationOptions options; /** @@ -148,55 +155,26 @@ private JsonRpcException invalidRequest() { * @param code integer error code */ private void validateErrorCodePolicy(int code) { - JsonRpcResponseErrorCodePolicy policy = options.errorCodePolicy(); - if (policy == JsonRpcResponseErrorCodePolicy.ANY_INTEGER) { - return; - } - if (policy == JsonRpcResponseErrorCodePolicy.STANDARD_ONLY) { - if (!isStandardErrorCode(code)) { - throw invalidRequest(); + switch (options.errorCodePolicy()) { + case ANY_INTEGER -> { } - return; - } - if (policy == JsonRpcResponseErrorCodePolicy.STANDARD_OR_SERVER_ERROR_RANGE) { - if (!isStandardErrorCode(code) && !isServerErrorRangeCode(code)) { - throw invalidRequest(); + case STANDARD_ONLY -> { + if (!ERROR_CLASSIFIER.isStandard(code)) { + throw invalidRequest(); + } } - return; - } - if (policy == JsonRpcResponseErrorCodePolicy.CUSTOM_RANGE) { - Integer min = options.errorCodeRangeMin(); - Integer max = options.errorCodeRangeMax(); - if (min == null || max == null) { - throw invalidRequest(); + case STANDARD_OR_SERVER_ERROR_RANGE -> { + if (!ERROR_CLASSIFIER.isStandard(code) && !ERROR_CLASSIFIER.isServerErrorRange(code)) { + throw invalidRequest(); + } } - if (code < min || code > max) { - throw invalidRequest(); + case CUSTOM_RANGE -> { + Integer min = options.errorCodeRangeMin(); + Integer max = options.errorCodeRangeMax(); + if (min == null || max == null || code < min || code > max) { + throw invalidRequest(); + } } } } - - /** - * Checks whether a code is one of the JSON-RPC standard error codes. - * - * @param code integer error code - * @return {@code true} when code is standard - */ - private boolean isStandardErrorCode(int code) { - return code == JsonRpcErrorCode.PARSE_ERROR - || code == JsonRpcErrorCode.INVALID_REQUEST - || code == JsonRpcErrorCode.METHOD_NOT_FOUND - || code == JsonRpcErrorCode.INVALID_PARAMS - || code == JsonRpcErrorCode.INTERNAL_ERROR; - } - - /** - * Checks whether a code belongs to the JSON-RPC server-error reserved range. - * - * @param code integer error code - * @return {@code true} when code is within {@code -32099..-32000} - */ - private boolean isServerErrorRangeCode(int code) { - return code >= -32099 && code <= -32000; - } } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorClassifier.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorClassifier.java new file mode 100644 index 0000000..01ed9b2 --- /dev/null +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorClassifier.java @@ -0,0 +1,49 @@ +package com.limehee.jsonrpc.core; + +/** + * Classifies integer JSON-RPC {@code error.code} values into semantic categories. + *

+ * This utility describes what a code represents. It does not decide whether a response should be accepted or rejected + * under a validation policy. + *

+ */ +public interface JsonRpcErrorClassifier { + + /** + * Classifies the provided integer error code. + * + * @param code integer JSON-RPC error code + * @return semantic error-code category + */ + JsonRpcErrorCodeCategory classify(int code); + + /** + * Checks whether the provided code is one of the JSON-RPC standard error codes. + * + * @param code integer JSON-RPC error code + * @return {@code true} when the code belongs to the standard set + */ + default boolean isStandard(int code) { + return classify(code) == JsonRpcErrorCodeCategory.STANDARD; + } + + /** + * Checks whether the provided code belongs to the server-reserved range. + * + * @param code integer JSON-RPC error code + * @return {@code true} when the code is within {@code -32099..-32000} + */ + default boolean isServerErrorRange(int code) { + return classify(code) == JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE; + } + + /** + * Checks whether the provided code is neither standard nor server-reserved. + * + * @param code integer JSON-RPC error code + * @return {@code true} when the code falls into the custom category + */ + default boolean isCustom(int code) { + return classify(code) == JsonRpcErrorCodeCategory.CUSTOM; + } +} diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorCodeCategory.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorCodeCategory.java new file mode 100644 index 0000000..575fb20 --- /dev/null +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcErrorCodeCategory.java @@ -0,0 +1,26 @@ +package com.limehee.jsonrpc.core; + +/** + * Semantic category for an integer JSON-RPC {@code error.code}. + */ +public enum JsonRpcErrorCodeCategory { + + /** + * One of the JSON-RPC 2.0 standard error codes. + */ + STANDARD, + + /** + * Value inside the server-reserved range {@code -32099..-32000}. + */ + SERVER_RESERVED_RANGE, + + /** + * Any integer outside the standard set and the server-reserved range. + *

+ * This category does not mean the code is invalid. It only means the code is application-defined or otherwise + * outside the standard and reserved sets. + *

+ */ + CUSTOM +} diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifierTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifierTest.java new file mode 100644 index 0000000..2758252 --- /dev/null +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcErrorClassifierTest.java @@ -0,0 +1,52 @@ +package com.limehee.jsonrpc.core; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DefaultJsonRpcErrorClassifierTest { + + private final JsonRpcErrorClassifier classifier = new DefaultJsonRpcErrorClassifier(); + + @Test + void classifyReturnsStandardForAllStandardJsonRpcErrorCodes() { + assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.PARSE_ERROR)); + assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INVALID_REQUEST)); + assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.METHOD_NOT_FOUND)); + assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INVALID_PARAMS)); + assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INTERNAL_ERROR)); + } + + @Test + void classifyReturnsServerReservedRangeForBoundaryValues() { + assertSame(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, classifier.classify(-32099)); + assertSame(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, classifier.classify(-32000)); + } + + @Test + void classifyReturnsCustomForAllNonStandardNonReservedValues() { + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-32100)); + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-31999)); + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-40000)); + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(0)); + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(1)); + assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(1001)); + } + + @Test + void helperMethodsReflectClassificationResult() { + assertTrue(classifier.isStandard(JsonRpcErrorCode.METHOD_NOT_FOUND)); + assertFalse(classifier.isServerErrorRange(JsonRpcErrorCode.METHOD_NOT_FOUND)); + assertFalse(classifier.isCustom(JsonRpcErrorCode.METHOD_NOT_FOUND)); + + assertTrue(classifier.isServerErrorRange(-32050)); + assertFalse(classifier.isStandard(-32050)); + assertFalse(classifier.isCustom(-32050)); + + assertTrue(classifier.isCustom(1001)); + assertFalse(classifier.isStandard(1001)); + assertFalse(classifier.isServerErrorRange(1001)); + } +} diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidatorTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidatorTest.java index b3557fe..cf21477 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidatorTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseValidatorTest.java @@ -359,11 +359,38 @@ void validateRejectsNonStandardErrorCodeWhenPolicyIsStandardOnly() throws Except ); JsonRpcException ex = assertThrows(JsonRpcException.class, () -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"server"}} + {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"server"}} + """))); + assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); + } + + @Test + void validateRejectsCustomErrorCodeWhenPolicyIsStandardOnly() throws Exception { + JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( + JsonRpcResponseValidationOptions.builder() + .errorCodePolicy(JsonRpcResponseErrorCodePolicy.STANDARD_ONLY) + .build() + ); + + JsonRpcException ex = assertThrows(JsonRpcException.class, () -> custom.validate(incoming(""" + {"jsonrpc":"2.0","id":1,"error":{"code":1001,"message":"custom"}} """))); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); } + @Test + void validateAllowsStandardErrorCodeWhenPolicyIsStandardOnly() throws Exception { + JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( + JsonRpcResponseValidationOptions.builder() + .errorCodePolicy(JsonRpcResponseErrorCodePolicy.STANDARD_ONLY) + .build() + ); + + assertDoesNotThrow(() -> custom.validate(incoming(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}} + """))); + } + @Test void validateAllowsServerErrorRangeWhenPolicyConfigured() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( @@ -375,6 +402,9 @@ void validateAllowsServerErrorRangeWhenPolicyConfigured() throws Exception { assertDoesNotThrow(() -> custom.validate(incoming(""" {"jsonrpc":"2.0","id":1,"error":{"code":-32050,"message":"server"}} """))); + assertDoesNotThrow(() -> custom.validate(incoming(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}} + """))); } @Test @@ -437,6 +467,37 @@ void validateAllowsInRangeCustomErrorCode() throws Exception { """))); } + @Test + void validateAllowsStandardCodeWhenCustomRangeContainsIt() throws Exception { + JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( + JsonRpcResponseValidationOptions.builder() + .errorCodePolicy(JsonRpcResponseErrorCodePolicy.CUSTOM_RANGE) + .errorCodeRangeMin(-40000) + .errorCodeRangeMax(-30000) + .build() + ); + + assertDoesNotThrow(() -> custom.validate(incoming(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}} + """))); + } + + @Test + void validateRejectsStandardCodeWhenCustomRangeDoesNotContainIt() throws Exception { + JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( + JsonRpcResponseValidationOptions.builder() + .errorCodePolicy(JsonRpcResponseErrorCodePolicy.CUSTOM_RANGE) + .errorCodeRangeMin(1000) + .errorCodeRangeMax(2000) + .build() + ); + + JsonRpcException ex = assertThrows(JsonRpcException.class, () -> custom.validate(incoming(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}} + """))); + assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); + } + private JsonRpcIncomingResponse incoming(String json) throws Exception { JsonNode payload = OBJECT_MAPPER.readTree(json); return PARSER.parse(payload).singleResponse().orElseThrow(); diff --git a/samples/pure-java-demo/README.md b/samples/pure-java-demo/README.md index 5b6ea4e..bdcfa90 100644 --- a/samples/pure-java-demo/README.md +++ b/samples/pure-java-demo/README.md @@ -31,7 +31,7 @@ From repository root: `reject-response-fields`) - Response validation profile with `JsonRpcResponseValidationOptions` (`reject-request-fields`, `reject-duplicate-members`, `error-code.policy`) -- Incoming response-side flow using classifier/parser/validator utilities +- Incoming response-side flow using envelope classifier, error-code classifier, parser, and validator utilities - Interceptor lifecycle flow (`beforeValidate`, `beforeInvoke`, `afterInvoke`, `onError`) - Outbound request composition using `JsonRpcRequestBuilder` and `JsonRpcRequestBatchBuilder` - Direct `paramsObject(...)` and `paramsArray(...)` builder examples for outbound requests diff --git a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplication.java b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplication.java index 507d076..5093a78 100644 --- a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplication.java +++ b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplication.java @@ -19,6 +19,7 @@ import com.limehee.jsonrpc.core.JsonRpcIncomingResponse; import com.limehee.jsonrpc.core.JsonRpcMethodRegistrationConflictPolicy; import com.limehee.jsonrpc.core.JsonRpcParamsTypeViolationCodePolicy; +import com.limehee.jsonrpc.core.JsonRpcResponseValidationOptions; import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; import tools.jackson.databind.node.StringNode; @@ -65,6 +66,18 @@ public static void main(String[] args) throws JacksonException { {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"server"}} """); print("strict response profile", OBJECT_MAPPER.writeValueAsString(strictResponses)); + ResponseSideUtilitiesExample responseUtilities = new ResponseSideUtilitiesExample( + JsonRpcResponseValidationOptions.defaults() + ); + print("response error-code categories", OBJECT_MAPPER.writeValueAsString( + responseUtilities.classifyValidatedErrorCodes(""" + [ + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}, + {"jsonrpc":"2.0","id":2,"error":{"code":-32001,"message":"Server issue"}}, + {"jsonrpc":"2.0","id":3,"error":{"code":1001,"message":"Domain error"}} + ] + """) + )); print("outbound request", OBJECT_MAPPER.writeValueAsString( OutboundRequestCompositionExample.buildInventoryLookupRequest() )); diff --git a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExample.java b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExample.java index eca02c1..6da3d7e 100644 --- a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExample.java +++ b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExample.java @@ -5,9 +5,12 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import com.limehee.jsonrpc.core.DefaultJsonRpcEnvelopeClassifier; +import com.limehee.jsonrpc.core.DefaultJsonRpcErrorClassifier; import com.limehee.jsonrpc.core.DefaultJsonRpcResponseParser; import com.limehee.jsonrpc.core.DefaultJsonRpcResponseValidator; import com.limehee.jsonrpc.core.JsonRpcEnvelopeClassifier; +import com.limehee.jsonrpc.core.JsonRpcErrorClassifier; +import com.limehee.jsonrpc.core.JsonRpcErrorCodeCategory; import com.limehee.jsonrpc.core.JsonRpcEnvelopeType; import com.limehee.jsonrpc.core.JsonRpcIncomingResponse; import com.limehee.jsonrpc.core.JsonRpcIncomingResponseEnvelope; @@ -22,11 +25,13 @@ public final class ResponseSideUtilitiesExample { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); private final JsonRpcEnvelopeClassifier classifier; + private final JsonRpcErrorClassifier errorClassifier; private final DefaultJsonRpcResponseParser parser; private final JsonRpcResponseValidator validator; public ResponseSideUtilitiesExample(JsonRpcResponseValidationOptions options) { this.classifier = new DefaultJsonRpcEnvelopeClassifier(); + this.errorClassifier = new DefaultJsonRpcErrorClassifier(); this.parser = new DefaultJsonRpcResponseParser(options.rejectDuplicateMembers()); this.validator = new DefaultJsonRpcResponseValidator(options); } @@ -47,7 +52,54 @@ public Result inspect(String rawMessage) throws JacksonException { return new Result(envelopeType, validated); } + /** + * Classifies a validated or application-provided integer JSON-RPC error code. + * + * @param code integer JSON-RPC error code + * @return semantic error-code category + */ + public JsonRpcErrorCodeCategory classifyErrorCode(int code) { + return errorClassifier.classify(code); + } + + /** + * Parses, validates, and classifies integer response error codes found in the provided payload. + * + * @param rawMessage raw JSON message that may contain response envelopes + * @return classified error-code entries extracted from validated responses + * @throws JacksonException when parsing or validation fails + */ + public List classifyValidatedErrorCodes(String rawMessage) throws JacksonException { + Result result = inspect(rawMessage); + if (result.envelopeType() != JsonRpcEnvelopeType.RESPONSE) { + return List.of(); + } + + List classified = new ArrayList<>(); + for (JsonRpcIncomingResponse response : result.responses()) { + if (!response.errorPresent() || response.error() == null || !response.error().isObject()) { + continue; + } + JsonNode code = response.error().get("code"); + if (code == null || !code.isNumber() || code.isFloatingPointNumber()) { + continue; + } + classified.add(new ClassifiedErrorCode(code.intValue(), errorClassifier.classify(code.intValue()))); + } + return classified; + } + public record Result(JsonRpcEnvelopeType envelopeType, List responses) { } + + /** + * Classified integer error-code entry extracted from a validated response. + * + * @param code integer JSON-RPC error code + * @param category semantic classifier result + */ + public record ClassifiedErrorCode(int code, JsonRpcErrorCodeCategory category) { + + } } diff --git a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExampleTest.java b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExampleTest.java index cf5b94d..ea1a6b6 100644 --- a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExampleTest.java +++ b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/ResponseSideUtilitiesExampleTest.java @@ -1,7 +1,9 @@ package com.limehee.jsonrpc.sample.purejava; import com.limehee.jsonrpc.core.JsonRpcEnvelopeType; +import com.limehee.jsonrpc.core.JsonRpcErrorCodeCategory; import com.limehee.jsonrpc.core.JsonRpcException; +import com.limehee.jsonrpc.core.JsonRpcResponseErrorCodePolicy; import com.limehee.jsonrpc.core.JsonRpcResponseValidationOptions; import org.junit.jupiter.api.Test; @@ -95,4 +97,49 @@ void rejectsRequestFieldsInsideResponseWhenConfigured() { {"jsonrpc":"2.0","id":1,"result":"pong","method":"ping"} """)); } + + @Test + void classifiesStandardServerRangeAndCustomErrorCodes() { + ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( + JsonRpcResponseValidationOptions.defaults() + ); + + assertEquals(JsonRpcErrorCodeCategory.STANDARD, example.classifyErrorCode(-32601)); + assertEquals(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, example.classifyErrorCode(-32001)); + assertEquals(JsonRpcErrorCodeCategory.CUSTOM, example.classifyErrorCode(1001)); + } + + @Test + void classifiesValidatedErrorCodesFromResponsePayload() throws Exception { + ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( + JsonRpcResponseValidationOptions.builder() + .errorCodePolicy(JsonRpcResponseErrorCodePolicy.ANY_INTEGER) + .build() + ); + + var classified = example.classifyValidatedErrorCodes(""" + [ + {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}, + {"jsonrpc":"2.0","id":2,"error":{"code":-32001,"message":"Server issue"}}, + {"jsonrpc":"2.0","id":3,"error":{"code":1001,"message":"Domain error"}}, + {"jsonrpc":"2.0","id":4,"result":"ok"} + ] + """); + + assertEquals(3, classified.size()); + assertEquals(JsonRpcErrorCodeCategory.STANDARD, classified.get(0).category()); + assertEquals(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, classified.get(1).category()); + assertEquals(JsonRpcErrorCodeCategory.CUSTOM, classified.get(2).category()); + } + + @Test + void skipsErrorCodeClassificationWhenPayloadIsNotResponseEnvelope() throws Exception { + ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( + JsonRpcResponseValidationOptions.defaults() + ); + + assertTrue(example.classifyValidatedErrorCodes(""" + {"jsonrpc":"2.0","method":"ping","id":1} + """).isEmpty()); + } }