diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0de6d40..6e2c8f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ Thanks for contributing to `jsonrpc-spring-boot-starter`. ## Scope This project provides JSON-RPC 2.0 server components for Spring Boot: + - core protocol/dispatch module - Spring WebMVC transport module - Spring Boot auto-configuration and starter @@ -20,6 +21,7 @@ Protocol behavior should stay aligned with the JSON-RPC 2.0 specification. ## Development Setup Requirements: + - JDK 17+ - Gradle wrapper (`./gradlew`) @@ -32,26 +34,36 @@ Common commands: ./gradlew -p samples/spring-boot-demo classes ``` +Null-safety gate: + +- Production code (`compile*Java` except test source sets) is validated by NullAway during `check`. +- Test source sets are intentionally excluded from NullAway to keep tests focused on behavior assertions. +- If NullAway fails, fix nullable contracts (`@Nullable`, guard clauses, fallback defaults) before opening a PR. + ## Coding Guidelines - Follow existing module boundaries and abstraction style. - Preserve JSON-RPC 2.0 compliance. - Add or update tests for: - - success paths - - failure paths - - exception/edge branches + - success paths + - failure paths + - exception/edge branches - Keep public API behavior backward compatible unless a breaking change is intentional and documented. ## Issue Labels and Triage This repository uses a two-axis label taxonomy: + - `type:*` labels classify issue category (`type: bug`, `type: feature`, etc.). -- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`, `status: waiting-for-feedback`). +- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`, + `status: waiting-for-feedback`). Rules: + 1. Every issue template must define exactly one `type:*` label and exactly one `status:*` label. 2. Only one `status:*` label should be present on an issue at a time. -3. Automated triage keeps status labels normalized on open/reopen/label events and can remove `status: waiting-for-feedback` when the issue author replies. +3. Automated triage keeps status labels normalized on open/reopen/label events and can remove + `status: waiting-for-feedback` when the issue author replies. ## Commit and PR Guidelines diff --git a/build.gradle b/build.gradle index 2f4a3ab..f703403 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,10 @@ import me.champeau.gradle.japicmp.JapicmpTask +import net.ltgt.gradle.errorprone.CheckSeverity plugins { id 'base' alias(libs.plugins.japicmp) apply false + alias(libs.plugins.errorprone) apply false } allprojects { @@ -15,6 +17,7 @@ subprojects { apply plugin: 'java-library' apply plugin: 'maven-publish' apply plugin: 'signing' + apply plugin: 'net.ltgt.errorprone' java { toolchain { @@ -33,11 +36,23 @@ subprojects { testImplementation libs.junit.jupiter testRuntimeOnly libs.junit.platform.launcher compileOnly libs.jspecify + errorprone libs.errorprone.core + errorprone libs.nullaway } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' options.compilerArgs += ['-parameters'] + + def isTestCompile = name.toLowerCase().contains('test') + options.errorprone.enabled = !isTestCompile + options.errorprone.disableWarningsInGeneratedCode = true + if (!isTestCompile) { + options.errorprone.disableAllChecks = true + options.errorprone.check('NullAway', CheckSeverity.ERROR) + options.errorprone.option('NullAway:AnnotatedPackages', 'com.limehee.jsonrpc') + options.errorprone.option('NullAway:JSpecifyMode', 'true') + } } tasks.withType(Test).configureEach { @@ -162,10 +177,10 @@ subprojects { } def publishedApiModules = [ - project(':jsonrpc-core'), - project(':jsonrpc-spring-webmvc'), - project(':jsonrpc-spring-boot-autoconfigure'), - project(':jsonrpc-spring-boot-starter') + project(':jsonrpc-core'), + project(':jsonrpc-spring-webmvc'), + project(':jsonrpc-spring-boot-autoconfigure'), + project(':jsonrpc-spring-boot-starter') ] def apiBaselineVersionProvider = providers.gradleProperty('apiBaselineVersion') diff --git a/docs/configuration-reference.md b/docs/configuration-reference.md index d86d258..6923e72 100644 --- a/docs/configuration-reference.md +++ b/docs/configuration-reference.md @@ -4,35 +4,35 @@ All properties are under `jsonrpc.*` and are bound to `JsonRpcProperties`. ## 1. Property Table -| Key | Type | Default | Description | -|--------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------| -| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration | -| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path | -| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request | -| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes | -| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` | -| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses | -| `jsonrpc.validation.request.params-type-violation-code-policy` | `INVALID_PARAMS` or `INVALID_REQUEST` | `INVALID_PARAMS` | Error code used when `params` exists but is neither object nor array | -| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` | -| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member | -| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses | -| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses | -| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses | -| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses | -| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` | -| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present | -| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer | -| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string | -| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses | -| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy | -| `jsonrpc.method-allowlist` | `List` | `[]` | Allowlist for method access filtering | -| `jsonrpc.method-denylist` | `List` | `[]` | Denylist for method access filtering (higher priority) | -| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present | -| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets | -| `jsonrpc.metrics-latency-percentiles` | `List` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) | -| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` | -| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch | -| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications | +| Key | Type | Default | Description | +|-----------------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------| +| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration | +| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path | +| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request | +| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes | +| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` | +| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses | +| `jsonrpc.validation.request.params-type-violation-code-policy` | `INVALID_PARAMS` or `INVALID_REQUEST` | `INVALID_PARAMS` | Error code used when `params` exists but is neither object nor array | +| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` | +| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member | +| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses | +| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses | +| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses | +| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses | +| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` | +| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present | +| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer | +| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string | +| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses | +| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy | +| `jsonrpc.method-allowlist` | `List` | `[]` | Allowlist for method access filtering | +| `jsonrpc.method-denylist` | `List` | `[]` | Denylist for method access filtering (higher priority) | +| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present | +| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets | +| `jsonrpc.metrics-latency-percentiles` | `List` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) | +| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` | +| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch | +| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications | ## 2. Validation Rules (Fail Fast) @@ -87,7 +87,8 @@ If a configured bean name is missing, startup fails. - `REJECT`: first duplicate fails registration. - `REPLACE`: later registration wins. -In auto-configuration, annotation scanning runs after manual registrations, so annotation handlers can replace manual handlers under `REPLACE`. +In auto-configuration, annotation scanning runs after manual registrations, so annotation handlers can replace manual +handlers under `REPLACE`. ## 4. Property Source Precedence (Spring Boot) @@ -102,7 +103,8 @@ Example environment variable mapping: - `jsonrpc.max-request-bytes` -> `JSONRPC_MAX_REQUEST_BYTES` - `jsonrpc.method-registration-conflict-policy` -> `JSONRPC_METHOD_REGISTRATION_CONFLICT_POLICY` -- `jsonrpc.validation.request.params-type-violation-code-policy` -> `JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY` +- `jsonrpc.validation.request.params-type-violation-code-policy` -> + `JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY` ## 5. Example Configurations @@ -131,16 +133,16 @@ jsonrpc: require-integer-error-code: true require-string-error-message: true allow-request-fields-in-response: true - method-allowlist: [] - method-denylist: [] + method-allowlist: [ ] + method-denylist: [ ] ``` ## 5.2 Strict access profile ```yaml jsonrpc: - method-allowlist: [user.find, user.update] - method-denylist: [user.delete] + method-allowlist: [ user.find, user.update ] + method-denylist: [ user.delete ] ``` ## 5.3 Async notification profile @@ -157,7 +159,7 @@ jsonrpc: jsonrpc: metrics-enabled: true metrics-latency-histogram-enabled: true - metrics-latency-percentiles: [0.9, 0.95, 0.99] + metrics-latency-percentiles: [ 0.9, 0.95, 0.99 ] metrics-max-method-tag-values: 200 ``` diff --git a/docs/extension-points.md b/docs/extension-points.md index ee0fc78..764d2bf 100644 --- a/docs/extension-points.md +++ b/docs/extension-points.md @@ -69,19 +69,19 @@ When `MeterRegistry` is present and `jsonrpc.metrics-enabled=true`, a Micrometer Metrics: - Counter: `jsonrpc.server.calls` - - tags: `method`, `outcome`, `errorCode` + - tags: `method`, `outcome`, `errorCode` - Timer: `jsonrpc.server.latency` - - tags: `method`, `outcome` + - tags: `method`, `outcome` - Counter: `jsonrpc.server.stage.events` - - tags: `method`, `stage` + - tags: `method`, `stage` - Counter: `jsonrpc.server.failures` - - tags: `method`, `errorCode`, `source` + - tags: `method`, `errorCode`, `source` - Counter: `jsonrpc.server.transport.errors` - - tags: `reason` (`parse_error`, `request_too_large`) + - tags: `reason` (`parse_error`, `request_too_large`) - Counter: `jsonrpc.server.batch.requests` - - tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`) + - tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`) - Counter: `jsonrpc.server.batch.entries` - - tags: `outcome` (`success`, `error`, `notification`) + - tags: `outcome` (`success`, `error`, `notification`) - Summary: `jsonrpc.server.batch.size` - Timer: `jsonrpc.server.notification.queue.delay` - Timer: `jsonrpc.server.notification.execution` @@ -115,6 +115,7 @@ Default strategy returns `200` for protocol responses and `204` for notification - `DirectJsonRpcNotificationExecutor`: same thread - `ExecutorJsonRpcNotificationExecutor`: delegated to Java `Executor` -- `InstrumentedJsonRpcNotificationExecutor`: wraps notification execution for queue/latency/failure metrics when metrics are enabled +- `InstrumentedJsonRpcNotificationExecutor`: wraps notification execution for queue/latency/failure metrics when metrics + are enabled You can provide your own implementation for custom backpressure/isolation/retry behavior. diff --git a/docs/performance.md b/docs/performance.md index 7881855..d16ab80 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -27,7 +27,8 @@ JMH benchmark exists in `jsonrpc-core`: ./gradlew :jsonrpc-core:jmh ``` -It includes dispatcher scenarios for single success/error/invalid cases and large batch profiles (all-success, all-error, mixed, notification-only). +It includes dispatcher scenarios for single success/error/invalid cases and large batch profiles (all-success, +all-error, mixed, notification-only). Quick profile (short warmup/measurement): @@ -50,8 +51,8 @@ Run quick profile for a specific benchmark include pattern: - Use allowlist/denylist to reduce exposed method surface area. - Set `jsonrpc.metrics-max-method-tag-values` to bound method tag cardinality. - Enable histogram/percentiles only when needed: - - `jsonrpc.metrics-latency-histogram-enabled` - - `jsonrpc.metrics-latency-percentiles` + - `jsonrpc.metrics-latency-histogram-enabled` + - `jsonrpc.metrics-latency-percentiles` ## Performance Testing Guidance diff --git a/docs/protocol-and-compliance.md b/docs/protocol-and-compliance.md index fe052f4..2158c53 100644 --- a/docs/protocol-and-compliance.md +++ b/docs/protocol-and-compliance.md @@ -67,9 +67,9 @@ By default, `JsonRpcResponseValidationOptions.defaults()` enforces: - `id` member exists and is `string | number | null` - exactly one of `result` or `error` is present - when `error` is present: - - `error` is an object - - `error.code` is an integer - - `error.message` is a string + - `error` is an object + - `error.code` is an integer + - `error.message` is a string RFC SHOULD or stricter interoperability policies are configurable via per-rule options. This library does not expose predefined strict/lenient modes; policy is controlled per rule. @@ -108,7 +108,8 @@ This library does not expose predefined strict/lenient modes; policy is controll ## Reserved Method Namespace -Methods starting with `rpc.` are rejected at registration (`IllegalArgumentException`) to preserve reserved namespace semantics. +Methods starting with `rpc.` are rejected at registration (`IllegalArgumentException`) to preserve reserved namespace +semantics. ## HTTP Mapping Notes @@ -121,7 +122,9 @@ This is transport policy, not protocol rule, and can be overridden via `JsonRpcH ## Known Deliberate Policy Choices -- Oversized request body (`jsonrpc.max-request-bytes`) maps to protocol error `-32600` with message `Request payload too large`. +- Oversized request body (`jsonrpc.max-request-bytes`) maps to protocol error `-32600` with message + `Request payload too large`. - Parse errors always use `id: null`. - Generic exceptions are intentionally normalized to `-32603` to avoid leaking internals. -- `params` type violations (non-array/object) default to `-32602`; in Spring Boot this can be changed with `jsonrpc.validation.request.params-type-violation-code-policy=INVALID_REQUEST`. +- `params` type violations (non-array/object) default to `-32602`; in Spring Boot this can be changed with + `jsonrpc.validation.request.params-type-violation-code-policy=INVALID_REQUEST`. diff --git a/docs/pure-java-guide.md b/docs/pure-java-guide.md index cebc9d7..c3cc9d3 100644 --- a/docs/pure-java-guide.md +++ b/docs/pure-java-guide.md @@ -1,6 +1,7 @@ # Pure Java Guide -`jsonrpc-core` is transport-agnostic and can be used without Spring. This guide shows how to use it in plain Java applications, custom servers, workers, and tests. +`jsonrpc-core` is transport-agnostic and can be used without Spring. This guide shows how to use it in plain Java +applications, custom servers, workers, and tests. ## 1. Dependency diff --git a/docs/registration-and-binding.md b/docs/registration-and-binding.md index 836efbf..e653397 100644 --- a/docs/registration-and-binding.md +++ b/docs/registration-and-binding.md @@ -1,6 +1,7 @@ # Registration and Binding -This document explains every supported method registration style, how conflicts are resolved, and how parameters/results are bound. +This document explains every supported method registration style, how conflicts are resolved, and how parameters/results +are bound. ## 1. Registration Styles @@ -86,12 +87,12 @@ Use this style when you want strict DTO-based mapping while still registering ma In Spring Boot auto-configuration, registration happens in two phases: 1. During `JsonRpcDispatcher` bean creation: - - all `JsonRpcMethodRegistration` beans are registered - - registration uses `ObjectProvider.orderedStream()` - - `@Order` / `Ordered` affects order in this phase + - all `JsonRpcMethodRegistration` beans are registered + - registration uses `ObjectProvider.orderedStream()` + - `@Order` / `Ordered` affects order in this phase 2. After singletons are instantiated: - - `JsonRpcAnnotatedMethodRegistrar` scans beans for `@JsonRpcMethod` - - annotated methods are registered + - `JsonRpcAnnotatedMethodRegistrar` scans beans for `@JsonRpcMethod` + - annotated methods are registered Priority summary: diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 3d2b70e..c9991c0 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -11,33 +11,34 @@ Use this checklist before creating a release tag (`vX.Y.Z`). ## 2) Local Verification - Run full verification: - - `./gradlew clean check` - - `./gradlew test integrationTest e2eTest` + - `./gradlew clean check` + - `./gradlew test integrationTest e2eTest` - Run API compatibility check against latest released version: - - `./gradlew apiCompat -PapiBaselineVersion=` + - `./gradlew apiCompat -PapiBaselineVersion=` - Run benchmark smoke: - - `./gradlew :jsonrpc-core:jmhQuick` + - `./gradlew :jsonrpc-core:jmhQuick` - Run consumer smoke verification: - - `./scripts/verify-consumer-smoke.sh` + - `./scripts/verify-consumer-smoke.sh` ## 3) Publication Preconditions - Confirm Sonatype Central Portal user token credentials are available: - - `OSSRH_USERNAME`, `OSSRH_PASSWORD` - - These env var names are kept for workflow compatibility, but values must be Central Portal user token username/password. + - `OSSRH_USERNAME`, `OSSRH_PASSWORD` + - These env var names are kept for workflow compatibility, but values must be Central Portal user token + username/password. - Confirm signing credentials are available: - - `SIGNING_KEY`, `SIGNING_PASSWORD` + - `SIGNING_KEY`, `SIGNING_PASSWORD` - Validate generated artifacts: - - jars, sources jar, javadoc jar, signatures, pom metadata + - jars, sources jar, javadoc jar, signatures, pom metadata ## 4) Tag and Publish - Commit release changes. - Create annotated git tag: - - `git tag -a vX.Y.Z -m "Release vX.Y.Z"` + - `git tag -a vX.Y.Z -m "Release vX.Y.Z"` - Push branch and tag: - - `git push` - - `git push origin vX.Y.Z` + - `git push` + - `git push origin vX.Y.Z` - Verify GitHub Actions `Publish` workflow completed successfully. ## 5) Post-Release @@ -46,6 +47,6 @@ Use this checklist before creating a release tag (`vX.Y.Z`). - Publish GitHub release notes. - Smoke test the sample project with the published version. - Verify Central Portal deployment visibility: - - `Publish` workflow should call the `manual/upload/defaultRepository/` finalize API. - - `Publish` workflow should poll deployment status and complete only when state reaches `PUBLISHED`. - - Confirm `central.sonatype.com/publishing/deployments` shows the component and reaches `Published`. + - `Publish` workflow should call the `manual/upload/defaultRepository/` finalize API. + - `Publish` workflow should poll deployment status and complete only when state reaches `PUBLISHED`. + - Confirm `central.sonatype.com/publishing/deployments` shows the component and reaches `Published`. diff --git a/docs/spring-boot-guide.md b/docs/spring-boot-guide.md index c976728..bc885c7 100644 --- a/docs/spring-boot-guide.md +++ b/docs/spring-boot-guide.md @@ -1,6 +1,7 @@ # Spring Boot Guide -This guide covers production-style Spring Boot usage, including registration strategies, conflict handling, and runtime customization. +This guide covers production-style Spring Boot usage, including registration strategies, conflict handling, and runtime +customization. ## 1. Dependency @@ -152,7 +153,8 @@ Use this when you want compile-time DTO types and reuse the standard binder/writ Two registration phases exist in auto-configuration: 1. `JsonRpcMethodRegistration` beans are applied while creating `JsonRpcDispatcher`. -2. `@JsonRpcMethod` scanner (`JsonRpcAnnotatedMethodRegistrar`) runs after singleton initialization and registers annotated handlers. +2. `@JsonRpcMethod` scanner (`JsonRpcAnnotatedMethodRegistrar`) runs after singleton initialization and registers + annotated handlers. Within manual registrations, `orderedStream()` is used, so `@Order` / `Ordered` can control order. @@ -342,7 +344,8 @@ If the configured bean name is missing, startup fails with an explicit error. ## 9. Metrics -If Micrometer `MeterRegistry` exists and `jsonrpc.metrics-enabled=true` (default), metrics interceptor/observer are enabled. +If Micrometer `MeterRegistry` exists and `jsonrpc.metrics-enabled=true` (default), metrics interceptor/observer are +enabled. Key metrics: diff --git a/docs/testing-and-quality.md b/docs/testing-and-quality.md index e0ddd9e..7b6a42d 100644 --- a/docs/testing-and-quality.md +++ b/docs/testing-and-quality.md @@ -28,17 +28,17 @@ Commands: - Parser/validator rules (`jsonrpc`, `method`, `params`, `id`) - Response-side utilities: - - envelope classifier (`REQUEST` / `RESPONSE` / `INVALID`) - - incoming response parser (single/batch and field-presence semantics) - - configurable response validator (`JsonRpcResponseValidationOptions`) + - envelope classifier (`REQUEST` / `RESPONSE` / `INVALID`) + - incoming response parser (single/batch and field-presence semantics) + - configurable response validator (`JsonRpcResponseValidationOptions`) - Dispatcher branches: - - success - - invalid request - - method missing - - invalid params - - internal exceptions - - notification no-response - - batch (mixed/single/empty) + - success + - invalid request + - method missing + - invalid params + - internal exceptions + - notification no-response + - batch (mixed/single/empty) - Interceptor callbacks and error resilience - Typed binder/writer behavior for records/classes/collections - Pure Java integration and e2e usage @@ -75,12 +75,25 @@ Binary compatibility checks are provided via JApiCmp: ./gradlew apiCompat -PapiBaselineVersion= ``` +## Static Analysis + +Production Java source sets are checked with NullAway through Error Prone. + +- Enabled for `compileJava` in all library modules. +- Disabled for `compileTestJava`, `compileIntegrationTestJava`, and `compileE2eTestJava`. +- `check` fails on any NullAway violation. + +Goal: + +- catch nullable contract bugs at compile time +- keep runtime behavior stable while strengthening type-level safety + ## CI GitHub Actions runs matrix tests and compatibility checks (when release tag baseline exists): - `.github/workflows/ci.yml` - - Java matrix: 17 / 21 / 25 + - Java matrix: 17 / 21 / 25 - `.github/workflows/publish.yml` - `.github/workflows/consumer-smoke.yml` (publishes to `mavenLocal` and verifies Maven/Gradle consumer projects) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 654333a..985742b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -88,3 +88,20 @@ Action: - Increase limit if use case requires larger requests. - Consider splitting large payload into smaller calls. + +## Build Fails with NullAway Error + +Symptom: + +- `./gradlew check` fails with `[NullAway]` diagnostics. + +Cause: + +- Production source code violated null-safety contracts enforced at compile time. + +Checks: + +- Add `@Nullable` to parameters/returns/fields that can legitimately be null. +- Add explicit null guard and fallback values before passing data into non-null APIs. +- Align overridden method signatures with nullable contracts from super interfaces/classes. +- For JSON-RPC request/response models, keep nullability aligned with protocol semantics. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dbade3f..09cd4bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,9 @@ japicmp-plugin = "0.4.6" jmh-plugin = "0.7.3" spring-dependency-management-plugin = "1.1.7" jsonrpc-starter = "0.1.2" +errorprone-plugin = "5.1.0" +errorprone = "2.42.0" +nullaway = "0.13.1" [libraries] spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } @@ -24,6 +27,8 @@ spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-start spring-boot-configuration-processor = { module = "org.springframework.boot:spring-boot-configuration-processor" } micrometer-core = { module = "io.micrometer:micrometer-core" } jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } +nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } jsonrpc-spring-boot-starter = { module = "io.github.limehee:jsonrpc-spring-boot-starter", version.ref = "jsonrpc-starter" } [plugins] @@ -31,3 +36,4 @@ japicmp = { id = "me.champeau.gradle.japicmp", version.ref = "japicmp-plugin" } jmh = { id = "me.champeau.jmh", version.ref = "jmh-plugin" } spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management-plugin" } +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } diff --git a/jsonrpc-core/src/e2eTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaE2ETest.java b/jsonrpc-core/src/e2eTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaE2ETest.java index 9318654..cebec3b 100644 --- a/jsonrpc-core/src/e2eTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaE2ETest.java +++ b/jsonrpc-core/src/e2eTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaE2ETest.java @@ -1,17 +1,16 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import tools.jackson.core.JacksonException; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; class JsonRpcPureJavaE2ETest { @@ -21,13 +20,13 @@ class JsonRpcPureJavaE2ETest { void setUp() { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcTypedMethodHandlerFactory typedFactory = new DefaultJsonRpcTypedMethodHandlerFactory( - new JacksonJsonRpcParameterBinder(PureJavaJsonRpcServer.OBJECT_MAPPER), - new JacksonJsonRpcResultWriter(PureJavaJsonRpcServer.OBJECT_MAPPER) + new JacksonJsonRpcParameterBinder(PureJavaJsonRpcServer.OBJECT_MAPPER), + new JacksonJsonRpcResultWriter(PureJavaJsonRpcServer.OBJECT_MAPPER) ); dispatcher.register("manual.ping", params -> StringNode.valueOf("pong")); dispatcher.register("typed.upper", typedFactory.unary(UpperInput.class, - input -> new UpperOutput(input.value == null ? "" : input.value.toUpperCase()))); + input -> new UpperOutput(input.value == null ? "" : input.value.toUpperCase()))); dispatcher.register("typed.tags", typedFactory.noParams(() -> List.of("alpha", "beta"))); server = new PureJavaJsonRpcServer(dispatcher); @@ -36,11 +35,11 @@ void setUp() { @Test void e2eReturnsSuccessJsonForManualAndTypedMethods() throws Exception { JsonNode ping = parse(server.handle(""" - {"jsonrpc":"2.0","method":"manual.ping","id":1} - """)); + {"jsonrpc":"2.0","method":"manual.ping","id":1} + """)); JsonNode upper = parse(server.handle(""" - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"core"},"id":2} - """)); + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"core"},"id":2} + """)); assertEquals("pong", ping.get("result").asString()); assertEquals("CORE", upper.get("result").get("value").asString()); @@ -49,19 +48,19 @@ void e2eReturnsSuccessJsonForManualAndTypedMethods() throws Exception { @Test void e2eReturnsNoBodyForNotification() throws Exception { String body = server.handle(""" - {"jsonrpc":"2.0","method":"manual.ping"} - """); + {"jsonrpc":"2.0","method":"manual.ping"} + """); assertTrue(body.isEmpty()); } @Test void e2eHandlesBatchAndParseError() throws Exception { JsonNode batch = parse(server.handle(""" - [ - {"jsonrpc":"2.0","method":"typed.tags","id":3}, - {"jsonrpc":"2.0","method":"missing","id":4} - ] - """)); + [ + {"jsonrpc":"2.0","method":"typed.tags","id":3}, + {"jsonrpc":"2.0","method":"missing","id":4} + ] + """)); JsonNode parseError = parse(server.handle("{")); assertTrue(batch.isArray()); @@ -76,10 +75,12 @@ private JsonNode parse(String json) throws JacksonException { } static class UpperInput { + public String value; } static class UpperOutput { + public String value; UpperOutput(String value) { @@ -88,6 +89,7 @@ static class UpperOutput { } static class PureJavaJsonRpcServer { + private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); private final JsonRpcDispatcher dispatcher; diff --git a/jsonrpc-core/src/integrationTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaIntegrationTest.java b/jsonrpc-core/src/integrationTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaIntegrationTest.java index 643bd9c..5692d45 100644 --- a/jsonrpc-core/src/integrationTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaIntegrationTest.java +++ b/jsonrpc-core/src/integrationTest/java/com/limehee/jsonrpc/core/JsonRpcPureJavaIntegrationTest.java @@ -1,16 +1,15 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; class JsonRpcPureJavaIntegrationTest { @@ -23,24 +22,24 @@ void setUp() { dispatcher = new JsonRpcDispatcher(); JsonRpcTypedMethodHandlerFactory typedFactory = new DefaultJsonRpcTypedMethodHandlerFactory( - new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), - new JacksonJsonRpcResultWriter(OBJECT_MAPPER) + new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), + new JacksonJsonRpcResultWriter(OBJECT_MAPPER) ); dispatcher.register("manual.ping", params -> StringNode.valueOf("pong")); dispatcher.register("typed.user", typedFactory.unary(UserRequest.class, - request -> new UserResponse(request.id, "user-" + request.id))); + request -> new UserResponse(request.id, "user-" + request.id))); dispatcher.register("typed.tags", typedFactory.noParams(() -> List.of("alpha", "beta"))); } @Test void supportsManualAndTypedRegistrationsWithoutSpring() throws Exception { JsonNode ping = call(""" - {"jsonrpc":"2.0","method":"manual.ping","id":1} - """); + {"jsonrpc":"2.0","method":"manual.ping","id":1} + """); JsonNode user = call(""" - {"jsonrpc":"2.0","method":"typed.user","params":{"id":7},"id":2} - """); + {"jsonrpc":"2.0","method":"typed.user","params":{"id":7},"id":2} + """); assertEquals("pong", ping.get("result").asString()); assertEquals(7, user.get("result").get("id").asInt()); @@ -50,8 +49,8 @@ void supportsManualAndTypedRegistrationsWithoutSpring() throws Exception { @Test void supportsClassParamRecordReturnAndCollectionReturn() throws Exception { JsonNode tags = call(""" - {"jsonrpc":"2.0","method":"typed.tags","id":3} - """); + {"jsonrpc":"2.0","method":"typed.tags","id":3} + """); assertTrue(tags.get("result").isArray()); assertEquals(2, tags.get("result").size()); @@ -62,12 +61,12 @@ void supportsClassParamRecordReturnAndCollectionReturn() throws Exception { @Test void supportsBatchInPureJavaEnvironment() throws Exception { JsonNode batchResult = call(""" - [ - {"jsonrpc":"2.0","method":"manual.ping","id":1}, - {"jsonrpc":"2.0","method":"manual.ping"}, - {"jsonrpc":"2.0","method":"missing","id":2} - ] - """); + [ + {"jsonrpc":"2.0","method":"manual.ping","id":1}, + {"jsonrpc":"2.0","method":"manual.ping"}, + {"jsonrpc":"2.0","method":"missing","id":2} + ] + """); assertTrue(batchResult.isArray()); assertEquals(2, batchResult.size()); @@ -88,9 +87,11 @@ private JsonNode call(String json) throws Exception { } static class UserRequest { + public int id; } record UserResponse(int id, String name) { + } } diff --git a/jsonrpc-core/src/jmh/java/com/limehee/jsonrpc/core/JsonRpcDispatcherBenchmark.java b/jsonrpc-core/src/jmh/java/com/limehee/jsonrpc/core/JsonRpcDispatcherBenchmark.java index 2899884..7ab20ec 100644 --- a/jsonrpc-core/src/jmh/java/com/limehee/jsonrpc/core/JsonRpcDispatcherBenchmark.java +++ b/jsonrpc-core/src/jmh/java/com/limehee/jsonrpc/core/JsonRpcDispatcherBenchmark.java @@ -1,16 +1,16 @@ package com.limehee.jsonrpc.core; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; import tools.jackson.databind.node.StringNode; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; @State(Scope.Benchmark) public class JsonRpcDispatcherBenchmark { @@ -42,27 +42,27 @@ public void setUp() throws Exception { }); singlePayload = OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); methodNotFoundPayload = OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"missing","id":9} - """); + {"jsonrpc":"2.0","method":"missing","id":9} + """); invalidParamsPayload = OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"strict.object","params":"wrong","id":10} - """); + {"jsonrpc":"2.0","method":"strict.object","params":"wrong","id":10} + """); invalidRequestPayload = OBJECT_MAPPER.readTree(""" - {"jsonrpc":"1.0","method":"ping","id":11} - """); + {"jsonrpc":"1.0","method":"ping","id":11} + """); notificationPayload = OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping"} - """); + {"jsonrpc":"2.0","method":"ping"} + """); batchPayload = OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"ping","id":2} - ] - """); + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"ping","id":2} + ] + """); batchAllSuccessLargePayload = buildLargeBatchPayload("ping", LARGE_BATCH_SIZE, false); batchAllErrorsLargePayload = buildLargeBatchPayload("missing", LARGE_BATCH_SIZE, false); batchMixedLargePayload = buildMixedLargeBatchPayload(LARGE_BATCH_SIZE); diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifier.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifier.java index f2d8d21..52e0c11 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifier.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifier.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default envelope classifier using top-level field presence heuristics. @@ -43,14 +43,14 @@ public JsonRpcEnvelopeType classify(@Nullable JsonNode payload) { /** * Classifies a single object node by request/response marker fields. *

- * When request and response hints coexist, response classification takes precedence to keep - * routing aligned with response-side validation policies. + * When request and response hints coexist, response classification takes precedence to keep routing aligned with + * response-side validation policies. * * @param node object node candidate * @return envelope classification */ private JsonRpcEnvelopeType classifyObject(JsonNode node) { - if (node == null || !node.isObject()) { + if (!node.isObject()) { return JsonRpcEnvelopeType.INVALID; } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolver.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolver.java index c5dfdf7..5d25719 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolver.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolver.java @@ -1,5 +1,7 @@ package com.limehee.jsonrpc.core; +import java.util.Objects; + /** * Default exception resolver that maps {@link JsonRpcException} and falls back to internal error. */ @@ -32,10 +34,14 @@ public DefaultJsonRpcExceptionResolver(boolean includeErrorData) { @Override public JsonRpcError resolve(Throwable throwable) { if (throwable instanceof JsonRpcException jsonRpcException) { + String message = Objects.requireNonNullElse( + jsonRpcException.getMessage(), + JsonRpcConstants.MESSAGE_INTERNAL_ERROR + ); return new JsonRpcError( - jsonRpcException.getCode(), - jsonRpcException.getMessage(), - includeErrorData ? jsonRpcException.getData() : null + jsonRpcException.getCode(), + message, + includeErrorData ? jsonRpcException.getData() : null ); } return JsonRpcError.of(JsonRpcErrorCode.INTERNAL_ERROR, JsonRpcConstants.MESSAGE_INTERNAL_ERROR); diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcMethodInvoker.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcMethodInvoker.java index 9154fcf..8ce4061 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcMethodInvoker.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcMethodInvoker.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default invoker delegating directly to the handler. @@ -12,7 +12,7 @@ public class DefaultJsonRpcMethodInvoker implements JsonRpcMethodInvoker { * Invokes a handler with provided params. * * @param handler method handler - * @param params optional request params + * @param params optional request params * @return handler result */ @Override diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidator.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidator.java index 23b46b3..0ef1823 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidator.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidator.java @@ -1,8 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; - import java.util.Objects; +import tools.jackson.databind.JsonNode; /** * Default JSON-RPC request validator enforcing core protocol constraints. @@ -12,8 +11,8 @@ public class DefaultJsonRpcRequestValidator implements JsonRpcRequestValidator { private final JsonRpcParamsTypeViolationCodePolicy paramsTypeViolationCodePolicy; /** - * Creates a validator using {@link JsonRpcParamsTypeViolationCodePolicy#INVALID_PARAMS} for - * invalid {@code params} type violations. + * Creates a validator using {@link JsonRpcParamsTypeViolationCodePolicy#INVALID_PARAMS} for invalid {@code params} + * type violations. */ public DefaultJsonRpcRequestValidator() { this(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); @@ -22,13 +21,12 @@ public DefaultJsonRpcRequestValidator() { /** * Creates a validator with an explicit error-code policy for invalid {@code params} type. * - * @param paramsTypeViolationCodePolicy policy selecting the error code for invalid - * {@code params} type violations + * @param paramsTypeViolationCodePolicy policy selecting the error code for invalid {@code params} type violations */ public DefaultJsonRpcRequestValidator(JsonRpcParamsTypeViolationCodePolicy paramsTypeViolationCodePolicy) { this.paramsTypeViolationCodePolicy = Objects.requireNonNull( - paramsTypeViolationCodePolicy, - "paramsTypeViolationCodePolicy" + paramsTypeViolationCodePolicy, + "paramsTypeViolationCodePolicy" ); } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseComposer.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseComposer.java index f138dd6..4ce03f8 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseComposer.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseComposer.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default response composer delegating to {@link JsonRpcResponse} factories. @@ -11,7 +11,7 @@ public class DefaultJsonRpcResponseComposer implements JsonRpcResponseComposer { /** * Creates a success response. * - * @param id request id + * @param id request id * @param result success payload * @return success response */ @@ -23,7 +23,7 @@ public JsonRpcResponse success(@Nullable JsonNode id, JsonNode result) { /** * Creates an error response. * - * @param id request id + * @param id request id * @param error error payload * @return error response */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParser.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParser.java index 11c734e..78e5e6c 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParser.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParser.java @@ -1,10 +1,9 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default parser for incoming JSON-RPC response payloads. @@ -40,7 +39,7 @@ public JsonRpcIncomingResponseEnvelope parse(@Nullable JsonNode payload) { * @return parsed incoming response */ private JsonRpcIncomingResponse parseObject(JsonNode node) { - if (node == null || !node.isObject()) { + if (!node.isObject()) { throw invalidResponseEnvelope(); } @@ -57,14 +56,14 @@ private JsonRpcIncomingResponse parseObject(JsonNode node) { JsonNode error = node.get("error"); return new JsonRpcIncomingResponse( - node, - jsonrpc, - id, - idPresent, - result, - resultPresent, - error, - errorPresent + node, + jsonrpc, + id, + idPresent, + result, + resultPresent, + error, + errorPresent ); } 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 f950419..d55b1ce 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 @@ -1,8 +1,8 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; - import java.util.Objects; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default validator for parsed incoming JSON-RPC responses. @@ -73,7 +73,7 @@ public void validate(JsonRpcIncomingResponse response) { * * @param id response id node */ - private void validateId(JsonNode id) { + private void validateId(@Nullable JsonNode id) { if (id == null || id.isNull()) { if (!options.allowNullResponseId()) { throw invalid("response id must not be null"); @@ -106,7 +106,7 @@ private void validateId(JsonNode id) { * * @param error error node */ - private void validateError(JsonNode error) { + private void validateError(@Nullable JsonNode error) { if (options.requireErrorObjectWhenPresent() && (error == null || !error.isObject())) { throw invalid("response error must be an object"); } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcTypedMethodHandlerFactory.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcTypedMethodHandlerFactory.java index f8f681a..2b6af23 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcTypedMethodHandlerFactory.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/DefaultJsonRpcTypedMethodHandlerFactory.java @@ -1,10 +1,10 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; - import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Default typed handler factory using binder/writer components for conversion. @@ -18,9 +18,10 @@ public class DefaultJsonRpcTypedMethodHandlerFactory implements JsonRpcTypedMeth * Creates a typed method handler factory. * * @param parameterBinder binder for converting params to Java values - * @param resultWriter serializer for converting Java return values to JSON + * @param resultWriter serializer for converting Java return values to JSON */ - public DefaultJsonRpcTypedMethodHandlerFactory(JsonRpcParameterBinder parameterBinder, JsonRpcResultWriter resultWriter) { + public DefaultJsonRpcTypedMethodHandlerFactory(JsonRpcParameterBinder parameterBinder, + JsonRpcResultWriter resultWriter) { this.parameterBinder = Objects.requireNonNull(parameterBinder, "parameterBinder"); this.resultWriter = Objects.requireNonNull(resultWriter, "resultWriter"); } @@ -55,7 +56,7 @@ public

JsonRpcMethodHandler unary(Class

paramType, Function method) * @param params params payload to validate * @throws JsonRpcException when unexpected params are provided */ - private void validateNoParams(JsonNode params) { + private void validateNoParams(@Nullable JsonNode params) { if (params == null || params.isNull()) { return; } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/ExecutorJsonRpcNotificationExecutor.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/ExecutorJsonRpcNotificationExecutor.java index 47d7e6f..0fa471b 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/ExecutorJsonRpcNotificationExecutor.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/ExecutorJsonRpcNotificationExecutor.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import java.util.concurrent.Executor; import java.util.Objects; +import java.util.concurrent.Executor; /** * Notification executor that delegates execution to a supplied {@link Executor}. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinder.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinder.java index 173a3f6..10dab75 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinder.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinder.java @@ -1,11 +1,11 @@ package com.limehee.jsonrpc.core; +import java.util.Objects; +import org.jspecify.annotations.Nullable; import tools.jackson.core.JacksonException; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import org.jspecify.annotations.Nullable; - -import java.util.Objects; +import tools.jackson.databind.node.NullNode; /** * {@link JsonRpcParameterBinder} implementation based on Jackson object mapping. @@ -32,7 +32,7 @@ public T bind(@Nullable JsonNode params, Class targetType) { throw new IllegalArgumentException("targetType must not be null"); } if (targetType == JsonNode.class) { - return targetType.cast(params); + return targetType.cast(params == null ? NullNode.getInstance() : params); } try { @@ -42,10 +42,10 @@ public T bind(@Nullable JsonNode params, Class targetType) { return objectMapper.convertValue(params, targetType); } catch (JacksonException | IllegalArgumentException ex) { throw new JsonRpcException( - JsonRpcErrorCode.INVALID_PARAMS, - JsonRpcConstants.MESSAGE_INVALID_PARAMS, - null, - ex + JsonRpcErrorCode.INVALID_PARAMS, + JsonRpcConstants.MESSAGE_INVALID_PARAMS, + null, + ex ); } } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcResultWriter.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcResultWriter.java index f4d008c..25314dd 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcResultWriter.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JacksonJsonRpcResultWriter.java @@ -1,8 +1,8 @@ package com.limehee.jsonrpc.core; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import org.jspecify.annotations.Nullable; /** * {@link JsonRpcResultWriter} backed by Jackson tree conversion. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatchResult.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatchResult.java index e2f086a..5571491 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatchResult.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatchResult.java @@ -14,7 +14,7 @@ public final class JsonRpcDispatchResult { /** * Creates a dispatcher result with immutable response storage. * - * @param batch whether the source payload was processed as a batch request + * @param batch whether the source payload was processed as a batch request * @param responses response entries generated by dispatching */ private JsonRpcDispatchResult(boolean batch, List responses) { diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatcher.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatcher.java index f5c068d..998eca0 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatcher.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcDispatcher.java @@ -1,12 +1,11 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Central JSON-RPC dispatcher orchestrating parsing, validation, invocation, interception, and response composition. @@ -44,109 +43,109 @@ public class JsonRpcDispatcher { */ public JsonRpcDispatcher() { this( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(), - new DirectJsonRpcNotificationExecutor() + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(), + new DirectJsonRpcNotificationExecutor() ); } /** * Creates a dispatcher with custom core pipeline components and default interceptor/notification behavior. * - * @param methodRegistry method registry - * @param requestParser request parser - * @param requestValidator request validator - * @param methodInvoker method invoker + * @param methodRegistry method registry + * @param requestParser request parser + * @param requestValidator request validator + * @param methodInvoker method invoker * @param exceptionResolver exception resolver - * @param responseComposer response composer - * @param maxBatchSize maximum number of elements allowed in batch payloads + * @param responseComposer response composer + * @param maxBatchSize maximum number of elements allowed in batch payloads */ public JsonRpcDispatcher( - JsonRpcMethodRegistry methodRegistry, - JsonRpcRequestParser requestParser, - JsonRpcRequestValidator requestValidator, - JsonRpcMethodInvoker methodInvoker, - JsonRpcExceptionResolver exceptionResolver, - JsonRpcResponseComposer responseComposer, - int maxBatchSize + JsonRpcMethodRegistry methodRegistry, + JsonRpcRequestParser requestParser, + JsonRpcRequestValidator requestValidator, + JsonRpcMethodInvoker methodInvoker, + JsonRpcExceptionResolver exceptionResolver, + JsonRpcResponseComposer responseComposer, + int maxBatchSize ) { this( - methodRegistry, - requestParser, - requestValidator, - methodInvoker, - exceptionResolver, - responseComposer, - maxBatchSize, - List.of(), - new DirectJsonRpcNotificationExecutor() + methodRegistry, + requestParser, + requestValidator, + methodInvoker, + exceptionResolver, + responseComposer, + maxBatchSize, + List.of(), + new DirectJsonRpcNotificationExecutor() ); } /** * Creates a dispatcher with custom components and interceptors. * - * @param methodRegistry method registry - * @param requestParser request parser - * @param requestValidator request validator - * @param methodInvoker method invoker + * @param methodRegistry method registry + * @param requestParser request parser + * @param requestValidator request validator + * @param methodInvoker method invoker * @param exceptionResolver exception resolver - * @param responseComposer response composer - * @param maxBatchSize maximum number of elements allowed in batch payloads - * @param interceptors interceptor chain executed around request handling + * @param responseComposer response composer + * @param maxBatchSize maximum number of elements allowed in batch payloads + * @param interceptors interceptor chain executed around request handling */ public JsonRpcDispatcher( - JsonRpcMethodRegistry methodRegistry, - JsonRpcRequestParser requestParser, - JsonRpcRequestValidator requestValidator, - JsonRpcMethodInvoker methodInvoker, - JsonRpcExceptionResolver exceptionResolver, - JsonRpcResponseComposer responseComposer, - int maxBatchSize, - List interceptors + JsonRpcMethodRegistry methodRegistry, + JsonRpcRequestParser requestParser, + JsonRpcRequestValidator requestValidator, + JsonRpcMethodInvoker methodInvoker, + JsonRpcExceptionResolver exceptionResolver, + JsonRpcResponseComposer responseComposer, + int maxBatchSize, + List interceptors ) { this( - methodRegistry, - requestParser, - requestValidator, - methodInvoker, - exceptionResolver, - responseComposer, - maxBatchSize, - interceptors, - new DirectJsonRpcNotificationExecutor() + methodRegistry, + requestParser, + requestValidator, + methodInvoker, + exceptionResolver, + responseComposer, + maxBatchSize, + interceptors, + new DirectJsonRpcNotificationExecutor() ); } /** * Creates a fully customized dispatcher. * - * @param methodRegistry method registry - * @param requestParser request parser - * @param requestValidator request validator - * @param methodInvoker method invoker - * @param exceptionResolver exception resolver - * @param responseComposer response composer - * @param maxBatchSize maximum number of elements allowed in batch payloads - * @param interceptors interceptor chain executed around request handling + * @param methodRegistry method registry + * @param requestParser request parser + * @param requestValidator request validator + * @param methodInvoker method invoker + * @param exceptionResolver exception resolver + * @param responseComposer response composer + * @param maxBatchSize maximum number of elements allowed in batch payloads + * @param interceptors interceptor chain executed around request handling * @param notificationExecutor executor used for notification invocations */ public JsonRpcDispatcher( - JsonRpcMethodRegistry methodRegistry, - JsonRpcRequestParser requestParser, - JsonRpcRequestValidator requestValidator, - JsonRpcMethodInvoker methodInvoker, - JsonRpcExceptionResolver exceptionResolver, - JsonRpcResponseComposer responseComposer, - int maxBatchSize, - List interceptors, - JsonRpcNotificationExecutor notificationExecutor + JsonRpcMethodRegistry methodRegistry, + JsonRpcRequestParser requestParser, + JsonRpcRequestValidator requestValidator, + JsonRpcMethodInvoker methodInvoker, + JsonRpcExceptionResolver exceptionResolver, + JsonRpcResponseComposer responseComposer, + int maxBatchSize, + List interceptors, + JsonRpcNotificationExecutor notificationExecutor ) { this.methodRegistry = Objects.requireNonNull(methodRegistry, "methodRegistry"); this.requestParser = Objects.requireNonNull(requestParser, "requestParser"); @@ -163,7 +162,7 @@ public JsonRpcDispatcher( /** * Registers a method handler. * - * @param method JSON-RPC method name + * @param method JSON-RPC method name * @param handler method handler */ public void register(String method, JsonRpcMethodHandler handler) { @@ -179,20 +178,20 @@ public void register(String method, JsonRpcMethodHandler handler) { public JsonRpcDispatchResult dispatch(@Nullable JsonNode payload) { if (payload == null) { return JsonRpcDispatchResult.single(errorResponse(null, new JsonRpcException( - JsonRpcErrorCode.INVALID_REQUEST, - JsonRpcConstants.MESSAGE_INVALID_REQUEST))); + JsonRpcErrorCode.INVALID_REQUEST, + JsonRpcConstants.MESSAGE_INVALID_REQUEST))); } if (payload.isArray()) { if (payload.isEmpty()) { return JsonRpcDispatchResult.single(errorResponse(null, new JsonRpcException( - JsonRpcErrorCode.INVALID_REQUEST, - JsonRpcConstants.MESSAGE_INVALID_REQUEST))); + JsonRpcErrorCode.INVALID_REQUEST, + JsonRpcConstants.MESSAGE_INVALID_REQUEST))); } if (payload.size() > maxBatchSize) { return JsonRpcDispatchResult.single(errorResponse(null, new JsonRpcException( - JsonRpcErrorCode.INVALID_REQUEST, - "Batch size exceeds configured maximum"))); + JsonRpcErrorCode.INVALID_REQUEST, + "Batch size exceeds configured maximum"))); } List responses = new ArrayList<>(payload.size()); @@ -214,6 +213,9 @@ public JsonRpcDispatchResult dispatch(@Nullable JsonNode payload) { public @Nullable JsonRpcResponse dispatch(@Nullable JsonRpcRequest request) { boolean validRequest = false; try { + if (request == null) { + throw new JsonRpcException(JsonRpcErrorCode.INVALID_REQUEST, JsonRpcConstants.MESSAGE_INVALID_REQUEST); + } requestValidator.validate(request); validRequest = true; return dispatchSingleRequest(request).orElse(null); @@ -232,8 +234,8 @@ public JsonRpcDispatchResult dispatch(@Nullable JsonNode payload) { */ public JsonRpcResponse parseErrorResponse() { return responseComposer.error(null, JsonRpcError.of( - JsonRpcErrorCode.PARSE_ERROR, - JsonRpcConstants.MESSAGE_PARSE_ERROR)); + JsonRpcErrorCode.PARSE_ERROR, + JsonRpcConstants.MESSAGE_PARSE_ERROR)); } /** @@ -242,11 +244,11 @@ public JsonRpcResponse parseErrorResponse() { * @param node request object node * @return optional response; empty for notifications */ - private Optional dispatchSingleNode(@Nullable JsonNode node) { - if (node == null || !node.isObject()) { + private Optional dispatchSingleNode(JsonNode node) { + if (!node.isObject()) { return Optional.of(errorResponse(null, new JsonRpcException( - JsonRpcErrorCode.INVALID_REQUEST, - JsonRpcConstants.MESSAGE_INVALID_REQUEST))); + JsonRpcErrorCode.INVALID_REQUEST, + JsonRpcConstants.MESSAGE_INVALID_REQUEST))); } JsonNode errorId = extractIdForError(node); @@ -274,10 +276,17 @@ private Optional dispatchSingleNode(@Nullable JsonNode node) { * @throws Exception when invocation fails before error mapping */ private Optional dispatchSingleRequest(JsonRpcRequest request) throws Exception { - JsonRpcMethodHandler handler = methodRegistry.find(request.method()) - .orElseThrow(() -> new JsonRpcException( - JsonRpcErrorCode.METHOD_NOT_FOUND, - JsonRpcConstants.MESSAGE_METHOD_NOT_FOUND)); + String methodName = request.method(); + if (methodName == null || methodName.isBlank()) { + throw new JsonRpcException( + JsonRpcErrorCode.INVALID_REQUEST, + JsonRpcConstants.MESSAGE_INVALID_REQUEST + ); + } + JsonRpcMethodHandler handler = methodRegistry.find(methodName) + .orElseThrow(() -> new JsonRpcException( + JsonRpcErrorCode.METHOD_NOT_FOUND, + JsonRpcConstants.MESSAGE_METHOD_NOT_FOUND)); if (request.isNotification()) { notificationExecutor.execute(() -> invokeNotificationHandler(request, handler)); @@ -306,17 +315,17 @@ private JsonRpcResponse errorResponse(@Nullable JsonNode id, Throwable ex) { /** * Handles errors for requests parsed from payload nodes. * - * @param id normalized error id - * @param request request if parsing/validation reached request construction + * @param id normalized error id + * @param request request if parsing/validation reached request construction * @param validRequest whether request validation succeeded before error - * @param ex thrown exception + * @param ex thrown exception * @return response unless the request is a valid notification */ private Optional handleRequestError( - @Nullable JsonNode id, - @Nullable JsonRpcRequest request, - boolean validRequest, - Throwable ex + @Nullable JsonNode id, + @Nullable JsonRpcRequest request, + boolean validRequest, + Throwable ex ) { JsonRpcError error = exceptionResolver.resolve(ex); runOnError(request, ex, error); @@ -397,7 +406,7 @@ private void runBeforeInvoke(JsonRpcRequest request) { * Runs {@code afterInvoke} interceptors. * * @param request validated request - * @param result invocation result + * @param result invocation result */ private void runAfterInvoke(JsonRpcRequest request, JsonNode result) { if (!hasInterceptors) { @@ -419,9 +428,9 @@ private void runAfterInvoke(JsonRpcRequest request, JsonNode result) { *

* Interceptor failures are intentionally ignored to avoid masking original request-processing errors. * - * @param request request model when available + * @param request request model when available * @param throwable original throwable - * @param error mapped JSON-RPC error + * @param error mapped JSON-RPC error */ private void runOnError(@Nullable JsonRpcRequest request, Throwable throwable, JsonRpcError error) { if (!hasInterceptors) { diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcEnvelopeClassifier.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcEnvelopeClassifier.java index a51a1a0..9d27295 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcEnvelopeClassifier.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcEnvelopeClassifier.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Classifies raw JSON payloads into request/response/invalid envelope types. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcError.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcError.java index 3989e87..70a289f 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcError.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcError.java @@ -1,15 +1,15 @@ package com.limehee.jsonrpc.core; import com.fasterxml.jackson.annotation.JsonInclude; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * JSON-RPC error object model. * - * @param code JSON-RPC error code + * @param code JSON-RPC error code * @param message human-readable error message - * @param data optional error data payload + * @param data optional error data payload */ @JsonInclude(JsonInclude.Include.NON_NULL) public record JsonRpcError(int code, String message, @Nullable JsonNode data) { @@ -17,7 +17,7 @@ public record JsonRpcError(int code, String message, @Nullable JsonNode data) { /** * Creates an error object without optional data payload. * - * @param code JSON-RPC error code + * @param code JSON-RPC error code * @param message human-readable error message * @return error object */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcException.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcException.java index 2b6f78a..b870bde 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcException.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcException.java @@ -1,13 +1,13 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Runtime exception representing JSON-RPC domain errors. *

- * Unlike generic runtime exceptions, this exception carries a JSON-RPC error code and optional - * structured error data that can be propagated to clients. + * Unlike generic runtime exceptions, this exception carries a JSON-RPC error code and optional structured error data + * that can be propagated to clients. */ public class JsonRpcException extends RuntimeException { @@ -24,7 +24,7 @@ public class JsonRpcException extends RuntimeException { /** * Creates an exception with code/message and no data/cause. * - * @param code JSON-RPC error code + * @param code JSON-RPC error code * @param message human-readable message */ public JsonRpcException(int code, String message) { @@ -34,9 +34,9 @@ public JsonRpcException(int code, String message) { /** * Creates an exception with code/message/data and no cause. * - * @param code JSON-RPC error code + * @param code JSON-RPC error code * @param message human-readable message - * @param data optional error data payload + * @param data optional error data payload */ public JsonRpcException(int code, String message, @Nullable JsonNode data) { this(code, message, data, null); @@ -45,10 +45,10 @@ public JsonRpcException(int code, String message, @Nullable JsonNode data) { /** * Creates an exception with code/message/data/cause. * - * @param code JSON-RPC error code + * @param code JSON-RPC error code * @param message human-readable message - * @param data optional error data payload - * @param cause original cause + * @param data optional error data payload + * @param cause original cause */ public JsonRpcException(int code, String message, @Nullable JsonNode data, @Nullable Throwable cause) { super(message, cause); diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponse.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponse.java index 30ccafe..b2dfe03 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponse.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponse.java @@ -1,28 +1,29 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Parsed incoming JSON-RPC response model preserving field presence semantics. * - * @param source original response object node - * @param jsonrpc protocol version field value when textual; otherwise {@code null} - * @param id id field value when present; may be {@code null} - * @param idPresent whether the response explicitly contained an {@code id} member - * @param result result field value when present; may be {@code null} + * @param source original response object node + * @param jsonrpc protocol version field value when textual; otherwise {@code null} + * @param id id field value when present; may be {@code null} + * @param idPresent whether the response explicitly contained an {@code id} member + * @param result result field value when present; may be {@code null} * @param resultPresent whether the response explicitly contained a {@code result} member - * @param error error field value when present; may be {@code null} - * @param errorPresent whether the response explicitly contained an {@code error} member + * @param error error field value when present; may be {@code null} + * @param errorPresent whether the response explicitly contained an {@code error} member */ public record JsonRpcIncomingResponse( - JsonNode source, - @Nullable String jsonrpc, - @Nullable JsonNode id, - boolean idPresent, - @Nullable JsonNode result, - boolean resultPresent, - @Nullable JsonNode error, - boolean errorPresent + JsonNode source, + @Nullable String jsonrpc, + @Nullable JsonNode id, + boolean idPresent, + @Nullable JsonNode result, + boolean resultPresent, + @Nullable JsonNode error, + boolean errorPresent ) { + } diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcInterceptor.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcInterceptor.java index 308b96b..4fa2473 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcInterceptor.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcInterceptor.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Intercepts dispatcher lifecycle events. @@ -30,7 +30,7 @@ default void beforeInvoke(JsonRpcRequest request) { * Called right after a method handler returns successfully. * * @param request validated request model - * @param result result payload returned by the handler + * @param result result payload returned by the handler */ default void afterInvoke(JsonRpcRequest request, JsonNode result) { } @@ -38,8 +38,8 @@ default void afterInvoke(JsonRpcRequest request, JsonNode result) { /** * Called when any error is mapped to a JSON-RPC error. * - * @param request request model when available; {@code null} when parsing failed before request construction - * @param throwable original exception + * @param request request model when available; {@code null} when parsing failed before request construction + * @param throwable original exception * @param mappedError mapped JSON-RPC error object */ default void onError(@Nullable JsonRpcRequest request, Throwable throwable, JsonRpcError mappedError) { diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodHandler.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodHandler.java index a9bca32..f287ecd 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodHandler.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodHandler.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Functional handler for a single JSON-RPC method. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodInvoker.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodInvoker.java index 87dbdb3..e654afc 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodInvoker.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodInvoker.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Invokes a registered JSON-RPC method handler. @@ -12,7 +12,7 @@ public interface JsonRpcMethodInvoker { * Invokes a handler with optional parameters. * * @param handler handler to invoke - * @param params JSON-RPC params node; may be {@code null} + * @param params JSON-RPC params node; may be {@code null} * @return JSON node returned by the handler * @throws Exception when handler invocation fails */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistration.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistration.java index a0f86d6..7ac8939 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistration.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistration.java @@ -3,7 +3,7 @@ /** * Immutable method registration entry used for programmatic registration. * - * @param method JSON-RPC method name + * @param method JSON-RPC method name * @param handler handler implementation for the method */ public record JsonRpcMethodRegistration(String method, JsonRpcMethodHandler handler) { @@ -11,7 +11,7 @@ public record JsonRpcMethodRegistration(String method, JsonRpcMethodHandler hand /** * Creates a registration entry. * - * @param method JSON-RPC method name + * @param method JSON-RPC method name * @param handler handler implementation for the method * @return registration entry */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistry.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistry.java index 71202f0..cbdc259 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistry.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcMethodRegistry.java @@ -10,7 +10,7 @@ public interface JsonRpcMethodRegistry { /** * Registers a handler for a method name. * - * @param method JSON-RPC method name + * @param method JSON-RPC method name * @param handler handler to register */ void register(String method, JsonRpcMethodHandler handler); diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcParameterBinder.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcParameterBinder.java index a971397..62a0071 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcParameterBinder.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcParameterBinder.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Binds raw JSON-RPC parameters to Java target types. @@ -11,9 +11,9 @@ public interface JsonRpcParameterBinder { /** * Converts a params node into a typed Java value. * - * @param params JSON-RPC params value; may be {@code null} + * @param params JSON-RPC params value; may be {@code null} * @param targetType Java target class for conversion - * @param target value type + * @param target value type * @return converted value * @throws JsonRpcException when parameter conversion fails */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcRequest.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcRequest.java index 04a35f6..209d311 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcRequest.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcRequest.java @@ -1,23 +1,23 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Parsed JSON-RPC request model used by the dispatcher pipeline. * - * @param jsonrpc protocol version string from payload; may be {@code null} - * @param id request id value; may be {@code null} - * @param method method name; may be {@code null} - * @param params raw params payload; may be {@code null} + * @param jsonrpc protocol version string from payload; may be {@code null} + * @param id request id value; may be {@code null} + * @param method method name; may be {@code null} + * @param params raw params payload; may be {@code null} * @param idPresent whether the original payload explicitly contained an {@code id} field */ public record JsonRpcRequest( - @Nullable String jsonrpc, - @Nullable JsonNode id, - @Nullable String method, - @Nullable JsonNode params, - boolean idPresent + @Nullable String jsonrpc, + @Nullable JsonNode id, + @Nullable String method, + @Nullable JsonNode params, + boolean idPresent ) { /** diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponse.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponse.java index 8b32acb..8087b17 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponse.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponse.java @@ -1,8 +1,8 @@ package com.limehee.jsonrpc.core; import com.fasterxml.jackson.annotation.JsonInclude; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * JSON-RPC response payload model. @@ -10,27 +10,26 @@ * Exactly one of {@code result} or {@code error} must be present. * * @param jsonrpc protocol version string - * @param id request id echoed back to caller; may be {@code null} - * @param result success payload; may be {@code null} when {@code error} is present - * @param error error payload; may be {@code null} when {@code result} is present + * @param id request id echoed back to caller; may be {@code null} + * @param result success payload; may be {@code null} when {@code error} is present + * @param error error payload; may be {@code null} when {@code result} is present */ @JsonInclude(JsonInclude.Include.NON_NULL) public record JsonRpcResponse( - String jsonrpc, - @JsonInclude(JsonInclude.Include.ALWAYS) @Nullable JsonNode id, - @Nullable JsonNode result, - @Nullable JsonRpcError error + String jsonrpc, + @JsonInclude(JsonInclude.Include.ALWAYS) @Nullable JsonNode id, + @Nullable JsonNode result, + @Nullable JsonRpcError error ) { /** * Validates canonical response invariants. * * @param jsonrpc protocol version string - * @param id request id echoed back to caller; may be {@code null} - * @param result success payload; must be non-null when {@code error} is {@code null} - * @param error error payload; must be non-null when {@code result} is {@code null} - * @throws IllegalArgumentException when both {@code result} and {@code error} are present or - * when both are absent + * @param id request id echoed back to caller; may be {@code null} + * @param result success payload; must be non-null when {@code error} is {@code null} + * @param error error payload; must be non-null when {@code result} is {@code null} + * @throws IllegalArgumentException when both {@code result} and {@code error} are present or when both are absent */ public JsonRpcResponse { boolean hasResult = result != null; @@ -43,7 +42,7 @@ public record JsonRpcResponse( /** * Creates a successful response. * - * @param id request id; may be {@code null} + * @param id request id; may be {@code null} * @param result success payload * @return success response */ @@ -54,8 +53,8 @@ public static JsonRpcResponse success(@Nullable JsonNode id, JsonNode result) { /** * Creates an error response from code/message. * - * @param id request id; may be {@code null} - * @param code JSON-RPC error code + * @param id request id; may be {@code null} + * @param code JSON-RPC error code * @param message JSON-RPC error message * @return error response */ @@ -66,7 +65,7 @@ public static JsonRpcResponse error(@Nullable JsonNode id, int code, String mess /** * Creates an error response from a prebuilt error object. * - * @param id request id; may be {@code null} + * @param id request id; may be {@code null} * @param error error payload * @return error response */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseComposer.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseComposer.java index 98a8628..6692dad 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseComposer.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseComposer.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Creates JSON-RPC response payloads from method invocation outcomes. @@ -11,7 +11,7 @@ public interface JsonRpcResponseComposer { /** * Creates a successful JSON-RPC response. * - * @param id request identifier; may be {@code null} + * @param id request identifier; may be {@code null} * @param result computed result payload * @return success response */ @@ -20,7 +20,7 @@ public interface JsonRpcResponseComposer { /** * Creates an error JSON-RPC response. * - * @param id request identifier; may be {@code null} + * @param id request identifier; may be {@code null} * @param error mapped error payload * @return error response */ diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseParser.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseParser.java index eb0ee9c..7df0d1c 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseParser.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseParser.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Parses raw JSON payloads into incoming JSON-RPC response envelopes. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptions.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptions.java index 098540c..1e48b2f 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptions.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptions.java @@ -34,8 +34,8 @@ private JsonRpcResponseValidationOptions(Builder builder) { /** * Returns default validation options. *

- * RFC MUST rules are enabled by default. Compatibility-related rules are also configured with - * permissive defaults unless explicitly restricted through builder switches. + * RFC MUST rules are enabled by default. Compatibility-related rules are also configured with permissive defaults + * unless explicitly restricted through builder switches. * * @return default options */ @@ -123,8 +123,8 @@ public boolean requireStringErrorMessage() { } /** - * @return whether response objects may include request-only fields such as {@code method}/{@code params}; - * this is a compatibility policy and not an RFC MUST rule + * @return whether response objects may include request-only fields such as {@code method}/{@code params}; this is a + * compatibility policy and not an RFC MUST rule */ public boolean allowRequestFieldsInResponse() { return allowRequestFieldsInResponse; diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResultWriter.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResultWriter.java index 6724733..1c3e132 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResultWriter.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcResultWriter.java @@ -1,7 +1,7 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Serializes Java method results into JSON trees for JSON-RPC responses. diff --git a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactory.java b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactory.java index bf0be64..0dddbba 100644 --- a/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactory.java +++ b/jsonrpc-core/src/main/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactory.java @@ -6,9 +6,8 @@ /** * Creates {@link JsonRpcMethodHandler} instances from strongly-typed Java callbacks. *

- * This factory bridges the JSON-centric dispatcher contract and user-friendly Java method signatures. - * Implementations are responsible for parameter binding and result serialization through the configured - * binder/writer strategy. + * This factory bridges the JSON-centric dispatcher contract and user-friendly Java method signatures. Implementations + * are responsible for parameter binding and result serialization through the configured binder/writer strategy. */ public interface JsonRpcTypedMethodHandlerFactory { @@ -24,8 +23,8 @@ public interface JsonRpcTypedMethodHandlerFactory { * Creates a handler for methods that accept a single typed argument. * * @param paramType target Java type used for binding the incoming {@code params} - * @param method callback to invoke after binding succeeds - * @param

bound argument type + * @param method callback to invoke after binding succeeds + * @param

bound argument type * @return method handler that binds one argument and serializes the callback result */

JsonRpcMethodHandler unary(Class

paramType, Function method); diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/CoreConstructorNullGuardTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/CoreConstructorNullGuardTest.java index cc5834b..f57c9d7 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/CoreConstructorNullGuardTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/CoreConstructorNullGuardTest.java @@ -1,48 +1,47 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.StringNode; class CoreConstructorNullGuardTest { @Test void dispatcherConstructorRejectsNullMethodRegistry() { assertThrows( - NullPointerException.class, - () -> new JsonRpcDispatcher( - null, - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(), - new DirectJsonRpcNotificationExecutor() - ) + NullPointerException.class, + () -> new JsonRpcDispatcher( + null, + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(), + new DirectJsonRpcNotificationExecutor() + ) ); } @Test void dispatcherConstructorRejectsNullInterceptors() { assertThrows( - NullPointerException.class, - () -> new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - null, - new DirectJsonRpcNotificationExecutor() - ) + NullPointerException.class, + () -> new JsonRpcDispatcher( + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + null, + new DirectJsonRpcNotificationExecutor() + ) ); } @@ -54,20 +53,20 @@ void parameterBinderConstructorRejectsNullObjectMapper() { @Test void typedMethodHandlerFactoryConstructorRejectsNullDependencies() { assertThrows( - NullPointerException.class, - () -> new DefaultJsonRpcTypedMethodHandlerFactory(null, value -> StringNode.valueOf("ok")) + NullPointerException.class, + () -> new DefaultJsonRpcTypedMethodHandlerFactory(null, value -> StringNode.valueOf("ok")) ); assertThrows( - NullPointerException.class, - () -> new DefaultJsonRpcTypedMethodHandlerFactory( - new JsonRpcParameterBinder() { - @Override - public T bind(JsonNode params, Class targetType) { - return null; - } - }, - null - ) + NullPointerException.class, + () -> new DefaultJsonRpcTypedMethodHandlerFactory( + new JsonRpcParameterBinder() { + @Override + public T bind(JsonNode params, Class targetType) { + return null; + } + }, + null + ) ); } diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifierTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifierTest.java index c28c824..f1fdbe7 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifierTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcEnvelopeClassifierTest.java @@ -1,10 +1,10 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; class DefaultJsonRpcEnvelopeClassifierTest { @@ -15,112 +15,112 @@ class DefaultJsonRpcEnvelopeClassifierTest { @Test void classifyReturnsRequestForMethodObject() throws Exception { assertEquals( - JsonRpcEnvelopeType.REQUEST, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """)) + JsonRpcEnvelopeType.REQUEST, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","method":"ping","id":1} + """)) ); } @Test void classifyReturnsRequestForParamsOnlyObject() throws Exception { assertEquals( - JsonRpcEnvelopeType.REQUEST, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","params":{"x":1}} - """)) + JsonRpcEnvelopeType.REQUEST, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","params":{"x":1}} + """)) ); } @Test void classifyReturnsResponseForResultObject() throws Exception { assertEquals( - JsonRpcEnvelopeType.RESPONSE, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","id":1,"result":true} - """)) + JsonRpcEnvelopeType.RESPONSE, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","id":1,"result":true} + """)) ); } @Test void classifyReturnsResponseForErrorObject() throws Exception { assertEquals( - JsonRpcEnvelopeType.RESPONSE, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}} - """)) + JsonRpcEnvelopeType.RESPONSE, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}} + """)) ); } @Test void classifyReturnsResponseWhenResponseHintsExistWithRequestFields() throws Exception { assertEquals( - JsonRpcEnvelopeType.RESPONSE, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","result":true} - """)) + JsonRpcEnvelopeType.RESPONSE, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","method":"ping","result":true} + """)) ); } @Test void classifyReturnsInvalidForObjectWithoutHints() throws Exception { assertEquals( - JsonRpcEnvelopeType.INVALID, - classifier.classify(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","id":1} - """)) + JsonRpcEnvelopeType.INVALID, + classifier.classify(OBJECT_MAPPER.readTree(""" + {"jsonrpc":"2.0","id":1} + """)) ); } @Test void classifyReturnsRequestForHomogeneousRequestBatch() throws Exception { assertEquals( - JsonRpcEnvelopeType.REQUEST, - classifier.classify(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"a","id":1}, - {"jsonrpc":"2.0","method":"b"} - ] - """)) + JsonRpcEnvelopeType.REQUEST, + classifier.classify(OBJECT_MAPPER.readTree(""" + [ + {"jsonrpc":"2.0","method":"a","id":1}, + {"jsonrpc":"2.0","method":"b"} + ] + """)) ); } @Test void classifyReturnsResponseForHomogeneousResponseBatch() throws Exception { assertEquals( - JsonRpcEnvelopeType.RESPONSE, - classifier.classify(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","id":1,"result":1}, - {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"x"}} - ] - """)) + JsonRpcEnvelopeType.RESPONSE, + classifier.classify(OBJECT_MAPPER.readTree(""" + [ + {"jsonrpc":"2.0","id":1,"result":1}, + {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"x"}} + ] + """)) ); } @Test void classifyReturnsInvalidForMixedBatch() throws Exception { assertEquals( - JsonRpcEnvelopeType.INVALID, - classifier.classify(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"a","id":1}, - {"jsonrpc":"2.0","id":1,"result":1} - ] - """)) + JsonRpcEnvelopeType.INVALID, + classifier.classify(OBJECT_MAPPER.readTree(""" + [ + {"jsonrpc":"2.0","method":"a","id":1}, + {"jsonrpc":"2.0","id":1,"result":1} + ] + """)) ); } @Test void classifyReturnsInvalidForNonObjectBatchEntry() throws Exception { assertEquals( - JsonRpcEnvelopeType.INVALID, - classifier.classify(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","id":1,"result":1}, - 3 - ] - """)) + JsonRpcEnvelopeType.INVALID, + classifier.classify(OBJECT_MAPPER.readTree(""" + [ + {"jsonrpc":"2.0","id":1,"result":1}, + 3 + ] + """)) ); } diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolverTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolverTest.java index 1501b81..e6852e5 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolverTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcExceptionResolverTest.java @@ -1,11 +1,11 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.StringNode; + class DefaultJsonRpcExceptionResolverTest { @Test @@ -47,4 +47,14 @@ void mapsNonJsonRpcExceptionsToInternalError() { assertEquals(JsonRpcErrorCode.INTERNAL_ERROR, error.code()); assertEquals(JsonRpcConstants.MESSAGE_INTERNAL_ERROR, error.message()); } + + @Test + void fallsBackToInternalErrorMessageWhenDomainExceptionMessageIsNull() { + DefaultJsonRpcExceptionResolver resolver = new DefaultJsonRpcExceptionResolver(false); + + JsonRpcError error = resolver.resolve(new JsonRpcException(-32000, null)); + + assertEquals(-32000, error.code()); + assertEquals(JsonRpcConstants.MESSAGE_INTERNAL_ERROR, error.message()); + } } diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestParserTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestParserTest.java index 4638dca..926cb14 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestParserTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestParserTest.java @@ -1,15 +1,15 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + class DefaultJsonRpcRequestParserTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); @@ -34,8 +34,8 @@ void parseRejectsNonObjectPayload() throws Exception { @Test void parseExtractsRequestFields() throws Exception { JsonRpcRequest request = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","params":{"value":1},"id":"abc"} - """)); + {"jsonrpc":"2.0","method":"ping","params":{"value":1},"id":"abc"} + """)); assertEquals("2.0", request.jsonrpc()); assertEquals("ping", request.method()); @@ -48,8 +48,8 @@ void parseExtractsRequestFields() throws Exception { @Test void parseTreatsMissingIdAsNotification() throws Exception { JsonRpcRequest request = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping"} - """)); + {"jsonrpc":"2.0","method":"ping"} + """)); assertFalse(request.idPresent()); assertNull(request.id()); @@ -59,8 +59,8 @@ void parseTreatsMissingIdAsNotification() throws Exception { @Test void parseDistinguishesExplicitNullIdFromAbsentId() throws Exception { JsonRpcRequest request = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":null} - """)); + {"jsonrpc":"2.0","method":"ping","id":null} + """)); assertTrue(request.idPresent()); assertTrue(request.id().isNull()); @@ -70,8 +70,8 @@ void parseDistinguishesExplicitNullIdFromAbsentId() throws Exception { @Test void parseConvertsNonTextJsonrpcAndMethodToNull() throws Exception { JsonRpcRequest request = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":2.0,"method":true,"id":1} - """)); + {"jsonrpc":2.0,"method":true,"id":1} + """)); assertNull(request.jsonrpc()); assertNull(request.method()); diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidatorTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidatorTest.java index 33d5aaa..0375c19 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidatorTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcRequestValidatorTest.java @@ -1,15 +1,15 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.IntNode; import tools.jackson.databind.node.NullNode; import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; class DefaultJsonRpcRequestValidatorTest { @@ -43,11 +43,11 @@ void validateRejectsMissingMethod() { @Test void validateRejectsInvalidIdType() { JsonRpcRequest request = new JsonRpcRequest( - "2.0", - OBJECT_MAPPER.createObjectNode().put("x", 1), - "ping", - null, - true + "2.0", + OBJECT_MAPPER.createObjectNode().put("x", 1), + "ping", + null, + true ); JsonRpcException ex = assertThrows(JsonRpcException.class, () -> validator.validate(request)); @@ -65,7 +65,7 @@ void validateRejectsPrimitiveParams() { @Test void validateRejectsPrimitiveParamsAsInvalidRequestWhenPolicyIsConfigured() { DefaultJsonRpcRequestValidator strictShapeValidator = new DefaultJsonRpcRequestValidator( - JsonRpcParamsTypeViolationCodePolicy.INVALID_REQUEST + JsonRpcParamsTypeViolationCodePolicy.INVALID_REQUEST ); JsonRpcRequest request = new JsonRpcRequest("2.0", IntNode.valueOf(1), "ping", IntNode.valueOf(3), true); @@ -76,37 +76,37 @@ void validateRejectsPrimitiveParamsAsInvalidRequestWhenPolicyIsConfigured() { @Test void constructorRejectsNullParamsTypeViolationPolicy() { assertThrows( - NullPointerException.class, - () -> new DefaultJsonRpcRequestValidator(null) + NullPointerException.class, + () -> new DefaultJsonRpcRequestValidator(null) ); } @Test void validateAllowsTextOrNumberOrNullId() { assertDoesNotThrow(() -> validator.validate( - new JsonRpcRequest("2.0", StringNode.valueOf("abc"), "ping", null, true))); + new JsonRpcRequest("2.0", StringNode.valueOf("abc"), "ping", null, true))); assertDoesNotThrow(() -> validator.validate( - new JsonRpcRequest("2.0", IntNode.valueOf(7), "ping", null, true))); + new JsonRpcRequest("2.0", IntNode.valueOf(7), "ping", null, true))); assertDoesNotThrow(() -> validator.validate( - new JsonRpcRequest("2.0", NullNode.getInstance(), "ping", null, true))); + new JsonRpcRequest("2.0", NullNode.getInstance(), "ping", null, true))); } @Test void validateAllowsObjectAndArrayParams() throws Exception { assertDoesNotThrow(() -> validator.validate(new JsonRpcRequest( - "2.0", - IntNode.valueOf(1), - "ping", - OBJECT_MAPPER.readTree("{\"x\":1}"), - true + "2.0", + IntNode.valueOf(1), + "ping", + OBJECT_MAPPER.readTree("{\"x\":1}"), + true ))); assertDoesNotThrow(() -> validator.validate(new JsonRpcRequest( - "2.0", - IntNode.valueOf(2), - "ping", - OBJECT_MAPPER.readTree("[1,2,3]"), - true + "2.0", + IntNode.valueOf(2), + "ping", + OBJECT_MAPPER.readTree("[1,2,3]"), + true ))); } diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParserTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParserTest.java index cd84828..63113b9 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParserTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/DefaultJsonRpcResponseParserTest.java @@ -1,15 +1,15 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + class DefaultJsonRpcResponseParserTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); @@ -19,8 +19,8 @@ class DefaultJsonRpcResponseParserTest { @Test void parseParsesSingleResponseObject() throws Exception { JsonRpcIncomingResponseEnvelope envelope = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","id":1,"result":{"ok":true}} - """)); + {"jsonrpc":"2.0","id":1,"result":{"ok":true}} + """)); assertFalse(envelope.isBatch()); JsonRpcIncomingResponse response = envelope.singleResponse().orElseThrow(); @@ -33,11 +33,11 @@ void parseParsesSingleResponseObject() throws Exception { @Test void parseParsesBatchResponseArray() throws Exception { JsonRpcIncomingResponseEnvelope envelope = parser.parse(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","id":"a","result":1}, - {"jsonrpc":"2.0","id":"b","error":{"code":-32000,"message":"x"}} - ] - """)); + [ + {"jsonrpc":"2.0","id":"a","result":1}, + {"jsonrpc":"2.0","id":"b","error":{"code":-32000,"message":"x"}} + ] + """)); assertTrue(envelope.isBatch()); assertEquals(2, envelope.responses().size()); @@ -46,8 +46,8 @@ void parseParsesBatchResponseArray() throws Exception { @Test void parseStoresNullVersionWhenJsonrpcFieldIsNotString() throws Exception { JsonRpcIncomingResponseEnvelope envelope = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":2,"id":1,"result":true} - """)); + {"jsonrpc":2,"id":1,"result":true} + """)); JsonRpcIncomingResponse response = envelope.singleResponse().orElseThrow(); assertNull(response.jsonrpc()); @@ -56,8 +56,8 @@ void parseStoresNullVersionWhenJsonrpcFieldIsNotString() throws Exception { @Test void parsePreservesFieldPresenceForNullValues() throws Exception { JsonRpcIncomingResponseEnvelope envelope = parser.parse(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","id":null,"result":null} - """)); + {"jsonrpc":"2.0","id":null,"result":null} + """)); JsonRpcIncomingResponse response = envelope.singleResponse().orElseThrow(); assertTrue(response.idPresent()); @@ -76,7 +76,7 @@ void parseRejectsPrimitiveOrEmptyArrayOrNonObjectBatchEntries() throws Exception assertThrows(JsonRpcException.class, () -> parser.parse(OBJECT_MAPPER.readTree("1"))); assertThrows(JsonRpcException.class, () -> parser.parse(OBJECT_MAPPER.readTree("[]"))); assertThrows(JsonRpcException.class, () -> parser.parse(OBJECT_MAPPER.readTree(""" - [{"jsonrpc":"2.0","id":1,"result":1}, 2] - """))); + [{"jsonrpc":"2.0","id":1,"result":1}, 2] + """))); } } 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 668399f..77db095 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 @@ -1,14 +1,14 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + class DefaultJsonRpcResponseValidatorTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); @@ -19,15 +19,15 @@ class DefaultJsonRpcResponseValidatorTest { @Test void validateAcceptsValidResultResponse() throws Exception { assertDoesNotThrow(() -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":{"ok":true}} - """))); + {"jsonrpc":"2.0","id":1,"result":{"ok":true}} + """))); } @Test void validateAcceptsValidErrorResponse() throws Exception { assertDoesNotThrow(() -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":"abc","error":{"code":-32000,"message":"x"}} - """))); + {"jsonrpc":"2.0","id":"abc","error":{"code":-32000,"message":"x"}} + """))); } @Test @@ -39,201 +39,201 @@ void validateRejectsNullResponse() { @Test void validateRejectsWrongProtocolVersionByDefault() throws Exception { JsonRpcException ex = assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"1.0","id":1,"result":1} - """))); + {"jsonrpc":"1.0","id":1,"result":1} + """))); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); } @Test void validateAllowsWrongProtocolVersionWhenDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireJsonRpcVersion20(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireJsonRpcVersion20(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"1.0","id":1,"result":1} - """))); + {"jsonrpc":"1.0","id":1,"result":1} + """))); } @Test void validateRejectsMissingIdByDefault() throws Exception { JsonRpcException ex = assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","result":1} - """))); + {"jsonrpc":"2.0","result":1} + """))); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); } @Test void validateAllowsMissingIdWhenRuleDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireResponseIdMember(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireResponseIdMember(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"2.0","result":1} - """))); + {"jsonrpc":"2.0","result":1} + """))); } @Test void validateRejectsInvalidIdTypes() throws Exception { JsonRpcException ex = assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":{"x":1},"result":1} - """))); + {"jsonrpc":"2.0","id":{"x":1},"result":1} + """))); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, ex.getCode()); } @Test void validateRespectsIdTypeOptions() throws Exception { JsonRpcResponseValidator noNull = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder().allowNullResponseId(false).build() + JsonRpcResponseValidationOptions.builder().allowNullResponseId(false).build() ); assertThrows(JsonRpcException.class, () -> noNull.validate(incoming(""" - {"jsonrpc":"2.0","id":null,"result":1} - """))); + {"jsonrpc":"2.0","id":null,"result":1} + """))); JsonRpcResponseValidator noString = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder().allowStringResponseId(false).build() + JsonRpcResponseValidationOptions.builder().allowStringResponseId(false).build() ); assertThrows(JsonRpcException.class, () -> noString.validate(incoming(""" - {"jsonrpc":"2.0","id":"x","result":1} - """))); + {"jsonrpc":"2.0","id":"x","result":1} + """))); JsonRpcResponseValidator noNumeric = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder().allowNumericResponseId(false).build() + JsonRpcResponseValidationOptions.builder().allowNumericResponseId(false).build() ); assertThrows(JsonRpcException.class, () -> noNumeric.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":1} - """))); + {"jsonrpc":"2.0","id":1,"result":1} + """))); } @Test void validateRejectsFractionalIdWhenDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .allowFractionalResponseId(false) - .build() + JsonRpcResponseValidationOptions.builder() + .allowFractionalResponseId(false) + .build() ); assertThrows(JsonRpcException.class, () -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1.5,"result":1} - """))); + {"jsonrpc":"2.0","id":1.5,"result":1} + """))); } @Test void validateRejectsInvalidResultAndErrorCombination() throws Exception { assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":1,"error":{"code":-32000,"message":"x"}} - """))); + {"jsonrpc":"2.0","id":1,"result":1,"error":{"code":-32000,"message":"x"}} + """))); assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1} - """))); + {"jsonrpc":"2.0","id":1} + """))); } @Test void validateAllowsResultAndErrorCombinationWhenExclusiveRuleDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireExclusiveResultOrError(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireExclusiveResultOrError(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":1,"error":{"code":-32000,"message":"x"}} - """))); + {"jsonrpc":"2.0","id":1,"result":1,"error":{"code":-32000,"message":"x"}} + """))); } @Test void validateAllowsMissingResultAndErrorWhenExclusiveRuleDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireExclusiveResultOrError(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireExclusiveResultOrError(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1} - """))); + {"jsonrpc":"2.0","id":1} + """))); } @Test void validateRejectsInvalidErrorObjectStructure() throws Exception { assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":1} - """))); + {"jsonrpc":"2.0","id":1,"error":1} + """))); assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32000}} - """))); + {"jsonrpc":"2.0","id":1,"error":{"code":-32000}} + """))); assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"code":1.5,"message":"x"}} - """))); + {"jsonrpc":"2.0","id":1,"error":{"code":1.5,"message":"x"}} + """))); } @Test void validateRejectsNonNumericErrorCodeAndNonStringErrorMessage() throws Exception { assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"code":"x","message":"err"}} - """))); + {"jsonrpc":"2.0","id":1,"error":{"code":"x","message":"err"}} + """))); assertThrows(JsonRpcException.class, () -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":3}} - """))); + {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":3}} + """))); } @Test void validateAllowsNonObjectErrorWhenObjectRuleIsDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireErrorObjectWhenPresent(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireErrorObjectWhenPresent(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":1} - """))); + {"jsonrpc":"2.0","id":1,"error":1} + """))); } @Test void validateAllowsMissingErrorCodeAndMessageWhenRulesAreDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .requireIntegerErrorCode(false) - .requireStringErrorMessage(false) - .build() + JsonRpcResponseValidationOptions.builder() + .requireIntegerErrorCode(false) + .requireStringErrorMessage(false) + .build() ); assertDoesNotThrow(() -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"error":{"data":{"x":1}}} - """))); + {"jsonrpc":"2.0","id":1,"error":{"data":{"x":1}}} + """))); } @Test void validateCanRejectRequestFieldsWhenOptionDisabled() throws Exception { JsonRpcResponseValidator custom = new DefaultJsonRpcResponseValidator( - JsonRpcResponseValidationOptions.builder() - .allowRequestFieldsInResponse(false) - .build() + JsonRpcResponseValidationOptions.builder() + .allowRequestFieldsInResponse(false) + .build() ); assertThrows(JsonRpcException.class, () -> custom.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":1,"method":"ping"} - """))); + {"jsonrpc":"2.0","id":1,"result":1,"method":"ping"} + """))); } @Test void validateAllowsRequestFieldsByDefault() throws Exception { assertDoesNotThrow(() -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1,"result":1,"method":"ping","params":{"a":1}} - """))); + {"jsonrpc":"2.0","id":1,"result":1,"method":"ping","params":{"a":1}} + """))); } @Test void validateAllowsFractionalIdByDefault() throws Exception { assertDoesNotThrow(() -> validator.validate(incoming(""" - {"jsonrpc":"2.0","id":1.5,"result":1} - """))); + {"jsonrpc":"2.0","id":1.5,"result":1} + """))); } @Test diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/InMemoryJsonRpcMethodRegistryTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/InMemoryJsonRpcMethodRegistryTest.java index f20fb9c..b8f8f59 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/InMemoryJsonRpcMethodRegistryTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/InMemoryJsonRpcMethodRegistryTest.java @@ -1,11 +1,11 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.StringNode; + class InMemoryJsonRpcMethodRegistryTest { @Test @@ -14,13 +14,13 @@ void rejectsDuplicateRegistrationByDefault() { registry.register("ping", params -> StringNode.valueOf("pong1")); assertThrows(IllegalStateException.class, - () -> registry.register("ping", params -> StringNode.valueOf("pong2"))); + () -> registry.register("ping", params -> StringNode.valueOf("pong2"))); } @Test void replacesRegistrationWhenConfigured() { InMemoryJsonRpcMethodRegistry registry = new InMemoryJsonRpcMethodRegistry( - JsonRpcMethodRegistrationConflictPolicy.REPLACE + JsonRpcMethodRegistrationConflictPolicy.REPLACE ); registry.register("ping", params -> StringNode.valueOf("pong1")); @@ -32,11 +32,11 @@ void replacesRegistrationWhenConfigured() { @Test void alwaysRejectsReservedRpcPrefix() { InMemoryJsonRpcMethodRegistry registry = new InMemoryJsonRpcMethodRegistry( - JsonRpcMethodRegistrationConflictPolicy.REPLACE + JsonRpcMethodRegistrationConflictPolicy.REPLACE ); assertThrows(IllegalArgumentException.class, - () -> registry.register("rpc.system", params -> StringNode.valueOf("ok"))); + () -> registry.register("rpc.system", params -> StringNode.valueOf("ok"))); } @Test diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinderTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinderTest.java index 5170d26..cc3677f 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinderTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JacksonJsonRpcParameterBinderTest.java @@ -1,16 +1,16 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.NullNode; import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; class JacksonJsonRpcParameterBinderTest { @@ -31,6 +31,7 @@ void bindReturnsJsonNodeWithoutConversion() { assertSame(node, binder.bind(node, JsonNode.class)); assertSame(NullNode.getInstance(), binder.bind(NullNode.getInstance(), JsonNode.class)); + assertSame(NullNode.getInstance(), binder.bind(null, JsonNode.class)); } @Test @@ -49,12 +50,13 @@ void bindConvertsNullToReferenceType() { @Test void bindThrowsInvalidParamsWhenConversionFails() { JsonRpcException ex = assertThrows(JsonRpcException.class, - () -> binder.bind(StringNode.valueOf("bad"), PingParams.class)); + () -> binder.bind(StringNode.valueOf("bad"), PingParams.class)); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, ex.getCode()); assertEquals(JsonRpcConstants.MESSAGE_INVALID_PARAMS, ex.getMessage()); } record PingParams(String name) { + } } diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcDispatcherTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcDispatcherTest.java index d179b17..c158f1e 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcDispatcherTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcDispatcherTest.java @@ -1,17 +1,5 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.node.ObjectNode; -import tools.jackson.databind.node.IntNode; -import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -19,6 +7,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.StringNode; + class JsonRpcDispatcherTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); @@ -29,8 +28,8 @@ void dispatchSingleRequestReturnsSuccess() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """)); + {"jsonrpc":"2.0","method":"ping","id":1} + """)); assertFalse(result.isBatch()); assertTrue(result.hasResponse()); @@ -46,8 +45,8 @@ void dispatchSingleNotificationReturnsNoResponse() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping"} - """)); + {"jsonrpc":"2.0","method":"ping"} + """)); assertFalse(result.hasResponse()); assertTrue(result.singleResponse().isEmpty()); @@ -58,8 +57,8 @@ void dispatchInvalidRequestWithoutIdReturnsErrorResponse() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","params":[]} - """)); + {"jsonrpc":"2.0","params":[]} + """)); assertTrue(result.hasResponse()); JsonRpcResponse response = result.singleResponse().orElseThrow(); @@ -72,8 +71,8 @@ void dispatchInvalidRequestWithBooleanIdReturnsNullIdInErrorResponse() throws Ex JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":true} - """)); + {"jsonrpc":"2.0","method":"ping","id":true} + """)); assertTrue(result.hasResponse()); JsonRpcResponse response = result.singleResponse().orElseThrow(); @@ -86,8 +85,8 @@ void dispatchNotificationMethodNotFoundReturnsNoResponse() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"missing"} - """)); + {"jsonrpc":"2.0","method":"missing"} + """)); assertFalse(result.hasResponse()); assertTrue(result.singleResponse().isEmpty()); @@ -98,15 +97,15 @@ void dispatchNotificationUsesNotificationExecutor() throws Exception { RecordingNotificationExecutor notificationExecutor = new RecordingNotificationExecutor(); AtomicInteger invocationCount = new AtomicInteger(); JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(), - notificationExecutor + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(), + notificationExecutor ); dispatcher.register("ping", params -> { invocationCount.incrementAndGet(); @@ -114,8 +113,8 @@ void dispatchNotificationUsesNotificationExecutor() throws Exception { }); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping"} - """)); + {"jsonrpc":"2.0","method":"ping"} + """)); assertFalse(result.hasResponse()); assertEquals(1, notificationExecutor.executeCount); @@ -128,8 +127,8 @@ void dispatchRequestWithExplicitNullIdReturnsResponse() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":null} - """)); + {"jsonrpc":"2.0","method":"ping","id":null} + """)); assertTrue(result.hasResponse()); JsonRpcResponse response = result.singleResponse().orElseThrow(); @@ -142,8 +141,8 @@ void dispatchMethodNotFoundReturnsError() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"unknown","id":1} - """)); + {"jsonrpc":"2.0","method":"unknown","id":1} + """)); JsonRpcResponse response = result.singleResponse().orElseThrow(); assertNotNull(response.error()); @@ -156,8 +155,8 @@ void dispatchInvalidParamsReturnsError() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","params":1,"id":1} - """)); + {"jsonrpc":"2.0","method":"ping","params":1,"id":1} + """)); JsonRpcResponse response = result.singleResponse().orElseThrow(); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, response.error().code()); @@ -169,13 +168,13 @@ void dispatchBatchReturnsOnlyNonNotificationResponses() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"missing","id":2}, - 1 - ] - """)); + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"missing","id":2}, + 1 + ] + """)); assertTrue(result.isBatch()); List responses = result.responses(); @@ -196,11 +195,11 @@ void dispatchBatchIncludesInvalidRequestWithoutId() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","params":[]}, - {"jsonrpc":"2.0","method":"ping"} - ] - """)); + [ + {"jsonrpc":"2.0","params":[]}, + {"jsonrpc":"2.0","method":"ping"} + ] + """)); assertTrue(result.isBatch()); assertEquals(1, result.responses().size()); @@ -214,11 +213,11 @@ void dispatchBatchInvalidIdTypeUsesNullIdInErrorResponse() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"ping","id":{"x":1}}, - {"jsonrpc":"2.0","method":"ping","id":1} - ] - """)); + [ + {"jsonrpc":"2.0","method":"ping","id":{"x":1}}, + {"jsonrpc":"2.0","method":"ping","id":1} + ] + """)); assertTrue(result.isBatch()); assertEquals(2, result.responses().size()); @@ -233,11 +232,11 @@ void dispatchNotificationOnlyBatchReturnsNoResponses() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"ping"} - ] - """)); + [ + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"ping"} + ] + """)); assertTrue(result.isBatch()); assertFalse(result.hasResponse()); @@ -257,23 +256,23 @@ void dispatchEmptyBatchReturnsSingleInvalidRequest() throws Exception { @Test void dispatchBatchOverLimitReturnsInvalidRequest() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 1, - List.of() + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 1, + List.of() ); dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping","id":2} - ] - """)); + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping","id":2} + ] + """)); JsonRpcResponse response = result.singleResponse().orElseThrow(); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, response.error().code()); @@ -291,7 +290,8 @@ void parseErrorResponseReturnsParseErrorCode() { @Test void errorResponseWithUnknownIdSerializesIdAsNull() { - JsonRpcResponse response = JsonRpcResponse.error(null, JsonRpcErrorCode.INVALID_REQUEST, JsonRpcConstants.MESSAGE_INVALID_REQUEST); + JsonRpcResponse response = JsonRpcResponse.error(null, JsonRpcErrorCode.INVALID_REQUEST, + JsonRpcConstants.MESSAGE_INVALID_REQUEST); ObjectNode node = OBJECT_MAPPER.valueToTree(response); assertTrue(node.has("id")); @@ -303,7 +303,7 @@ void registeringReservedMethodThrowsByDefault() { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); assertThrows(IllegalArgumentException.class, - () -> dispatcher.register("rpc.system", params -> StringNode.valueOf("ok"))); + () -> dispatcher.register("rpc.system", params -> StringNode.valueOf("ok"))); } @Test @@ -340,18 +340,29 @@ void legacyDispatchValidNotificationMethodNotFoundReturnsNull() { assertNull(response); } + @Test + void legacyDispatchNullRequestReturnsInvalidRequestError() { + JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); + + JsonRpcResponse response = dispatcher.dispatch((JsonRpcRequest) null); + + assertNotNull(response); + assertEquals(JsonRpcErrorCode.INVALID_REQUEST, response.error().code()); + assertNull(response.id()); + } + @Test void interceptorCallbacksRunForSuccessfulRequest() throws Exception { RecordingInterceptor interceptor = new RecordingInterceptor(); JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(interceptor) + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(interceptor) ); dispatcher.register("ping", params -> StringNode.valueOf("pong")); @@ -364,17 +375,18 @@ void interceptorCallbacksRunForSuccessfulRequest() throws Exception { void interceptorOnErrorRunsForMethodErrors() throws Exception { RecordingInterceptor interceptor = new RecordingInterceptor(); JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(interceptor) + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(interceptor) ); - JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"method\":\"missing\",\"id\":1}")); + JsonRpcDispatchResult result = dispatcher.dispatch( + OBJECT_MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"method\":\"missing\",\"id\":1}")); assertEquals(JsonRpcErrorCode.METHOD_NOT_FOUND, result.singleResponse().orElseThrow().error().code()); assertTrue(interceptor.events.contains("onError:-32601")); @@ -385,23 +397,23 @@ void notificationInvocationErrorTriggersOnErrorInterceptor() throws Exception { RecordingInterceptor interceptor = new RecordingInterceptor(); RecordingNotificationExecutor notificationExecutor = new RecordingNotificationExecutor(); JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(interceptor), - notificationExecutor + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(interceptor), + notificationExecutor ); dispatcher.register("fail", params -> { throw new RuntimeException("boom"); }); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"fail"} - """)); + {"jsonrpc":"2.0","method":"fail"} + """)); assertFalse(result.hasResponse()); assertTrue(interceptor.events.contains("onError:-32603")); @@ -418,19 +430,19 @@ public void onError(JsonRpcRequest request, Throwable throwable, JsonRpcError ma }; JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(throwingInterceptor, interceptor) + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(throwingInterceptor, interceptor) ); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"missing","id":1} - """)); + {"jsonrpc":"2.0","method":"missing","id":1} + """)); JsonRpcResponse response = result.singleResponse().orElseThrow(); assertEquals(JsonRpcErrorCode.METHOD_NOT_FOUND, response.error().code()); @@ -445,8 +457,8 @@ void dispatchRequestPropagatesErrorFromHandler() throws Exception { }); assertThrows(AssertionError.class, () -> dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"fatal","id":1} - """))); + {"jsonrpc":"2.0","method":"fatal","id":1} + """))); } @Test @@ -469,11 +481,12 @@ void dispatchNotificationPropagatesErrorFromHandler() throws Exception { }); assertThrows(AssertionError.class, () -> dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"fatal"} - """))); + {"jsonrpc":"2.0","method":"fatal"} + """))); } private static final class RecordingInterceptor implements JsonRpcInterceptor { + private final List events = new ArrayList<>(); @Override @@ -498,6 +511,7 @@ public void onError(JsonRpcRequest request, Throwable throwable, JsonRpcError ma } private static final class RecordingNotificationExecutor implements JsonRpcNotificationExecutor { + private int executeCount; @Override diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponseEnvelopeTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponseEnvelopeTest.java index 22d2737..46ce75c 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponseEnvelopeTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcIncomingResponseEnvelopeTest.java @@ -1,29 +1,28 @@ package com.limehee.jsonrpc.core; -import tools.jackson.databind.node.IntNode; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.IntNode; + class JsonRpcIncomingResponseEnvelopeTest { @Test void singleResponseReturnsEntryForSingleEnvelope() { JsonRpcIncomingResponse response = new JsonRpcIncomingResponse( - IntNode.valueOf(1), - "2.0", - IntNode.valueOf(1), - true, - IntNode.valueOf(7), - true, - null, - false + IntNode.valueOf(1), + "2.0", + IntNode.valueOf(1), + true, + IntNode.valueOf(7), + true, + null, + false ); JsonRpcIncomingResponseEnvelope envelope = JsonRpcIncomingResponseEnvelope.single(response); @@ -36,14 +35,14 @@ void singleResponseReturnsEntryForSingleEnvelope() { @Test void singleResponseReturnsEmptyForBatchEnvelope() { JsonRpcIncomingResponse response = new JsonRpcIncomingResponse( - IntNode.valueOf(1), - "2.0", - IntNode.valueOf(1), - true, - IntNode.valueOf(7), - true, - null, - false + IntNode.valueOf(1), + "2.0", + IntNode.valueOf(1), + true, + IntNode.valueOf(7), + true, + null, + false ); JsonRpcIncomingResponseEnvelope envelope = JsonRpcIncomingResponseEnvelope.batch(List.of(response)); @@ -55,14 +54,14 @@ void singleResponseReturnsEmptyForBatchEnvelope() { @Test void responsesListIsImmutableSnapshot() { JsonRpcIncomingResponse response = new JsonRpcIncomingResponse( - IntNode.valueOf(1), - "2.0", - IntNode.valueOf(1), - true, - IntNode.valueOf(7), - true, - null, - false + IntNode.valueOf(1), + "2.0", + IntNode.valueOf(1), + true, + IntNode.valueOf(7), + true, + null, + false ); List mutable = new ArrayList<>(); mutable.add(response); diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptionsTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptionsTest.java index 1364d23..d88c520 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptionsTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcResponseValidationOptionsTest.java @@ -1,10 +1,10 @@ package com.limehee.jsonrpc.core; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + class JsonRpcResponseValidationOptionsTest { @Test @@ -27,18 +27,18 @@ void defaultsEnableRfcMustRules() { @Test void builderAllowsOverridingEachFlag() { JsonRpcResponseValidationOptions options = JsonRpcResponseValidationOptions.builder() - .requireJsonRpcVersion20(false) - .requireResponseIdMember(false) - .allowNullResponseId(false) - .allowStringResponseId(false) - .allowNumericResponseId(false) - .allowFractionalResponseId(false) - .requireExclusiveResultOrError(false) - .requireErrorObjectWhenPresent(false) - .requireIntegerErrorCode(false) - .requireStringErrorMessage(false) - .allowRequestFieldsInResponse(false) - .build(); + .requireJsonRpcVersion20(false) + .requireResponseIdMember(false) + .allowNullResponseId(false) + .allowStringResponseId(false) + .allowNumericResponseId(false) + .allowFractionalResponseId(false) + .requireExclusiveResultOrError(false) + .requireErrorObjectWhenPresent(false) + .requireIntegerErrorCode(false) + .requireStringErrorMessage(false) + .allowRequestFieldsInResponse(false) + .build(); assertFalse(options.requireJsonRpcVersion20()); assertFalse(options.requireResponseIdMember()); diff --git a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactoryTest.java b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactoryTest.java index 48c690e..fe30864 100644 --- a/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactoryTest.java +++ b/jsonrpc-core/src/test/java/com/limehee/jsonrpc/core/JsonRpcTypedMethodHandlerFactoryTest.java @@ -1,20 +1,20 @@ package com.limehee.jsonrpc.core; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.StringNode; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; class JsonRpcTypedMethodHandlerFactoryTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); private final JsonRpcTypedMethodHandlerFactory factory = new DefaultJsonRpcTypedMethodHandlerFactory( - new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), - new JacksonJsonRpcResultWriter(OBJECT_MAPPER) + new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), + new JacksonJsonRpcResultWriter(OBJECT_MAPPER) ); @Test @@ -45,11 +45,12 @@ void unaryBindingFailureThrowsInvalidParams() { JsonRpcMethodHandler handler = factory.unary(PingParams.class, params -> "hello " + params.name()); JsonRpcException ex = assertThrows(JsonRpcException.class, - () -> handler.handle(StringNode.valueOf("bad"))); + () -> handler.handle(StringNode.valueOf("bad"))); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, ex.getCode()); assertEquals(JsonRpcConstants.MESSAGE_INVALID_PARAMS, ex.getMessage()); } record PingParams(String name) { + } } diff --git a/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryE2ETest.java b/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryE2ETest.java index 77bb2c7..343440f 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryE2ETest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryE2ETest.java @@ -1,9 +1,13 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.limehee.jsonrpc.core.JsonRpcMethod; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -12,38 +16,31 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; @SpringBootTest( - classes = JsonRpcLibraryE2ETest.TestApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { - "jsonrpc.enabled=true", - "jsonrpc.path=/jsonrpc", - "jsonrpc.scan-annotated-methods=true" - } + classes = JsonRpcLibraryE2ETest.TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "jsonrpc.enabled=true", + "jsonrpc.path=/jsonrpc", + "jsonrpc.scan-annotated-methods=true" + } ) class JsonRpcLibraryE2ETest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); - + private final HttpClient httpClient = HttpClient.newHttpClient(); @LocalServerPort private int port; - private final HttpClient httpClient = HttpClient.newHttpClient(); - @Test void e2eHttpRequestReturnsJsonRpcSuccessResponse() throws Exception { HttpResponse response = call(""" - {"jsonrpc":"2.0","method":"ping","id":10} - """); + {"jsonrpc":"2.0","method":"ping","id":10} + """); assertEquals(200, response.statusCode()); JsonNode body = OBJECT_MAPPER.readTree(response.body()); @@ -55,8 +52,8 @@ void e2eHttpRequestReturnsJsonRpcSuccessResponse() throws Exception { @Test void e2eNotificationReturnsNoContent() throws Exception { HttpResponse response = call(""" - {"jsonrpc":"2.0","method":"ping"} - """); + {"jsonrpc":"2.0","method":"ping"} + """); assertEquals(204, response.statusCode()); assertTrue(response.body().isEmpty()); @@ -65,8 +62,8 @@ void e2eNotificationReturnsNoContent() throws Exception { @Test void e2eUnknownMethodReturnsJsonRpcError() throws Exception { HttpResponse response = call(""" - {"jsonrpc":"2.0","method":"missing","id":99} - """); + {"jsonrpc":"2.0","method":"missing","id":99} + """); assertEquals(200, response.statusCode()); JsonNode body = OBJECT_MAPPER.readTree(response.body()); @@ -76,10 +73,10 @@ void e2eUnknownMethodReturnsJsonRpcError() throws Exception { private HttpResponse call(String payload) throws Exception { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + port + "/jsonrpc")) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(payload)) - .build(); + .uri(URI.create("http://localhost:" + port + "/jsonrpc")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(payload)) + .build(); return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } @@ -87,10 +84,12 @@ private HttpResponse call(String payload) throws Exception { @EnableAutoConfiguration @Import(TestRpcConfiguration.class) static class TestApplication { + } @Configuration(proxyBeanMethods = false) static class TestRpcConfiguration { + @Bean E2eRpcService e2eRpcService() { return new E2eRpcService(); @@ -98,6 +97,7 @@ E2eRpcService e2eRpcService() { } static class E2eRpcService { + @JsonRpcMethod("ping") public String ping() { return "pong"; diff --git a/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesE2ETest.java b/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesE2ETest.java index ec1c9a7..4138330 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesE2ETest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/e2eTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesE2ETest.java @@ -1,12 +1,16 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.node.StringNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.limehee.jsonrpc.core.JsonRpcMethod; import com.limehee.jsonrpc.core.JsonRpcMethodRegistration; import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -15,39 +19,32 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.StringNode; @SpringBootTest( - classes = JsonRpcRegistrationStylesE2ETest.TestApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { - "jsonrpc.enabled=true", - "jsonrpc.path=/jsonrpc", - "jsonrpc.scan-annotated-methods=true" - } + classes = JsonRpcRegistrationStylesE2ETest.TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "jsonrpc.enabled=true", + "jsonrpc.path=/jsonrpc", + "jsonrpc.scan-annotated-methods=true" + } ) class JsonRpcRegistrationStylesE2ETest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); - + private final HttpClient httpClient = HttpClient.newHttpClient(); @LocalServerPort private int port; - private final HttpClient httpClient = HttpClient.newHttpClient(); - @Test void e2eSupportsAnnotationWithRecordReturn() throws Exception { JsonNode body = call(""" - {"jsonrpc":"2.0","method":"annot.user","params":{"id":11},"id":1} - """); + {"jsonrpc":"2.0","method":"annot.user","params":{"id":11},"id":1} + """); assertEquals(11, body.get("result").get("id").asInt()); assertEquals("user-11", body.get("result").get("name").asString()); @@ -56,8 +53,8 @@ void e2eSupportsAnnotationWithRecordReturn() throws Exception { @Test void e2eSupportsManualRegistration() throws Exception { JsonNode body = call(""" - {"jsonrpc":"2.0","method":"manual.ping","id":2} - """); + {"jsonrpc":"2.0","method":"manual.ping","id":2} + """); assertEquals("pong-manual", body.get("result").asString()); } @@ -65,11 +62,11 @@ void e2eSupportsManualRegistration() throws Exception { @Test void e2eSupportsTypedFactoryRegistrationWithClassParamAndCollectionReturn() throws Exception { JsonNode upper = call(""" - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"rpc"},"id":3} - """); + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"rpc"},"id":3} + """); JsonNode tags = call(""" - {"jsonrpc":"2.0","method":"typed.tags","id":4} - """); + {"jsonrpc":"2.0","method":"typed.tags","id":4} + """); assertEquals("RPC", upper.get("result").get("result").asString()); assertTrue(tags.get("result").isArray()); @@ -79,10 +76,10 @@ void e2eSupportsTypedFactoryRegistrationWithClassParamAndCollectionReturn() thro private JsonNode call(String payload) throws Exception { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + port + "/jsonrpc")) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(payload)) - .build(); + .uri(URI.create("http://localhost:" + port + "/jsonrpc")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(payload)) + .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); return OBJECT_MAPPER.readTree(response.body()); @@ -92,10 +89,12 @@ private JsonNode call(String payload) throws Exception { @EnableAutoConfiguration @Import(TestRpcConfiguration.class) static class TestApplication { + } @Configuration(proxyBeanMethods = false) static class TestRpcConfiguration { + @Bean RegistrationStyleAnnotatedService registrationStyleAnnotatedService() { return new RegistrationStyleAnnotatedService(); @@ -109,18 +108,19 @@ JsonRpcMethodRegistration manualPingRegistration() { @Bean JsonRpcMethodRegistration typedUpperRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of("typed.upper", - factory.unary(UpperInput.class, params -> new UpperOutput( - params.value == null ? "" : params.value.toUpperCase()))); + factory.unary(UpperInput.class, params -> new UpperOutput( + params.value == null ? "" : params.value.toUpperCase()))); } @Bean JsonRpcMethodRegistration typedTagsRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of("typed.tags", - factory.noParams(() -> List.of("alpha", "beta"))); + factory.noParams(() -> List.of("alpha", "beta"))); } } static class RegistrationStyleAnnotatedService { + @JsonRpcMethod("annot.user") public UserResponse user(UserRequest request) { return new UserResponse(request.id, "user-" + request.id); @@ -128,17 +128,21 @@ public UserResponse user(UserRequest request) { } static class UserRequest { + public int id; } record UserResponse(int id, String name) { + } static class UpperInput { + public String value; } static class UpperOutput { + public String result; UpperOutput(String result) { diff --git a/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryIntegrationTest.java b/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryIntegrationTest.java index fe35075..5921214 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryIntegrationTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcLibraryIntegrationTest.java @@ -1,8 +1,13 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.limehee.jsonrpc.core.JsonRpcDispatchResult; import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcMethod; @@ -21,24 +26,19 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; @SpringBootTest( - classes = JsonRpcLibraryIntegrationTest.TestApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.MOCK, - properties = { - "jsonrpc.enabled=true", - "jsonrpc.path=/jsonrpc", - "jsonrpc.scan-annotated-methods=true", - "jsonrpc.max-request-bytes=64" - } + classes = JsonRpcLibraryIntegrationTest.TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.MOCK, + properties = { + "jsonrpc.enabled=true", + "jsonrpc.path=/jsonrpc", + "jsonrpc.scan-annotated-methods=true", + "jsonrpc.max-request-bytes=64" + } ) class JsonRpcLibraryIntegrationTest { @@ -60,8 +60,8 @@ void setUp() { @Test void integrationWiresDispatcherAndAnnotatedMethodRegistration() throws Exception { JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"echo","params":{"value":"x"},"id":1} - """)); + {"jsonrpc":"2.0","method":"echo","params":{"value":"x"},"id":1} + """)); JsonRpcResponse response = result.singleResponse().orElseThrow(); assertEquals("echo:x", response.result().asString()); @@ -70,12 +70,12 @@ void integrationWiresDispatcherAndAnnotatedMethodRegistration() throws Exception @Test void integrationInvokesEndpointAndReturnsJsonRpcPayload() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"ping","id":2} - """)) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"ping","id":2} + """)) + .andExpect(status().isOk()) + .andReturn(); JsonNode body = OBJECT_MAPPER.readTree(result.getResponse().getContentAsByteArray()); assertEquals("2.0", body.get("jsonrpc").asString()); @@ -86,23 +86,23 @@ void integrationInvokesEndpointAndReturnsJsonRpcPayload() throws Exception { @Test void integrationReturnsNoContentForNotification() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"ping"} - """)) - .andExpect(status().isNoContent()) - .andExpect(content().string("")); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"ping"} + """)) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); } @Test void integrationRespectsRequestSizeLimit() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"echo","params":{"value":"abcdefghijklmnopqrstuvwxyz"},"id":3} - """)) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"echo","params":{"value":"abcdefghijklmnopqrstuvwxyz"},"id":3} + """)) + .andExpect(status().isOk()) + .andReturn(); JsonNode body = OBJECT_MAPPER.readTree(result.getResponse().getContentAsByteArray()); assertNotNull(body.get("error")); @@ -115,10 +115,12 @@ void integrationRespectsRequestSizeLimit() throws Exception { @EnableAutoConfiguration @Import(TestRpcConfiguration.class) static class TestApplication { + } @Configuration(proxyBeanMethods = false) static class TestRpcConfiguration { + @Bean IntegrationRpcService integrationRpcService() { return new IntegrationRpcService(); @@ -126,6 +128,7 @@ IntegrationRpcService integrationRpcService() { } static class IntegrationRpcService { + @JsonRpcMethod("ping") public String ping() { return "pong"; @@ -137,6 +140,7 @@ public String echo(EchoParams params) { } record EchoParams(String value) { + } } } diff --git a/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesIntegrationTest.java b/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesIntegrationTest.java index 89f5f49..4d880cd 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesIntegrationTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/integrationTest/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcRegistrationStylesIntegrationTest.java @@ -1,12 +1,15 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.node.StringNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.limehee.jsonrpc.core.JsonRpcMethod; import com.limehee.jsonrpc.core.JsonRpcMethodRegistration; import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,23 +24,19 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.StringNode; @SpringBootTest( - classes = JsonRpcRegistrationStylesIntegrationTest.TestApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.MOCK, - properties = { - "jsonrpc.enabled=true", - "jsonrpc.path=/jsonrpc", - "jsonrpc.scan-annotated-methods=true" - } + classes = JsonRpcRegistrationStylesIntegrationTest.TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.MOCK, + properties = { + "jsonrpc.enabled=true", + "jsonrpc.path=/jsonrpc", + "jsonrpc.scan-annotated-methods=true" + } ) class JsonRpcRegistrationStylesIntegrationTest { @@ -56,8 +55,8 @@ void setUp() { @Test void supportsAnnotationRegistrationWithClassParamAndRecordReturn() throws Exception { JsonNode body = invoke(""" - {"jsonrpc":"2.0","method":"annot.user","params":{"id":7},"id":1} - """); + {"jsonrpc":"2.0","method":"annot.user","params":{"id":7},"id":1} + """); assertEquals(7, body.get("result").get("id").asInt()); assertEquals("user-7", body.get("result").get("name").asString()); @@ -66,8 +65,8 @@ void supportsAnnotationRegistrationWithClassParamAndRecordReturn() throws Except @Test void supportsAnnotationRegistrationWithClassParamAndCollectionReturn() throws Exception { JsonNode body = invoke(""" - {"jsonrpc":"2.0","method":"annot.range","params":{"start":2,"end":4},"id":2} - """); + {"jsonrpc":"2.0","method":"annot.range","params":{"start":2,"end":4},"id":2} + """); assertTrue(body.get("result").isArray()); assertEquals(3, body.get("result").size()); @@ -78,8 +77,8 @@ void supportsAnnotationRegistrationWithClassParamAndCollectionReturn() throws Ex @Test void supportsManualJsonRpcMethodRegistration() throws Exception { JsonNode body = invoke(""" - {"jsonrpc":"2.0","method":"manual.ping","id":3} - """); + {"jsonrpc":"2.0","method":"manual.ping","id":3} + """); assertEquals("pong-manual", body.get("result").asString()); } @@ -87,8 +86,8 @@ void supportsManualJsonRpcMethodRegistration() throws Exception { @Test void supportsTypedFactoryRegistrationWithClassParamAndClassReturn() throws Exception { JsonNode body = invoke(""" - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"developer"},"id":4} - """); + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"developer"},"id":4} + """); assertEquals("DEVELOPER", body.get("result").get("result").asString()); } @@ -96,8 +95,8 @@ void supportsTypedFactoryRegistrationWithClassParamAndClassReturn() throws Excep @Test void supportsTypedFactoryNoParamsReturningCollection() throws Exception { JsonNode body = invoke(""" - {"jsonrpc":"2.0","method":"typed.tags","id":5} - """); + {"jsonrpc":"2.0","method":"typed.tags","id":5} + """); assertTrue(body.get("result").isArray()); assertEquals("alpha", body.get("result").get(0).asString()); @@ -106,10 +105,10 @@ void supportsTypedFactoryNoParamsReturningCollection() throws Exception { private JsonNode invoke(String payload) throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(payload)) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(payload)) + .andExpect(status().isOk()) + .andReturn(); return OBJECT_MAPPER.readTree(result.getResponse().getContentAsByteArray()); } @@ -117,10 +116,12 @@ private JsonNode invoke(String payload) throws Exception { @EnableAutoConfiguration @Import(TestRpcConfiguration.class) static class TestApplication { + } @Configuration(proxyBeanMethods = false) static class TestRpcConfiguration { + @Bean RegistrationStyleAnnotatedService registrationStyleAnnotatedService() { return new RegistrationStyleAnnotatedService(); @@ -134,18 +135,19 @@ JsonRpcMethodRegistration manualPingRegistration() { @Bean JsonRpcMethodRegistration typedUpperRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of("typed.upper", - factory.unary(UpperInput.class, params -> new UpperOutput( - params.value == null ? "" : params.value.toUpperCase()))); + factory.unary(UpperInput.class, params -> new UpperOutput( + params.value == null ? "" : params.value.toUpperCase()))); } @Bean JsonRpcMethodRegistration typedTagsRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of("typed.tags", - factory.noParams(() -> List.of("alpha", "beta"))); + factory.noParams(() -> List.of("alpha", "beta"))); } } static class RegistrationStyleAnnotatedService { + @JsonRpcMethod("annot.user") public UserResponse user(UserRequest request) { return new UserResponse(request.id, "user-" + request.id); @@ -162,22 +164,27 @@ public List range(RangeRequest request) { } static class UserRequest { + public int id; } record UserResponse(int id, String name) { + } static class RangeRequest { + public int start; public int end; } static class UpperInput { + public String value; } static class UpperOutput { + public String result; UpperOutput(String result) { diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfiguration.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfiguration.java index fff1bbe..aa23515 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfiguration.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfiguration.java @@ -1,7 +1,5 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; import com.limehee.jsonrpc.core.DefaultJsonRpcExceptionResolver; import com.limehee.jsonrpc.core.DefaultJsonRpcMethodInvoker; import com.limehee.jsonrpc.core.DefaultJsonRpcRequestParser; @@ -9,36 +7,42 @@ import com.limehee.jsonrpc.core.DefaultJsonRpcResponseComposer; import com.limehee.jsonrpc.core.DefaultJsonRpcResponseValidator; import com.limehee.jsonrpc.core.DefaultJsonRpcTypedMethodHandlerFactory; +import com.limehee.jsonrpc.core.DirectJsonRpcNotificationExecutor; +import com.limehee.jsonrpc.core.ExecutorJsonRpcNotificationExecutor; import com.limehee.jsonrpc.core.InMemoryJsonRpcMethodRegistry; import com.limehee.jsonrpc.core.JacksonJsonRpcParameterBinder; import com.limehee.jsonrpc.core.JacksonJsonRpcResultWriter; import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcExceptionResolver; +import com.limehee.jsonrpc.core.JsonRpcInterceptor; import com.limehee.jsonrpc.core.JsonRpcMethodInvoker; import com.limehee.jsonrpc.core.JsonRpcMethodRegistration; import com.limehee.jsonrpc.core.JsonRpcMethodRegistry; import com.limehee.jsonrpc.core.JsonRpcNotificationExecutor; import com.limehee.jsonrpc.core.JsonRpcParameterBinder; -import com.limehee.jsonrpc.core.JsonRpcInterceptor; import com.limehee.jsonrpc.core.JsonRpcRequestParser; import com.limehee.jsonrpc.core.JsonRpcRequestValidator; -import com.limehee.jsonrpc.core.JsonRpcResultWriter; import com.limehee.jsonrpc.core.JsonRpcResponseComposer; import com.limehee.jsonrpc.core.JsonRpcResponseValidationOptions; import com.limehee.jsonrpc.core.JsonRpcResponseValidator; +import com.limehee.jsonrpc.core.JsonRpcResultWriter; import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; -import com.limehee.jsonrpc.core.DirectJsonRpcNotificationExecutor; -import com.limehee.jsonrpc.core.ExecutorJsonRpcNotificationExecutor; -import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcAnnotatedMethodRegistrar; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.InstrumentedJsonRpcNotificationExecutor; +import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcAnnotatedMethodRegistrar; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcMethodAccessInterceptor; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcMetricsInterceptor; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcWebMvcMetricsObserver; -import io.micrometer.core.instrument.MeterRegistry; import com.limehee.jsonrpc.spring.webmvc.DefaultJsonRpcHttpStatusStrategy; import com.limehee.jsonrpc.spring.webmvc.JsonRpcHttpStatusStrategy; import com.limehee.jsonrpc.spring.webmvc.JsonRpcWebMvcEndpoint; import com.limehee.jsonrpc.spring.webmvc.JsonRpcWebMvcObserver; +import io.micrometer.core.instrument.MeterRegistry; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -49,18 +53,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Executor; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; /** * Spring Boot auto-configuration for JSON-RPC server components. *

- * This configuration wires core dispatcher components, optional metrics integration, annotation - * scanning, transport endpoint registration, and basic property validation. + * This configuration wires core dispatcher components, optional metrics integration, annotation scanning, transport + * endpoint registration, and basic property validation. *

*/ @AutoConfiguration @@ -110,11 +110,11 @@ public JsonRpcRequestValidator jsonRpcRequestValidator(JsonRpcProperties propert } if (request.getParamsTypeViolationCodePolicy() == null) { throw new IllegalArgumentException( - "jsonrpc.validation.request.params-type-violation-code-policy must not be null" + "jsonrpc.validation.request.params-type-violation-code-policy must not be null" ); } return new DefaultJsonRpcRequestValidator( - request.getParamsTypeViolationCodePolicy() + request.getParamsTypeViolationCodePolicy() ); } @@ -136,18 +136,18 @@ public JsonRpcResponseValidationOptions jsonRpcResponseValidationOptions(JsonRpc throw new IllegalArgumentException("jsonrpc.validation.response must not be null"); } return JsonRpcResponseValidationOptions.builder() - .requireJsonRpcVersion20(response.isRequireJsonRpcVersion20()) - .requireResponseIdMember(response.isRequireResponseIdMember()) - .allowNullResponseId(response.isAllowNullResponseId()) - .allowStringResponseId(response.isAllowStringResponseId()) - .allowNumericResponseId(response.isAllowNumericResponseId()) - .allowFractionalResponseId(response.isAllowFractionalResponseId()) - .requireExclusiveResultOrError(response.isRequireExclusiveResultOrError()) - .requireErrorObjectWhenPresent(response.isRequireErrorObjectWhenPresent()) - .requireIntegerErrorCode(response.isRequireIntegerErrorCode()) - .requireStringErrorMessage(response.isRequireStringErrorMessage()) - .allowRequestFieldsInResponse(response.isAllowRequestFieldsInResponse()) - .build(); + .requireJsonRpcVersion20(response.isRequireJsonRpcVersion20()) + .requireResponseIdMember(response.isRequireResponseIdMember()) + .allowNullResponseId(response.isAllowNullResponseId()) + .allowStringResponseId(response.isAllowStringResponseId()) + .allowNumericResponseId(response.isAllowNumericResponseId()) + .allowFractionalResponseId(response.isAllowFractionalResponseId()) + .requireExclusiveResultOrError(response.isRequireExclusiveResultOrError()) + .requireErrorObjectWhenPresent(response.isRequireErrorObjectWhenPresent()) + .requireIntegerErrorCode(response.isRequireIntegerErrorCode()) + .requireStringErrorMessage(response.isRequireStringErrorMessage()) + .allowRequestFieldsInResponse(response.isAllowRequestFieldsInResponse()) + .build(); } /** @@ -160,7 +160,7 @@ public JsonRpcResponseValidationOptions jsonRpcResponseValidationOptions(JsonRpc @ConditionalOnMissingBean public JsonRpcResponseValidator jsonRpcResponseValidator(JsonRpcResponseValidationOptions options) { return new DefaultJsonRpcResponseValidator( - options + options ); } @@ -207,7 +207,8 @@ public JsonRpcResponseComposer jsonRpcResponseComposer() { @Bean @ConditionalOnMissingBean public JsonRpcParameterBinder jsonRpcParameterBinder(ObjectProvider objectMapperProvider) { - return new JacksonJsonRpcParameterBinder(objectMapperProvider.getIfAvailable(() -> JsonMapper.builder().build())); + return new JacksonJsonRpcParameterBinder( + objectMapperProvider.getIfAvailable(() -> JsonMapper.builder().build())); } /** @@ -226,14 +227,14 @@ public JsonRpcResultWriter jsonRpcResultWriter(ObjectProvider obje * Creates typed method handler factory used by typed registration utilities. * * @param parameterBinder binder used for parameter conversion - * @param resultWriter writer used for serializing handler results + * @param resultWriter writer used for serializing handler results * @return typed method handler factory */ @Bean @ConditionalOnMissingBean public JsonRpcTypedMethodHandlerFactory jsonRpcTypedMethodHandlerFactory( - JsonRpcParameterBinder parameterBinder, - JsonRpcResultWriter resultWriter + JsonRpcParameterBinder parameterBinder, + JsonRpcResultWriter resultWriter ) { return new DefaultJsonRpcTypedMethodHandlerFactory(parameterBinder, resultWriter); } @@ -241,8 +242,8 @@ public JsonRpcTypedMethodHandlerFactory jsonRpcTypedMethodHandlerFactory( /** * Creates notification executor according to configuration and available executor beans. * - * @param properties bound JSON-RPC properties - * @param beanFactory bean factory used to discover candidate executors + * @param properties bound JSON-RPC properties + * @param beanFactory bean factory used to discover candidate executors * @param meterRegistryProvider optional meter registry for executor instrumentation * @return notification executor implementation * @throws IllegalStateException if a configured executor bean name does not exist @@ -250,9 +251,9 @@ public JsonRpcTypedMethodHandlerFactory jsonRpcTypedMethodHandlerFactory( @Bean @ConditionalOnMissingBean public JsonRpcNotificationExecutor jsonRpcNotificationExecutor( - JsonRpcProperties properties, - ListableBeanFactory beanFactory, - ObjectProvider meterRegistryProvider + JsonRpcProperties properties, + ListableBeanFactory beanFactory, + ObjectProvider meterRegistryProvider ) { JsonRpcNotificationExecutor executor; if (!properties.isNotificationExecutorEnabled()) { @@ -266,7 +267,7 @@ public JsonRpcNotificationExecutor jsonRpcNotificationExecutor( Executor configuredExecutor = executors.get(configuredBeanName); if (configuredExecutor == null) { throw new IllegalStateException( - "jsonrpc.notification-executor-bean-name points to missing Executor bean: " + configuredBeanName); + "jsonrpc.notification-executor-bean-name points to missing Executor bean: " + configuredBeanName); } executor = new ExecutorJsonRpcNotificationExecutor(configuredExecutor); return instrumentNotificationExecutorIfEnabled(executor, properties, meterRegistryProvider); @@ -296,15 +297,15 @@ public JsonRpcNotificationExecutor jsonRpcNotificationExecutor( @ConditionalOnMissingBean(name = "jsonRpcMethodAccessInterceptor") public JsonRpcInterceptor jsonRpcMethodAccessInterceptor(JsonRpcProperties properties) { return new JsonRpcMethodAccessInterceptor( - normalizeMethodSet(properties.getMethodAllowlist()), - normalizeMethodSet(properties.getMethodDenylist()) + normalizeMethodSet(properties.getMethodAllowlist()), + normalizeMethodSet(properties.getMethodDenylist()) ); } /** * Creates Micrometer metrics interceptor for dispatcher lifecycle metrics. * - * @param properties bound JSON-RPC properties + * @param properties bound JSON-RPC properties * @param meterRegistry meter registry used for metric publication * @return metrics interceptor */ @@ -315,17 +316,17 @@ public JsonRpcInterceptor jsonRpcMethodAccessInterceptor(JsonRpcProperties prope @ConditionalOnProperty(prefix = "jsonrpc", name = "metrics-enabled", havingValue = "true", matchIfMissing = true) public JsonRpcInterceptor jsonRpcMetricsInterceptor(JsonRpcProperties properties, MeterRegistry meterRegistry) { return new JsonRpcMetricsInterceptor( - meterRegistry, - properties.isMetricsLatencyHistogramEnabled(), - toPercentileArray(properties.getMetricsLatencyPercentiles()), - properties.getMetricsMaxMethodTagValues() + meterRegistry, + properties.isMetricsLatencyHistogramEnabled(), + toPercentileArray(properties.getMetricsLatencyPercentiles()), + properties.getMetricsMaxMethodTagValues() ); } /** * Creates transport metrics observer for WebMVC-specific events. * - * @param properties bound JSON-RPC properties + * @param properties bound JSON-RPC properties * @param meterRegistry meter registry used for metric publication * @return WebMVC metrics observer */ @@ -334,11 +335,12 @@ public JsonRpcInterceptor jsonRpcMetricsInterceptor(JsonRpcProperties properties @ConditionalOnBean(MeterRegistry.class) @ConditionalOnMissingBean(JsonRpcWebMvcObserver.class) @ConditionalOnProperty(prefix = "jsonrpc", name = "metrics-enabled", havingValue = "true", matchIfMissing = true) - public JsonRpcWebMvcObserver jsonRpcWebMvcMetricsObserver(JsonRpcProperties properties, MeterRegistry meterRegistry) { + public JsonRpcWebMvcObserver jsonRpcWebMvcMetricsObserver(JsonRpcProperties properties, + MeterRegistry meterRegistry) { return new JsonRpcWebMvcMetricsObserver( - meterRegistry, - properties.isMetricsLatencyHistogramEnabled(), - toPercentileArray(properties.getMetricsLatencyPercentiles()) + meterRegistry, + properties.isMetricsLatencyHistogramEnabled(), + toPercentileArray(properties.getMetricsLatencyPercentiles()) ); } @@ -354,77 +356,77 @@ public JsonRpcWebMvcObserver jsonRpcWebMvcObserver() { } /** - * Creates registrar that scans and registers {@link com.limehee.jsonrpc.core.JsonRpcMethod} - * annotated Spring bean methods. + * Creates registrar that scans and registers {@link com.limehee.jsonrpc.core.JsonRpcMethod} annotated Spring bean + * methods. * - * @param beanFactory Spring bean factory - * @param dispatcher dispatcher receiving discovered registrations + * @param beanFactory Spring bean factory + * @param dispatcher dispatcher receiving discovered registrations * @param typedMethodHandlerFactory typed handler factory - * @param parameterBinder parameter binder - * @param resultWriter result writer + * @param parameterBinder parameter binder + * @param resultWriter result writer * @return annotated method registrar */ @Bean @ConditionalOnProperty(prefix = "jsonrpc", name = "scan-annotated-methods", havingValue = "true", matchIfMissing = true) public JsonRpcAnnotatedMethodRegistrar jsonRpcAnnotatedMethodRegistrar( - ListableBeanFactory beanFactory, - JsonRpcDispatcher dispatcher, - JsonRpcTypedMethodHandlerFactory typedMethodHandlerFactory, - JsonRpcParameterBinder parameterBinder, - JsonRpcResultWriter resultWriter + ListableBeanFactory beanFactory, + JsonRpcDispatcher dispatcher, + JsonRpcTypedMethodHandlerFactory typedMethodHandlerFactory, + JsonRpcParameterBinder parameterBinder, + JsonRpcResultWriter resultWriter ) { return new JsonRpcAnnotatedMethodRegistrar( - beanFactory, - dispatcher, - typedMethodHandlerFactory, - parameterBinder, - resultWriter + beanFactory, + dispatcher, + typedMethodHandlerFactory, + parameterBinder, + resultWriter ); } /** * Creates the JSON-RPC dispatcher and applies additional method registrations. * - * @param methodRegistry method registry - * @param requestParser request parser - * @param requestValidator request validator - * @param methodInvoker method invoker - * @param exceptionResolver exception resolver - * @param responseComposer response composer + * @param methodRegistry method registry + * @param requestParser request parser + * @param requestValidator request validator + * @param methodInvoker method invoker + * @param exceptionResolver exception resolver + * @param responseComposer response composer * @param notificationExecutor notification executor - * @param properties bound JSON-RPC properties - * @param registrations additional static method registrations - * @param interceptors dispatcher interceptors + * @param properties bound JSON-RPC properties + * @param registrations additional static method registrations + * @param interceptors dispatcher interceptors * @return configured dispatcher instance */ @Bean @ConditionalOnMissingBean public JsonRpcDispatcher jsonRpcDispatcher( - JsonRpcMethodRegistry methodRegistry, - JsonRpcRequestParser requestParser, - JsonRpcRequestValidator requestValidator, - JsonRpcMethodInvoker methodInvoker, - JsonRpcExceptionResolver exceptionResolver, - JsonRpcResponseComposer responseComposer, - JsonRpcNotificationExecutor notificationExecutor, - JsonRpcProperties properties, - ObjectProvider registrations, - ObjectProvider interceptors + JsonRpcMethodRegistry methodRegistry, + JsonRpcRequestParser requestParser, + JsonRpcRequestValidator requestValidator, + JsonRpcMethodInvoker methodInvoker, + JsonRpcExceptionResolver exceptionResolver, + JsonRpcResponseComposer responseComposer, + JsonRpcNotificationExecutor notificationExecutor, + JsonRpcProperties properties, + ObjectProvider registrations, + ObjectProvider interceptors ) { validateProperties(properties); JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - methodRegistry, - requestParser, - requestValidator, - methodInvoker, - exceptionResolver, - responseComposer, - properties.getMaxBatchSize(), - interceptors.orderedStream().toList(), - notificationExecutor + methodRegistry, + requestParser, + requestValidator, + methodInvoker, + exceptionResolver, + responseComposer, + properties.getMaxBatchSize(), + interceptors.orderedStream().toList(), + notificationExecutor ); registrations.orderedStream().forEach(registration -> - dispatcher.register(registration.method(), registration.handler())); + dispatcher.register(registration.method(), registration.handler())); return dispatcher; } @@ -442,11 +444,11 @@ public JsonRpcHttpStatusStrategy jsonRpcHttpStatusStrategy() { /** * Creates JSON-RPC WebMVC endpoint for servlet applications. * - * @param dispatcher dispatcher handling JSON-RPC requests - * @param httpStatusStrategy strategy mapping protocol outcomes to HTTP status codes + * @param dispatcher dispatcher handling JSON-RPC requests + * @param httpStatusStrategy strategy mapping protocol outcomes to HTTP status codes * @param objectMapperProvider provider for custom or default {@link ObjectMapper} - * @param webMvcObserver observer for transport-level events - * @param properties bound JSON-RPC properties + * @param webMvcObserver observer for transport-level events + * @param properties bound JSON-RPC properties * @return WebMVC endpoint bean */ @Bean @@ -455,19 +457,19 @@ public JsonRpcHttpStatusStrategy jsonRpcHttpStatusStrategy() { @ConditionalOnClass(JsonRpcWebMvcEndpoint.class) @ConditionalOnProperty(prefix = "jsonrpc", name = "enabled", havingValue = "true", matchIfMissing = true) public JsonRpcWebMvcEndpoint jsonRpcWebMvcEndpoint( - JsonRpcDispatcher dispatcher, - JsonRpcHttpStatusStrategy httpStatusStrategy, - ObjectProvider objectMapperProvider, - JsonRpcWebMvcObserver webMvcObserver, - JsonRpcProperties properties + JsonRpcDispatcher dispatcher, + JsonRpcHttpStatusStrategy httpStatusStrategy, + ObjectProvider objectMapperProvider, + JsonRpcWebMvcObserver webMvcObserver, + JsonRpcProperties properties ) { ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(() -> JsonMapper.builder().build()); return new JsonRpcWebMvcEndpoint( - dispatcher, - objectMapper, - httpStatusStrategy, - properties.getMaxRequestBytes(), - webMvcObserver + dispatcher, + objectMapper, + httpStatusStrategy, + properties.getMaxRequestBytes(), + webMvcObserver ); } @@ -530,7 +532,7 @@ private void validateProperties(JsonRpcProperties properties) { } if (properties.getValidation().getRequest().getParamsTypeViolationCodePolicy() == null) { throw new IllegalArgumentException( - "jsonrpc.validation.request.params-type-violation-code-policy must not be null" + "jsonrpc.validation.request.params-type-violation-code-policy must not be null" ); } if (properties.getValidation().getResponse() == null) { @@ -561,7 +563,7 @@ private boolean containsWhitespace(String value) { * Validates method allow/deny list entries. * * @param propertyName validated property name for exception messages - * @param methods method names to validate + * @param methods method names to validate * @throws IllegalArgumentException if list is null or contains blank entries */ private void validateMethodList(String propertyName, List methods) { @@ -581,7 +583,7 @@ private void validateMethodList(String propertyName, List methods) { * @param value raw value * @return trimmed value or {@code null} */ - private String trimToNull(String value) { + private @Nullable String trimToNull(@Nullable String value) { if (value == null) { return null; } @@ -592,15 +594,15 @@ private String trimToNull(String value) { /** * Wraps notification executor with metrics instrumentation when enabled and available. * - * @param delegate delegate notification executor - * @param properties bound JSON-RPC properties + * @param delegate delegate notification executor + * @param properties bound JSON-RPC properties * @param meterRegistryProvider provider for optional meter registry * @return instrumented executor when possible, otherwise original delegate */ private JsonRpcNotificationExecutor instrumentNotificationExecutorIfEnabled( - JsonRpcNotificationExecutor delegate, - JsonRpcProperties properties, - ObjectProvider meterRegistryProvider + JsonRpcNotificationExecutor delegate, + JsonRpcProperties properties, + ObjectProvider meterRegistryProvider ) { if (!properties.isMetricsEnabled()) { return delegate; @@ -610,10 +612,10 @@ private JsonRpcNotificationExecutor instrumentNotificationExecutorIfEnabled( return delegate; } return new InstrumentedJsonRpcNotificationExecutor( - delegate, - meterRegistry, - properties.isMetricsLatencyHistogramEnabled(), - toPercentileArray(properties.getMetricsLatencyPercentiles()) + delegate, + meterRegistry, + properties.isMetricsLatencyHistogramEnabled(), + toPercentileArray(properties.getMetricsLatencyPercentiles()) ); } @@ -630,7 +632,7 @@ private void validatePercentiles(List percentiles) { for (Double percentile : percentiles) { if (percentile == null || percentile <= 0.0 || percentile >= 1.0) { throw new IllegalArgumentException( - "jsonrpc.metrics-latency-percentiles values must be greater than 0.0 and less than 1.0"); + "jsonrpc.metrics-latency-percentiles values must be greater than 0.0 and less than 1.0"); } } } @@ -651,7 +653,7 @@ private double[] toPercentileArray(List percentiles) { Double percentile = percentiles.get(i); if (percentile == null || percentile <= 0.0 || percentile >= 1.0) { throw new IllegalArgumentException( - "jsonrpc.metrics-latency-percentiles values must be greater than 0.0 and less than 1.0"); + "jsonrpc.metrics-latency-percentiles values must be greater than 0.0 and less than 1.0"); } values[i] = percentile; } diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcProperties.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcProperties.java index 3093960..725e568 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcProperties.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcProperties.java @@ -1,12 +1,11 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import com.limehee.jsonrpc.core.JsonRpcParamsTypeViolationCodePolicy; import com.limehee.jsonrpc.core.JsonRpcMethodRegistrationConflictPolicy; -import org.springframework.boot.context.properties.ConfigurationProperties; - +import com.limehee.jsonrpc.core.JsonRpcParamsTypeViolationCodePolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import org.springframework.boot.context.properties.ConfigurationProperties; /** * Externalized Spring Boot configuration properties for JSON-RPC auto-configuration. @@ -107,8 +106,8 @@ public void setMaxRequestBytes(int maxRequestBytes) { } /** - * Indicates whether {@link com.limehee.jsonrpc.core.JsonRpcMethod}-annotated methods are - * scanned and auto-registered. + * Indicates whether {@link com.limehee.jsonrpc.core.JsonRpcMethod}-annotated methods are scanned and + * auto-registered. * * @return {@code true} when annotation scanning is enabled */ @@ -191,8 +190,8 @@ public List getMetricsLatencyPercentiles() { /** * Sets configured latency percentiles to publish for supported metrics. * - * @param metricsLatencyPercentiles percentile list; each value must be greater than - * {@code 0.0} and less than {@code 1.0} + * @param metricsLatencyPercentiles percentile list; each value must be greater than {@code 0.0} and less than + * {@code 1.0} */ public void setMetricsLatencyPercentiles(List metricsLatencyPercentiles) { this.metricsLatencyPercentiles = metricsLatencyPercentiles; @@ -219,8 +218,8 @@ public void setMetricsMaxMethodTagValues(int metricsMaxMethodTagValues) { /** * Indicates whether notification handling should prefer an executor-backed path. *

- * When enabled, auto-configuration attempts to choose a Spring {@link java.util.concurrent.Executor}. - * If no suitable executor is resolved, handling falls back to direct execution. + * When enabled, auto-configuration attempts to choose a Spring {@link java.util.concurrent.Executor}. If no + * suitable executor is resolved, handling falls back to direct execution. *

* * @return {@code true} when executor-backed notification handling is enabled @@ -239,8 +238,7 @@ public void setNotificationExecutorEnabled(boolean notificationExecutorEnabled) } /** - * Returns the preferred Spring {@link java.util.concurrent.Executor} bean name for - * notification execution. + * Returns the preferred Spring {@link java.util.concurrent.Executor} bean name for notification execution. * * @return executor bean name, or empty when auto-selection should be used */ @@ -249,8 +247,7 @@ public String getNotificationExecutorBeanName() { } /** - * Sets the preferred Spring {@link java.util.concurrent.Executor} bean name for - * notification execution. + * Sets the preferred Spring {@link java.util.concurrent.Executor} bean name for notification execution. * * @param notificationExecutorBeanName preferred executor bean name, or empty for auto-selection */ @@ -273,7 +270,7 @@ public JsonRpcMethodRegistrationConflictPolicy getMethodRegistrationConflictPoli * @param methodRegistrationConflictPolicy conflict policy used by method registry */ public void setMethodRegistrationConflictPolicy( - JsonRpcMethodRegistrationConflictPolicy methodRegistrationConflictPolicy + JsonRpcMethodRegistrationConflictPolicy methodRegistrationConflictPolicy ) { this.methodRegistrationConflictPolicy = methodRegistrationConflictPolicy; } @@ -382,11 +379,11 @@ public void setResponse(Response response) { public static final class Request { private JsonRpcParamsTypeViolationCodePolicy paramsTypeViolationCodePolicy = - JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS; + JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS; /** - * Returns the error-code mapping policy used when request {@code params} exists but is - * neither an object nor an array. + * Returns the error-code mapping policy used when request {@code params} exists but is neither an object + * nor an array. * * @return params-type violation error-code policy */ @@ -395,17 +392,17 @@ public JsonRpcParamsTypeViolationCodePolicy getParamsTypeViolationCodePolicy() { } /** - * Sets the error-code mapping policy used when request {@code params} exists but is - * neither an object nor an array. + * Sets the error-code mapping policy used when request {@code params} exists but is neither an object nor + * an array. * * @param paramsTypeViolationCodePolicy params-type violation error-code policy */ public void setParamsTypeViolationCodePolicy( - JsonRpcParamsTypeViolationCodePolicy paramsTypeViolationCodePolicy + JsonRpcParamsTypeViolationCodePolicy paramsTypeViolationCodePolicy ) { this.paramsTypeViolationCodePolicy = Objects.requireNonNull( - paramsTypeViolationCodePolicy, - "paramsTypeViolationCodePolicy" + paramsTypeViolationCodePolicy, + "paramsTypeViolationCodePolicy" ); } } @@ -608,8 +605,8 @@ public void setRequireStringErrorMessage(boolean requireStringErrorMessage) { } /** - * Indicates whether request-only fields like {@code method}/{@code params} are - * allowed in response objects. + * Indicates whether request-only fields like {@code method}/{@code params} are allowed in response + * objects. * * @return {@code true} when request fields are tolerated in responses */ @@ -618,8 +615,7 @@ public boolean isAllowRequestFieldsInResponse() { } /** - * Sets whether request-only fields like {@code method}/{@code params} are allowed in - * response objects. + * Sets whether request-only fields like {@code method}/{@code params} are allowed in response objects. * * @param allowRequestFieldsInResponse {@code true} to allow request fields in response */ diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/InstrumentedJsonRpcNotificationExecutor.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/InstrumentedJsonRpcNotificationExecutor.java index 3edbbfe..6377ae5 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/InstrumentedJsonRpcNotificationExecutor.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/InstrumentedJsonRpcNotificationExecutor.java @@ -4,7 +4,7 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; - +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -35,32 +35,34 @@ public final class InstrumentedJsonRpcNotificationExecutor implements JsonRpcNot /** * Creates an instrumented notification executor. * - * @param delegate delegate executor that performs actual task scheduling and execution - * @param meterRegistry registry where notification metrics are emitted + * @param delegate delegate executor that performs actual task scheduling and execution + * @param meterRegistry registry where notification metrics are emitted * @param latencyHistogramEnabled whether percentile histograms are enabled for timers - * @param latencyPercentiles configured percentiles for queue/execution timers + * @param latencyPercentiles configured percentiles for queue/execution timers */ public InstrumentedJsonRpcNotificationExecutor( - JsonRpcNotificationExecutor delegate, - MeterRegistry meterRegistry, - boolean latencyHistogramEnabled, - double[] latencyPercentiles + JsonRpcNotificationExecutor delegate, + MeterRegistry meterRegistry, + boolean latencyHistogramEnabled, + double[] latencyPercentiles ) { - this.delegate = delegate; + this.delegate = Objects.requireNonNull(delegate, "delegate"); + MeterRegistry targetRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry"); + double[] configuredPercentiles = Objects.requireNonNull(latencyPercentiles, "latencyPercentiles"); this.queueDelayTimer = createTimer( - meterRegistry, - QUEUE_DELAY_METRIC, - latencyHistogramEnabled, - latencyPercentiles + targetRegistry, + QUEUE_DELAY_METRIC, + latencyHistogramEnabled, + configuredPercentiles ); this.executionTimer = createTimer( - meterRegistry, - EXECUTION_METRIC, - latencyHistogramEnabled, - latencyPercentiles + targetRegistry, + EXECUTION_METRIC, + latencyHistogramEnabled, + configuredPercentiles ); - this.submittedCounter = meterRegistry.counter(SUBMITTED_METRIC); - this.failedCounter = meterRegistry.counter(FAILED_METRIC); + this.submittedCounter = targetRegistry.counter(SUBMITTED_METRIC); + this.failedCounter = targetRegistry.counter(FAILED_METRIC); } /** @@ -89,23 +91,23 @@ public void execute(Runnable task) { /** * Creates a timer using shared histogram/percentile settings. * - * @param meterRegistry registry where the timer is registered - * @param name metric name + * @param meterRegistry registry where the timer is registered + * @param name metric name * @param latencyHistogramEnabled whether histogram publication is enabled - * @param latencyPercentiles percentile configuration to publish + * @param latencyPercentiles percentile configuration to publish * @return registered timer instance */ private Timer createTimer( - MeterRegistry meterRegistry, - String name, - boolean latencyHistogramEnabled, - double[] latencyPercentiles + MeterRegistry meterRegistry, + String name, + boolean latencyHistogramEnabled, + double[] latencyPercentiles ) { Timer.Builder builder = Timer.builder(name); if (latencyHistogramEnabled) { builder.publishPercentileHistogram(); } - if (latencyPercentiles != null && latencyPercentiles.length > 0) { + if (latencyPercentiles.length > 0) { builder.publishPercentiles(latencyPercentiles); } return builder.register(meterRegistry); diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcAnnotatedMethodRegistrar.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcAnnotatedMethodRegistrar.java index 7fa69b5..c391cbb 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcAnnotatedMethodRegistrar.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcAnnotatedMethodRegistrar.java @@ -1,30 +1,30 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure.support; -import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcConstants; +import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcException; import com.limehee.jsonrpc.core.JsonRpcMethod; import com.limehee.jsonrpc.core.JsonRpcMethodHandler; -import com.limehee.jsonrpc.core.JsonRpcParameterBinder; import com.limehee.jsonrpc.core.JsonRpcParam; +import com.limehee.jsonrpc.core.JsonRpcParameterBinder; import com.limehee.jsonrpc.core.JsonRpcResultWriter; import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.util.ClassUtils; -import tools.jackson.databind.JsonNode; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.util.ClassUtils; +import tools.jackson.databind.JsonNode; /** - * Registers methods annotated with {@link JsonRpcMethod} into the dispatcher after Spring - * singleton initialization is complete. + * Registers methods annotated with {@link JsonRpcMethod} into the dispatcher after Spring singleton initialization is + * complete. *

* Resolution rules: *

@@ -50,18 +50,18 @@ public final class JsonRpcAnnotatedMethodRegistrar implements SmartInitializingS /** * Creates a registrar that scans beans and wires annotated methods into the dispatcher. * - * @param beanFactory bean factory used to enumerate and resolve candidate beans - * @param dispatcher dispatcher where resolved methods are registered + * @param beanFactory bean factory used to enumerate and resolve candidate beans + * @param dispatcher dispatcher where resolved methods are registered * @param typedMethodHandlerFactory factory used for no-arg and unary handler creation - * @param parameterBinder binder used for parameter conversion from JSON values - * @param resultWriter writer used to serialize Java results into JSON nodes + * @param parameterBinder binder used for parameter conversion from JSON values + * @param resultWriter writer used to serialize Java results into JSON nodes */ public JsonRpcAnnotatedMethodRegistrar( - ListableBeanFactory beanFactory, - JsonRpcDispatcher dispatcher, - JsonRpcTypedMethodHandlerFactory typedMethodHandlerFactory, - JsonRpcParameterBinder parameterBinder, - JsonRpcResultWriter resultWriter + ListableBeanFactory beanFactory, + JsonRpcDispatcher dispatcher, + JsonRpcTypedMethodHandlerFactory typedMethodHandlerFactory, + JsonRpcParameterBinder parameterBinder, + JsonRpcResultWriter resultWriter ) { this.beanFactory = beanFactory; this.dispatcher = dispatcher; @@ -93,7 +93,8 @@ public void afterSingletonsInstantiated() { try { bean = beanFactory.getBean(beanName); } catch (Exception ex) { - throw new IllegalStateException("Failed to initialize bean for @JsonRpcMethod scanning: " + beanName, ex); + throw new IllegalStateException("Failed to initialize bean for @JsonRpcMethod scanning: " + beanName, + ex); } for (Method method : annotatedMethods) { @@ -126,7 +127,7 @@ private List findAnnotatedMethods(Class beanType) { /** * Builds a method handler based on target method signature shape. * - * @param bean bean instance declaring the method + * @param bean bean instance declaring the method * @param method method to expose as JSON-RPC handler * @return handler that performs binding, invocation, and result serialization */ @@ -149,12 +150,12 @@ private JsonRpcMethodHandler buildHandler(Object bean, Method method) { /** * Invokes the target method with prepared arguments. * - * @param bean target bean + * @param bean target bean * @param method method to invoke - * @param args invocation arguments + * @param args invocation arguments * @return invocation result * @throws IllegalStateException when reflection access is unexpectedly denied - * @throws RuntimeException wrapping non-runtime target exceptions + * @throws RuntimeException wrapping non-runtime target exceptions */ private Object invoke(Object bean, Method method, Object... args) { try { @@ -174,9 +175,9 @@ private Object invoke(Object bean, Method method, Object... args) { * Creates a unary handler that binds one parameter and invokes the target method. * * @param paramType method parameter type - * @param bean target bean - * @param method target method - * @param static parameter type + * @param bean target bean + * @param method target method + * @param static parameter type * @return unary JSON-RPC method handler */ @SuppressWarnings("unchecked") @@ -191,7 +192,7 @@ private JsonRpcMethodHandler unaryHandler(Class paramType, Object bean, M * @param params JSON-RPC {@code params} value * @return bound method arguments */ - private Object[] bindMethodParams(Method method, JsonNode params) { + private Object[] bindMethodParams(Method method, @Nullable JsonNode params) { if (params != null && params.isObject()) { return bindNamedParams(method, params); } @@ -206,7 +207,7 @@ private Object[] bindMethodParams(Method method, JsonNode params) { * @return bound arguments * @throws JsonRpcException when params are missing, not an array, or size does not match */ - private Object[] bindPositionalParams(Method method, JsonNode params) { + private Object[] bindPositionalParams(Method method, @Nullable JsonNode params) { Class[] parameterTypes = method.getParameterTypes(); if (params == null || !params.isArray() || params.size() != parameterTypes.length) { throw invalidParamsException(); @@ -288,7 +289,7 @@ private Method resolveInvocableMethod(Class beanClass, Method candidate) { /** * Makes the method accessible for reflective invocation when necessary. * - * @param bean bean instance used for accessibility checks + * @param bean bean instance used for accessibility checks * @param method method to make invocable */ private void makeInvocable(Object bean, Method method) { diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessDeniedException.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessDeniedException.java index 6ef504f..1c6028b 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessDeniedException.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessDeniedException.java @@ -7,8 +7,8 @@ /** * Exception raised when a JSON-RPC method is rejected by allowlist/denylist rules. *

- * The exception intentionally maps to {@link JsonRpcErrorCode#METHOD_NOT_FOUND} so callers do - * not learn whether the method exists but is blocked by policy. + * The exception intentionally maps to {@link JsonRpcErrorCode#METHOD_NOT_FOUND} so callers do not learn whether the + * method exists but is blocked by policy. *

*/ public final class JsonRpcMethodAccessDeniedException extends JsonRpcException { diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessInterceptor.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessInterceptor.java index 1a11422..7700bcd 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessInterceptor.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMethodAccessInterceptor.java @@ -2,9 +2,8 @@ import com.limehee.jsonrpc.core.JsonRpcInterceptor; import com.limehee.jsonrpc.core.JsonRpcRequest; -import org.springframework.core.Ordered; - import java.util.Set; +import org.springframework.core.Ordered; /** * Interceptor that enforces method-level access control using allowlist and denylist sets. @@ -29,7 +28,7 @@ public final class JsonRpcMethodAccessInterceptor implements JsonRpcInterceptor, * Creates a new method access interceptor. * * @param allowlist methods that are allowed when the set is non-empty - * @param denylist methods that are always denied + * @param denylist methods that are always denied */ public JsonRpcMethodAccessInterceptor(Set allowlist, Set denylist) { this.allowlist = allowlist; diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMetricsInterceptor.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMetricsInterceptor.java index b4fed4d..1625343 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMetricsInterceptor.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcMetricsInterceptor.java @@ -1,18 +1,19 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure.support; -import tools.jackson.databind.JsonNode; -import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcError; +import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcInterceptor; import com.limehee.jsonrpc.core.JsonRpcInterceptorExecutionException; import com.limehee.jsonrpc.core.JsonRpcRequest; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; - +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; /** * Micrometer-backed interceptor that records server-side JSON-RPC execution metrics. @@ -62,21 +63,21 @@ public JsonRpcMetricsInterceptor(MeterRegistry meterRegistry) { /** * Creates an interceptor with explicit metric options. * - * @param meterRegistry registry where JSON-RPC metrics are published + * @param meterRegistry registry where JSON-RPC metrics are published * @param latencyHistogramEnabled whether latency histogram buckets should be emitted - * @param latencyPercentiles latency percentiles to publish for timers - * @param maxMethodTagValues maximum number of distinct method tag values before collapsing to - * {@code other}; values less than or equal to zero disable collapsing + * @param latencyPercentiles latency percentiles to publish for timers + * @param maxMethodTagValues maximum number of distinct method tag values before collapsing to {@code other}; + * values less than or equal to zero disable collapsing */ public JsonRpcMetricsInterceptor( - MeterRegistry meterRegistry, - boolean latencyHistogramEnabled, - double[] latencyPercentiles, - int maxMethodTagValues + MeterRegistry meterRegistry, + boolean latencyHistogramEnabled, + double[] latencyPercentiles, + int maxMethodTagValues ) { - this.meterRegistry = meterRegistry; + this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry"); this.latencyHistogramEnabled = latencyHistogramEnabled; - this.latencyPercentiles = latencyPercentiles == null ? new double[0] : latencyPercentiles.clone(); + this.latencyPercentiles = Objects.requireNonNull(latencyPercentiles, "latencyPercentiles").clone(); this.maxMethodTagValues = maxMethodTagValues; } @@ -94,11 +95,11 @@ public void beforeInvoke(JsonRpcRequest request) { * Records success counters and invocation latency after successful method execution. * * @param request JSON-RPC request that completed successfully - * @param result JSON result produced by the method handler + * @param result JSON result produced by the method handler */ @Override public void afterInvoke(JsonRpcRequest request, JsonNode result) { - String method = normalizeMethodName(request == null ? null : request.method()); + String method = normalizeMethodName(request.method()); recordCallAndLatency(method, "success", "none"); counter(stageCounters, STAGE_EVENTS_METRIC, method, "invoke_success", "").increment(); } @@ -106,28 +107,29 @@ public void afterInvoke(JsonRpcRequest request, JsonNode result) { /** * Records error counters, latency, stage, and failure-source dimensions. * - * @param request request being processed when the error occurred - * @param throwable original throwable associated with the failure + * @param request request being processed when the error occurred + * @param throwable original throwable associated with the failure * @param mappedError protocol-level error mapped for the response */ @Override - public void onError(JsonRpcRequest request, Throwable throwable, JsonRpcError mappedError) { + public void onError(@Nullable JsonRpcRequest request, Throwable throwable, JsonRpcError mappedError) { + JsonRpcError error = Objects.requireNonNull(mappedError, "mappedError"); String method = normalizeMethodName(request == null ? null : request.method()); - String errorCode = mappedError == null ? "unknown" : String.valueOf(mappedError.code()); + String errorCode = String.valueOf(error.code()); recordCallAndLatency(method, "error", errorCode); - String stage = classifyStage(mappedError); + String stage = classifyStage(error); counter(stageCounters, STAGE_EVENTS_METRIC, method, stage, "").increment(); - String source = classifyFailureSource(throwable, mappedError); + String source = classifyFailureSource(throwable, error); counter(failureCounters, FAILURE_METRIC, method, errorCode, source).increment(); } /** * Records call counter and elapsed time from {@link #beforeInvoke(JsonRpcRequest)}. * - * @param method normalized method tag value - * @param outcome request outcome tag value + * @param method normalized method tag value + * @param outcome request outcome tag value * @param errorCode JSON-RPC error code tag value or semantic placeholder */ private void recordCallAndLatency(String method, String outcome, String errorCode) { @@ -144,7 +146,7 @@ private void recordCallAndLatency(String method, String outcome, String errorCod /** * Resolves or creates a latency timer for the given method/outcome pair. * - * @param method normalized method tag value + * @param method normalized method tag value * @param outcome outcome tag value * @return cached or newly registered timer */ @@ -152,8 +154,8 @@ private Timer latencyTimer(String method, String outcome) { LatencyKey key = new LatencyKey(method, outcome); return latencyTimers.computeIfAbsent(key, ignored -> { Timer.Builder builder = Timer.builder(LATENCY_METRIC) - .tag("method", method) - .tag("outcome", outcome); + .tag("method", method) + .tag("outcome", outcome); if (latencyHistogramEnabled) { builder.publishPercentileHistogram(); } @@ -171,9 +173,6 @@ private Timer latencyTimer(String method, String outcome) { * @return stage label used in stage-event metrics */ private String classifyStage(JsonRpcError mappedError) { - if (mappedError == null) { - return "unknown_error"; - } return switch (mappedError.code()) { case JsonRpcErrorCode.INVALID_REQUEST -> "invalid_request"; case JsonRpcErrorCode.METHOD_NOT_FOUND -> "method_not_found"; @@ -186,15 +185,11 @@ private String classifyStage(JsonRpcError mappedError) { /** * Classifies failure source based on throwable type and mapped protocol error. * - * @param throwable original throwable associated with the failure + * @param throwable original throwable associated with the failure * @param mappedError protocol error mapped for the response * @return source label used in failure metrics */ private String classifyFailureSource(Throwable throwable, JsonRpcError mappedError) { - if (mappedError == null) { - return "unknown"; - } - int code = mappedError.code(); if (code == JsonRpcErrorCode.INVALID_REQUEST) { return "validation"; @@ -220,42 +215,42 @@ private String classifyFailureSource(Throwable throwable, JsonRpcError mappedErr /** * Resolves or creates counter meters for the target metric family. * - * @param cache in-memory counter cache per metric dimensions - * @param metricName metric family name - * @param method method tag value - * @param firstTagValue first dimension tag value (semantic depends on metric family) + * @param cache in-memory counter cache per metric dimensions + * @param metricName metric family name + * @param method method tag value + * @param firstTagValue first dimension tag value (semantic depends on metric family) * @param secondTagValue second dimension tag value (semantic depends on metric family) * @return cached or newly registered counter */ private Counter counter( - ConcurrentHashMap cache, - String metricName, - String method, - String firstTagValue, - String secondTagValue + ConcurrentHashMap cache, + String metricName, + String method, + String firstTagValue, + String secondTagValue ) { CounterKey key = new CounterKey(metricName, method, firstTagValue, secondTagValue); return cache.computeIfAbsent(key, ignored -> { if (metricName.equals(CALLS_METRIC)) { return meterRegistry.counter( - metricName, - "method", method, - "outcome", firstTagValue, - "errorCode", secondTagValue + metricName, + "method", method, + "outcome", firstTagValue, + "errorCode", secondTagValue ); } if (metricName.equals(STAGE_EVENTS_METRIC)) { return meterRegistry.counter( - metricName, - "method", method, - "stage", firstTagValue + metricName, + "method", method, + "stage", firstTagValue ); } return meterRegistry.counter( - metricName, - "method", method, - "errorCode", firstTagValue, - "source", secondTagValue + metricName, + "method", method, + "errorCode", firstTagValue, + "source", secondTagValue ); }); } @@ -266,7 +261,7 @@ private Counter counter( * @param method raw method name from request * @return normalized method tag value */ - private String normalizeMethodName(String method) { + private String normalizeMethodName(@Nullable String method) { if (method == null || method.isBlank()) { return METHOD_UNKNOWN; } @@ -290,18 +285,20 @@ private String normalizeMethodName(String method) { * * @param metric metric family name * @param method method tag value - * @param first first metric-specific tag value + * @param first first metric-specific tag value * @param second second metric-specific tag value */ private record CounterKey(String metric, String method, String first, String second) { + } /** * Cache key for latency timers differentiated by method and outcome. * - * @param method method tag value + * @param method method tag value * @param outcome outcome tag value */ private record LatencyKey(String method, String outcome) { + } } diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcWebMvcMetricsObserver.java b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcWebMvcMetricsObserver.java index 8a440d8..eda0c99 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcWebMvcMetricsObserver.java +++ b/jsonrpc-spring-boot-autoconfigure/src/main/java/com/limehee/jsonrpc/spring/boot/autoconfigure/support/JsonRpcWebMvcMetricsObserver.java @@ -5,14 +5,14 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.MeterRegistry; - import java.util.List; +import java.util.Objects; /** * Micrometer-backed observer for transport-level JSON-RPC WebMVC events. *

- * This observer tracks parsing failures, request size violations, notification-only handling, - * and batch-level composition details. + * This observer tracks parsing failures, request size violations, notification-only handling, and batch-level + * composition details. *

*/ public final class JsonRpcWebMvcMetricsObserver implements JsonRpcWebMvcObserver { @@ -39,35 +39,42 @@ public final class JsonRpcWebMvcMetricsObserver implements JsonRpcWebMvcObserver /** * Creates a WebMVC observer that records transport and batch metrics. * - * @param meterRegistry registry where metrics are published + * @param meterRegistry registry where metrics are published * @param latencyHistogramEnabled whether histogram distribution is enabled for batch sizes - * @param latencyPercentiles configured percentiles for batch size distribution + * @param latencyPercentiles configured percentiles for batch size distribution */ public JsonRpcWebMvcMetricsObserver( - MeterRegistry meterRegistry, - boolean latencyHistogramEnabled, - double[] latencyPercentiles + MeterRegistry meterRegistry, + boolean latencyHistogramEnabled, + double[] latencyPercentiles ) { - this.parseErrorCounter = meterRegistry.counter(TRANSPORT_ERRORS_METRIC, "reason", "parse_error"); - this.requestTooLargeCounter = meterRegistry.counter(TRANSPORT_ERRORS_METRIC, "reason", "request_too_large"); - this.singleNotificationCounter = meterRegistry.counter(NOTIFICATION_METRIC, "mode", "single"); - this.batchNotificationCounter = meterRegistry.counter(NOTIFICATION_METRIC, "mode", "batch"); - this.batchRequestAllSuccessCounter = meterRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "all_success"); - this.batchRequestAllErrorCounter = meterRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "all_error"); - this.batchRequestMixedCounter = meterRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "mixed"); - this.batchRequestNotificationOnlyCounter = meterRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "notification_only"); - this.batchEntrySuccessCounter = meterRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "success"); - this.batchEntryErrorCounter = meterRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "error"); - this.batchEntryNotificationCounter = meterRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "notification"); + MeterRegistry targetRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry"); + double[] configuredPercentiles = Objects.requireNonNull(latencyPercentiles, "latencyPercentiles"); + + this.parseErrorCounter = targetRegistry.counter(TRANSPORT_ERRORS_METRIC, "reason", "parse_error"); + this.requestTooLargeCounter = targetRegistry.counter(TRANSPORT_ERRORS_METRIC, "reason", "request_too_large"); + this.singleNotificationCounter = targetRegistry.counter(NOTIFICATION_METRIC, "mode", "single"); + this.batchNotificationCounter = targetRegistry.counter(NOTIFICATION_METRIC, "mode", "batch"); + this.batchRequestAllSuccessCounter = targetRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "all_success"); + this.batchRequestAllErrorCounter = targetRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "all_error"); + this.batchRequestMixedCounter = targetRegistry.counter(BATCH_REQUEST_METRIC, "outcome", "mixed"); + this.batchRequestNotificationOnlyCounter = targetRegistry.counter( + BATCH_REQUEST_METRIC, + "outcome", + "notification_only" + ); + this.batchEntrySuccessCounter = targetRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "success"); + this.batchEntryErrorCounter = targetRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "error"); + this.batchEntryNotificationCounter = targetRegistry.counter(BATCH_ENTRY_METRIC, "outcome", "notification"); DistributionSummary.Builder summaryBuilder = DistributionSummary.builder(BATCH_SIZE_METRIC); if (latencyHistogramEnabled) { summaryBuilder.publishPercentileHistogram(); } - if (latencyPercentiles != null && latencyPercentiles.length > 0) { - summaryBuilder.publishPercentiles(latencyPercentiles); + if (configuredPercentiles.length > 0) { + summaryBuilder.publishPercentiles(configuredPercentiles); } - this.batchSizeSummary = summaryBuilder.register(meterRegistry); + this.batchSizeSummary = summaryBuilder.register(targetRegistry); } /** @@ -82,7 +89,7 @@ public void onParseError() { * Increments oversized request counter. * * @param actualBytes actual request payload size in bytes - * @param maxBytes configured maximum payload size in bytes + * @param maxBytes configured maximum payload size in bytes */ @Override public void onRequestTooLarge(int actualBytes, int maxBytes) { @@ -93,7 +100,7 @@ public void onRequestTooLarge(int actualBytes, int maxBytes) { * Records batch composition metrics for success, error, and notification outcomes. * * @param requestCount number of entries in the incoming batch payload - * @param responses emitted JSON-RPC responses for that batch + * @param responses emitted JSON-RPC responses for that batch */ @Override public void onBatchResponse(int requestCount, List responses) { @@ -129,7 +136,7 @@ public void onBatchResponse(int requestCount, List responses) { /** * Records notification-only request handling counts. * - * @param batch {@code true} if the original payload was a batch array + * @param batch {@code true} if the original payload was a batch array * @param requestCount number of request entries in the payload */ @Override @@ -145,7 +152,7 @@ public void onNotificationOnly(boolean batch, int requestCount) { * Increments batch entry counters by outcome type. * * @param outcome outcome label ({@code success}, {@code error}, or {@code notification}) - * @param count number of entries to increment + * @param count number of entries to increment */ private void incrementByOutcome(String outcome, int count) { if (count <= 0) { diff --git a/jsonrpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/jsonrpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d962287..8c0def4 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/jsonrpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -164,45 +164,73 @@ { "name": "jsonrpc.path", "values": [ - { "value": "/jsonrpc" }, - { "value": "/api/jsonrpc" } + { + "value": "/jsonrpc" + }, + { + "value": "/api/jsonrpc" + } ] }, { "name": "jsonrpc.method-registration-conflict-policy", "values": [ - { "value": "REJECT" }, - { "value": "REPLACE" } + { + "value": "REJECT" + }, + { + "value": "REPLACE" + } ] }, { "name": "jsonrpc.validation.request.params-type-violation-code-policy", "values": [ - { "value": "INVALID_PARAMS" }, - { "value": "INVALID_REQUEST" } + { + "value": "INVALID_PARAMS" + }, + { + "value": "INVALID_REQUEST" + } ] }, { "name": "jsonrpc.notification-executor-bean-name", "values": [ - { "value": "applicationTaskExecutor" } + { + "value": "applicationTaskExecutor" + } ] }, { "name": "jsonrpc.metrics-latency-percentiles", "values": [ - { "value": 0.5 }, - { "value": 0.9 }, - { "value": 0.95 }, - { "value": 0.99 } + { + "value": 0.5 + }, + { + "value": 0.9 + }, + { + "value": 0.95 + }, + { + "value": 0.99 + } ] }, { "name": "jsonrpc.metrics-max-method-tag-values", "values": [ - { "value": 50 }, - { "value": 100 }, - { "value": 200 } + { + "value": 50 + }, + { + "value": 100 + }, + { + "value": 200 + } ] } ] diff --git a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/InstrumentedJsonRpcNotificationExecutorTest.java b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/InstrumentedJsonRpcNotificationExecutorTest.java index c684f74..fdee2b6 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/InstrumentedJsonRpcNotificationExecutorTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/InstrumentedJsonRpcNotificationExecutorTest.java @@ -1,15 +1,14 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.limehee.jsonrpc.core.JsonRpcNotificationExecutor; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.InstrumentedJsonRpcNotificationExecutor; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.junit.jupiter.api.Test; - import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; class InstrumentedJsonRpcNotificationExecutorTest { @@ -22,10 +21,10 @@ void recordsQueueDelayAndExecutionMetrics() { task.run(); }; InstrumentedJsonRpcNotificationExecutor executor = new InstrumentedJsonRpcNotificationExecutor( - delegate, - meterRegistry, - false, - new double[0] + delegate, + meterRegistry, + false, + new double[0] ); executor.execute(() -> { @@ -43,16 +42,16 @@ void recordsFailedNotificationExecution() { MeterRegistry meterRegistry = new SimpleMeterRegistry(); JsonRpcNotificationExecutor delegate = Runnable::run; InstrumentedJsonRpcNotificationExecutor executor = new InstrumentedJsonRpcNotificationExecutor( - delegate, - meterRegistry, - false, - new double[0] + delegate, + meterRegistry, + false, + new double[0] ); assertThrows(IllegalStateException.class, () -> - executor.execute(() -> { - throw new IllegalStateException("boom"); - })); + executor.execute(() -> { + throw new IllegalStateException("boom"); + })); assertEquals(1.0, meterRegistry.counter("jsonrpc.server.notification.submitted").count()); assertEquals(1.0, meterRegistry.counter("jsonrpc.server.notification.failed").count()); diff --git a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfigurationTest.java b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfigurationTest.java index 691e802..0df25e5 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfigurationTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcAutoConfigurationTest.java @@ -1,9 +1,11 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.node.IntNode; -import tools.jackson.databind.node.StringNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcException; import com.limehee.jsonrpc.core.JsonRpcInterceptor; @@ -17,608 +19,605 @@ import com.limehee.jsonrpc.core.JsonRpcTypedMethodHandlerFactory; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.StringNode; class JsonRpcAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JsonRpcAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(JsonRpcAutoConfiguration.class)); @Test void createsDispatcherAndRegistersMethods() { contextRunner - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - assertNotNull(dispatcher); + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + assertNotNull(dispatcher); - JsonRpcRequest request = new JsonRpcRequest("2.0", IntNode.valueOf(1), "ping", null, true); - JsonRpcResponse response = dispatcher.dispatch(request); - assertNotNull(response); - assertEquals("pong", response.result().asString()); + JsonRpcRequest request = new JsonRpcRequest("2.0", IntNode.valueOf(1), "ping", null, true); + JsonRpcResponse response = dispatcher.dispatch(request); + assertNotNull(response); + assertEquals("pong", response.result().asString()); - JsonRpcTypedMethodHandlerFactory typedFactory = context.getBean(JsonRpcTypedMethodHandlerFactory.class); - assertNotNull(typedFactory); - }); + JsonRpcTypedMethodHandlerFactory typedFactory = context.getBean(JsonRpcTypedMethodHandlerFactory.class); + assertNotNull(typedFactory); + }); } @Test void registersAnnotatedMethods() throws Exception { contextRunner - .withUserConfiguration(AnnotatedMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - assertNotNull(dispatcher); + .withUserConfiguration(AnnotatedMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + assertNotNull(dispatcher); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(7), - "hello", - new ObjectMapper().readTree("{\"name\":\"developer\"}"), - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(7), + "hello", + new ObjectMapper().readTree("{\"name\":\"developer\"}"), + true + )); - assertNotNull(response); - assertEquals("hello developer", response.result().asString()); - }); + assertNotNull(response); + assertEquals("hello developer", response.result().asString()); + }); } @Test void doesNotRegisterAnnotatedMethodsWhenScanDisabled() throws Exception { contextRunner - .withPropertyValues("jsonrpc.scan-annotated-methods=false") - .withUserConfiguration(AnnotatedMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + .withPropertyValues("jsonrpc.scan-annotated-methods=false") + .withUserConfiguration(AnnotatedMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(70), - "hello", - new ObjectMapper().readTree("{\"name\":\"developer\"}"), - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(70), + "hello", + new ObjectMapper().readTree("{\"name\":\"developer\"}"), + true + )); - assertNotNull(response.error()); - assertEquals(-32601, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32601, response.error().code()); + }); } @Test void registersAnnotatedMethodsWithPositionalParams() throws Exception { contextRunner - .withUserConfiguration(AnnotatedPositionalMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - assertNotNull(dispatcher); + .withUserConfiguration(AnnotatedPositionalMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + assertNotNull(dispatcher); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(8), - "sum", - new ObjectMapper().readTree("[2,3]"), - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(8), + "sum", + new ObjectMapper().readTree("[2,3]"), + true + )); - assertNotNull(response); - assertEquals(5, response.result().asInt()); - }); + assertNotNull(response); + assertEquals(5, response.result().asInt()); + }); } @Test void positionalAnnotatedMethodReturnsInvalidParamsWhenRequiredFieldMissing() throws Exception { contextRunner - .withUserConfiguration(AnnotatedPositionalMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(9), - "sum", - new ObjectMapper().readTree("{\"left\":2}"), - true - )); + .withUserConfiguration(AnnotatedPositionalMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(9), + "sum", + new ObjectMapper().readTree("{\"left\":2}"), + true + )); - assertNotNull(response.error()); - assertEquals(-32602, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32602, response.error().code()); + }); } @Test void registersAnnotatedMethodsWithNamedParamsObject() throws Exception { contextRunner - .withUserConfiguration(AnnotatedNamedMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - assertNotNull(dispatcher); + .withUserConfiguration(AnnotatedNamedMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + assertNotNull(dispatcher); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(10), - "concat", - new ObjectMapper().readTree("{\"left\":\"a\",\"right\":\"b\"}"), - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(10), + "concat", + new ObjectMapper().readTree("{\"left\":\"a\",\"right\":\"b\"}"), + true + )); - assertNotNull(response); - assertEquals("ab", response.result().asString()); - }); + assertNotNull(response); + assertEquals("ab", response.result().asString()); + }); } @Test void namedAnnotatedMethodReturnsInvalidParamsWhenFieldMissing() throws Exception { contextRunner - .withUserConfiguration(AnnotatedNamedMethodConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(12), - "concat", - new ObjectMapper().readTree("{\"left\":\"a\"}"), - true - )); + .withUserConfiguration(AnnotatedNamedMethodConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(12), + "concat", + new ObjectMapper().readTree("{\"left\":\"a\"}"), + true + )); - assertNotNull(response.error()); - assertEquals(-32602, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32602, response.error().code()); + }); } @Test void namedAnnotatedMethodCanUseJavaParameterNamesWhenAvailable() throws Exception { contextRunner - .withUserConfiguration(AnnotatedNamedWithoutParamAnnotationConfig.class) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(13), - "join", - new ObjectMapper().readTree("{\"left\":\"x\",\"right\":\"y\"}"), - true - )); + .withUserConfiguration(AnnotatedNamedWithoutParamAnnotationConfig.class) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(13), + "join", + new ObjectMapper().readTree("{\"left\":\"x\",\"right\":\"y\"}"), + true + )); - assertNotNull(response); - assertEquals("xy", response.result().asString()); - }); + assertNotNull(response); + assertEquals("xy", response.result().asString()); + }); } @Test void wiresInterceptorsIntoDispatcher() { contextRunner - .withUserConfiguration(InterceptorConfig.class) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - CountingInterceptor interceptor = context.getBean(CountingInterceptor.class); + .withUserConfiguration(InterceptorConfig.class) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + CountingInterceptor interceptor = context.getBean(CountingInterceptor.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(1), - "ping", - null, - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(1), + "ping", + null, + true + )); - assertEquals("pong", response.result().asString()); - assertTrue(interceptor.beforeInvokeCount > 0); - assertTrue(interceptor.afterInvokeCount > 0); - }); + assertEquals("pong", response.result().asString()); + assertTrue(interceptor.beforeInvokeCount > 0); + assertTrue(interceptor.afterInvokeCount > 0); + }); } @Test void hidesErrorDataByDefault() { contextRunner - .withBean("boom", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("boom", params -> { - throw new JsonRpcException(-32001, "domain", StringNode.valueOf("secret")); - })) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(11), - "boom", - null, - true - )); + .withBean("boom", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("boom", params -> { + throw new JsonRpcException(-32001, "domain", StringNode.valueOf("secret")); + })) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(11), + "boom", + null, + true + )); - assertNotNull(response.error()); - assertEquals(-32001, response.error().code()); - assertEquals("domain", response.error().message()); - assertNull(response.error().data()); - }); + assertNotNull(response.error()); + assertEquals(-32001, response.error().code()); + assertEquals("domain", response.error().message()); + assertNull(response.error().data()); + }); } @Test void includesErrorDataWhenConfigured() { contextRunner - .withPropertyValues("jsonrpc.include-error-data=true") - .withBean("boom", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("boom", params -> { - throw new JsonRpcException(-32001, "domain", StringNode.valueOf("secret")); - })) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(11), - "boom", - null, - true - )); + .withPropertyValues("jsonrpc.include-error-data=true") + .withBean("boom", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("boom", params -> { + throw new JsonRpcException(-32001, "domain", StringNode.valueOf("secret")); + })) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(11), + "boom", + null, + true + )); - assertNotNull(response.error()); - assertEquals(-32001, response.error().code()); - assertEquals("secret", response.error().data().asString()); - }); + assertNotNull(response.error()); + assertEquals(-32001, response.error().code()); + assertEquals("secret", response.error().data().asString()); + }); } @Test void recordsMetricsWhenEnabled() { contextRunner - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(21), - "ping", - null, - true - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(21), + "ping", + null, + true + )); - double callCount = meterRegistry.counter( - "jsonrpc.server.calls", - "method", "ping", - "outcome", "success", - "errorCode", "none" - ).count(); - assertEquals(1.0, callCount); - }); + double callCount = meterRegistry.counter( + "jsonrpc.server.calls", + "method", "ping", + "outcome", "success", + "errorCode", "none" + ).count(); + assertEquals(1.0, callCount); + }); } @Test void recordsStageAndFailureMetricsForErrorCases() { contextRunner - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withBean("badParams", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("bad.params", params -> { - throw new JsonRpcException(-32602, "Invalid params"); - })) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(22), - "bad.params", - null, - true - )); - - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.stage.events", - "method", "bad.params", - "stage", "invalid_params" - ).count()); - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.failures", - "method", "bad.params", - "errorCode", "-32602", - "source", "binding" - ).count()); - }); + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withBean("badParams", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("bad.params", params -> { + throw new JsonRpcException(-32602, "Invalid params"); + })) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(22), + "bad.params", + null, + true + )); + + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.stage.events", + "method", "bad.params", + "stage", "invalid_params" + ).count()); + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.failures", + "method", "bad.params", + "errorCode", "-32602", + "source", "binding" + ).count()); + }); } @Test void recordsAccessControlFailureMetricWhenDenylistBlocksMethod() { contextRunner - .withPropertyValues("jsonrpc.method-denylist[0]=ping") - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + .withPropertyValues("jsonrpc.method-denylist[0]=ping") + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(25), - "ping", - null, - true - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(25), + "ping", + null, + true + )); - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.failures", - "method", "ping", - "errorCode", "-32601", - "source", "access_control" - ).count()); - }); + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.failures", + "method", "ping", + "errorCode", "-32601", + "source", "access_control" + ).count()); + }); } @Test void recordsInterceptorFailureMetricWhenInterceptorThrows() { contextRunner - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withUserConfiguration(ThrowingInterceptorConfig.class) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(26), - "ping", - null, - true - )); - - assertNotNull(response.error()); - assertEquals(-32603, response.error().code()); - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.failures", - "method", "ping", - "errorCode", "-32603", - "source", "interceptor" - ).count()); - }); + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withUserConfiguration(ThrowingInterceptorConfig.class) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(26), + "ping", + null, + true + )); + + assertNotNull(response.error()); + assertEquals(-32603, response.error().code()); + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.failures", + "method", "ping", + "errorCode", "-32603", + "source", "interceptor" + ).count()); + }); } @Test void capsMethodTagCardinalityWhenConfigured() { contextRunner - .withPropertyValues("jsonrpc.metrics-max-method-tag-values=1") - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withBean("a", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("method.a", params -> StringNode.valueOf("a"))) - .withBean("b", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("method.b", params -> StringNode.valueOf("b"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - - dispatcher.dispatch(new JsonRpcRequest( - "2.0", IntNode.valueOf(23), "method.a", null, true)); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", IntNode.valueOf(24), "method.b", null, true)); - - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.calls", - "method", "method.a", - "outcome", "success", - "errorCode", "none" - ).count()); - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.calls", - "method", "other", - "outcome", "success", - "errorCode", "none" - ).count()); - }); + .withPropertyValues("jsonrpc.metrics-max-method-tag-values=1") + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withBean("a", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("method.a", params -> StringNode.valueOf("a"))) + .withBean("b", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("method.b", params -> StringNode.valueOf("b"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + + dispatcher.dispatch(new JsonRpcRequest( + "2.0", IntNode.valueOf(23), "method.a", null, true)); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", IntNode.valueOf(24), "method.b", null, true)); + + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.calls", + "method", "method.a", + "outcome", "success", + "errorCode", "none" + ).count()); + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.calls", + "method", "other", + "outcome", "success", + "errorCode", "none" + ).count()); + }); } @Test void recordsLatencyForAsyncNotificationWhenExecutorEnabled() { contextRunner - .withPropertyValues("jsonrpc.notification-executor-enabled=true") - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .withUserConfiguration(AsyncNotificationExecutorConfig.class) - .withBean("notify", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - AsyncCountingExecutor executor = (AsyncCountingExecutor) context.getBean("applicationTaskExecutor"); - - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - null, - "notify", - null, - false - )); - - assertTrue(executor.awaitCompletion()); - assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.calls", - "method", "notify", - "outcome", "success", - "errorCode", "none" - ).count()); - assertEquals(1L, meterRegistry.timer( - "jsonrpc.server.latency", - "method", "notify", - "outcome", "success" - ).count()); - assertEquals(1.0, meterRegistry.counter("jsonrpc.server.notification.submitted").count()); - assertEquals(1L, meterRegistry.timer("jsonrpc.server.notification.execution").count()); - assertEquals(1L, meterRegistry.timer("jsonrpc.server.notification.queue.delay").count()); - }); + .withPropertyValues("jsonrpc.notification-executor-enabled=true") + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .withUserConfiguration(AsyncNotificationExecutorConfig.class) + .withBean("notify", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + AsyncCountingExecutor executor = (AsyncCountingExecutor) context.getBean("applicationTaskExecutor"); + + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + null, + "notify", + null, + false + )); + + assertTrue(executor.awaitCompletion()); + assertEquals(1.0, meterRegistry.counter( + "jsonrpc.server.calls", + "method", "notify", + "outcome", "success", + "errorCode", "none" + ).count()); + assertEquals(1L, meterRegistry.timer( + "jsonrpc.server.latency", + "method", "notify", + "outcome", "success" + ).count()); + assertEquals(1.0, meterRegistry.counter("jsonrpc.server.notification.submitted").count()); + assertEquals(1L, meterRegistry.timer("jsonrpc.server.notification.execution").count()); + assertEquals(1L, meterRegistry.timer("jsonrpc.server.notification.queue.delay").count()); + }); } @Test void doesNotCreateMetricsInterceptorWhenDisabled() { contextRunner - .withPropertyValues("jsonrpc.metrics-enabled=false") - .withBean(MeterRegistry.class, SimpleMeterRegistry::new) - .run(context -> assertFalse(context.containsBean("jsonRpcMetricsInterceptor"))); + .withPropertyValues("jsonrpc.metrics-enabled=false") + .withBean(MeterRegistry.class, SimpleMeterRegistry::new) + .run(context -> assertFalse(context.containsBean("jsonRpcMetricsInterceptor"))); } @Test void blocksMethodsNotInAllowlist() { contextRunner - .withPropertyValues("jsonrpc.method-allowlist[0]=ping") - .withBean("pong", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("pong", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + .withPropertyValues("jsonrpc.method-allowlist[0]=ping") + .withBean("pong", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("pong", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(31), - "pong", - null, - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(31), + "pong", + null, + true + )); - assertNotNull(response.error()); - assertEquals(-32601, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32601, response.error().code()); + }); } @Test void blocksMethodsInDenylist() { contextRunner - .withPropertyValues("jsonrpc.method-denylist[0]=ping") - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + .withPropertyValues("jsonrpc.method-denylist[0]=ping") + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(32), - "ping", - null, - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(32), + "ping", + null, + true + )); - assertNotNull(response.error()); - assertEquals(-32601, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32601, response.error().code()); + }); } @Test void denylistTakesPrecedenceWhenMethodInAllowlistAndDenylist() { contextRunner - .withPropertyValues( - "jsonrpc.method-allowlist[0]=ping", - "jsonrpc.method-denylist[0]=ping" - ) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + .withPropertyValues( + "jsonrpc.method-allowlist[0]=ping", + "jsonrpc.method-denylist[0]=ping" + ) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(33), - "ping", - null, - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(33), + "ping", + null, + true + )); - assertNotNull(response.error()); - assertEquals(-32601, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32601, response.error().code()); + }); } @Test void normalizesAllowlistValuesByTrimmingWhitespace() { contextRunner - .withPropertyValues( - "jsonrpc.method-allowlist[0]= ping " - ) - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + .withPropertyValues( + "jsonrpc.method-allowlist[0]= ping " + ) + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(34), - "ping", - null, - true - )); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(34), + "ping", + null, + true + )); - assertEquals("pong", response.result().asString()); - }); + assertEquals("pong", response.result().asString()); + }); } @Test void rejectsBlankMethodEntriesInAllowAndDenyLists() { contextRunner - .withPropertyValues( - "jsonrpc.method-allowlist[0]= ", - "jsonrpc.method-denylist[0]= " - ) - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues( + "jsonrpc.method-allowlist[0]= ", + "jsonrpc.method-denylist[0]= " + ) + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void mapsParamsTypeViolationToInvalidParamsByDefault() { contextRunner - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(35), - "ping", - StringNode.valueOf("invalid-shape"), - true - )); + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(35), + "ping", + StringNode.valueOf("invalid-shape"), + true + )); - assertNotNull(response.error()); - assertEquals(-32602, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32602, response.error().code()); + }); } @Test void mapsParamsTypeViolationToInvalidRequestWhenConfiguredViaProperty() { contextRunner - .withPropertyValues("jsonrpc.validation.request.params-type-violation-code-policy=INVALID_REQUEST") - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( - "2.0", - IntNode.valueOf(36), - "ping", - StringNode.valueOf("invalid-shape"), - true - )); + .withPropertyValues("jsonrpc.validation.request.params-type-violation-code-policy=INVALID_REQUEST") + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + JsonRpcResponse response = dispatcher.dispatch(new JsonRpcRequest( + "2.0", + IntNode.valueOf(36), + "ping", + StringNode.valueOf("invalid-shape"), + true + )); - assertNotNull(response.error()); - assertEquals(-32600, response.error().code()); - }); + assertNotNull(response.error()); + assertEquals(-32600, response.error().code()); + }); } @Test void rejectsUnknownParamsTypeViolationCodePolicyValue() { contextRunner - .withPropertyValues("jsonrpc.validation.request.params-type-violation-code-policy=NOT_A_POLICY") - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.validation.request.params-type-violation-code-policy=NOT_A_POLICY") + .run(context -> assertNotNull(context.getStartupFailure())); } @Test @@ -646,183 +645,184 @@ void bindsDefaultResponseValidationOptions() { @Test void appliesConfiguredResponseValidationOptions() { contextRunner - .withPropertyValues( - "jsonrpc.validation.response.require-response-id-member=false", - "jsonrpc.validation.response.allow-fractional-response-id=false", - "jsonrpc.validation.response.allow-request-fields-in-response=false" - ) - .run(context -> { - JsonRpcResponseValidationOptions options = context.getBean(JsonRpcResponseValidationOptions.class); + .withPropertyValues( + "jsonrpc.validation.response.require-response-id-member=false", + "jsonrpc.validation.response.allow-fractional-response-id=false", + "jsonrpc.validation.response.allow-request-fields-in-response=false" + ) + .run(context -> { + JsonRpcResponseValidationOptions options = context.getBean(JsonRpcResponseValidationOptions.class); - assertFalse(options.requireResponseIdMember()); - assertFalse(options.allowFractionalResponseId()); - assertFalse(options.allowRequestFieldsInResponse()); - }); + assertFalse(options.requireResponseIdMember()); + assertFalse(options.allowFractionalResponseId()); + assertFalse(options.allowRequestFieldsInResponse()); + }); } @Test void rejectsMaxBatchSizeLessThanOne() { contextRunner - .withPropertyValues("jsonrpc.max-batch-size=0") - .withBean("ping", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.max-batch-size=0") + .withBean("ping", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong"))) + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void rejectsInvalidPathThatDoesNotStartWithSlash() { contextRunner - .withPropertyValues("jsonrpc.path=jsonrpc") - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.path=jsonrpc") + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void rejectsMetricsMaxMethodTagValuesLessThanOne() { contextRunner - .withPropertyValues("jsonrpc.metrics-max-method-tag-values=0") - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.metrics-max-method-tag-values=0") + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void rejectsInvalidMetricsLatencyPercentiles() { contextRunner - .withPropertyValues("jsonrpc.metrics-latency-percentiles[0]=1.0") - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.metrics-latency-percentiles[0]=1.0") + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void duplicateMethodRegistrationFailsByDefault() { contextRunner - .withBean("ping1", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong1"))) - .withBean("ping2", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong2"))) - .run(context -> assertNotNull(context.getStartupFailure())); + .withBean("ping1", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong1"))) + .withBean("ping2", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong2"))) + .run(context -> assertNotNull(context.getStartupFailure())); } @Test void duplicateMethodRegistrationCanBeReplacedWhenConfigured() { contextRunner - .withPropertyValues("jsonrpc.method-registration-conflict-policy=REPLACE") - .withBean("ping1", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong1"))) - .withBean("ping2", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong2"))) - .run(context -> assertNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.method-registration-conflict-policy=REPLACE") + .withBean("ping1", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong1"))) + .withBean("ping2", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("pong2"))) + .run(context -> assertNull(context.getStartupFailure())); } @Test void usesExecutorForNotificationsWhenEnabled() { contextRunner - .withPropertyValues("jsonrpc.notification-executor-enabled=true") - .withUserConfiguration(NotificationExecutorConfig.class) - .withBean("notify", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - CountingExecutor executor = context.getBean(CountingExecutor.class); + .withPropertyValues("jsonrpc.notification-executor-enabled=true") + .withUserConfiguration(NotificationExecutorConfig.class) + .withBean("notify", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + CountingExecutor executor = context.getBean(CountingExecutor.class); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - null, - "notify", - null, - false - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + null, + "notify", + null, + false + )); - assertEquals(1, executor.executeCount.get()); - }); + assertEquals(1, executor.executeCount.get()); + }); } @Test void doesNotUseExecutorForNotificationsWhenDisabled() { contextRunner - .withPropertyValues("jsonrpc.notification-executor-enabled=false") - .withUserConfiguration(NotificationExecutorConfig.class) - .withBean("notify", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - CountingExecutor executor = context.getBean(CountingExecutor.class); + .withPropertyValues("jsonrpc.notification-executor-enabled=false") + .withUserConfiguration(NotificationExecutorConfig.class) + .withBean("notify", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + CountingExecutor executor = context.getBean(CountingExecutor.class); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - null, - "notify", - null, - false - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + null, + "notify", + null, + false + )); - assertEquals(0, executor.executeCount.get()); - }); + assertEquals(0, executor.executeCount.get()); + }); } @Test void usesDirectNotificationExecutionWhenMultipleExecutorsExistWithoutSelectionHint() { contextRunner - .withPropertyValues("jsonrpc.notification-executor-enabled=true") - .withUserConfiguration(MultipleNotificationExecutorConfig.class) - .withBean("notify", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - CountingExecutor firstExecutor = (CountingExecutor) context.getBean("firstExecutor"); - CountingExecutor secondExecutor = (CountingExecutor) context.getBean("secondExecutor"); + .withPropertyValues("jsonrpc.notification-executor-enabled=true") + .withUserConfiguration(MultipleNotificationExecutorConfig.class) + .withBean("notify", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + CountingExecutor firstExecutor = (CountingExecutor) context.getBean("firstExecutor"); + CountingExecutor secondExecutor = (CountingExecutor) context.getBean("secondExecutor"); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - null, - "notify", - null, - false - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + null, + "notify", + null, + false + )); - assertEquals(0, firstExecutor.executeCount.get()); - assertEquals(0, secondExecutor.executeCount.get()); - }); + assertEquals(0, firstExecutor.executeCount.get()); + assertEquals(0, secondExecutor.executeCount.get()); + }); } @Test void usesSelectedNotificationExecutorBeanWhenConfigured() { contextRunner - .withPropertyValues( - "jsonrpc.notification-executor-enabled=true", - "jsonrpc.notification-executor-bean-name=secondExecutor" - ) - .withUserConfiguration(MultipleNotificationExecutorConfig.class) - .withBean("notify", JsonRpcMethodRegistration.class, - () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) - .run(context -> { - JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); - CountingExecutor firstExecutor = (CountingExecutor) context.getBean("firstExecutor"); - CountingExecutor secondExecutor = (CountingExecutor) context.getBean("secondExecutor"); + .withPropertyValues( + "jsonrpc.notification-executor-enabled=true", + "jsonrpc.notification-executor-bean-name=secondExecutor" + ) + .withUserConfiguration(MultipleNotificationExecutorConfig.class) + .withBean("notify", JsonRpcMethodRegistration.class, + () -> JsonRpcMethodRegistration.of("notify", params -> StringNode.valueOf("ok"))) + .run(context -> { + JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); + CountingExecutor firstExecutor = (CountingExecutor) context.getBean("firstExecutor"); + CountingExecutor secondExecutor = (CountingExecutor) context.getBean("secondExecutor"); - dispatcher.dispatch(new JsonRpcRequest( - "2.0", - null, - "notify", - null, - false - )); + dispatcher.dispatch(new JsonRpcRequest( + "2.0", + null, + "notify", + null, + false + )); - assertEquals(0, firstExecutor.executeCount.get()); - assertEquals(1, secondExecutor.executeCount.get()); - }); + assertEquals(0, firstExecutor.executeCount.get()); + assertEquals(1, secondExecutor.executeCount.get()); + }); } @Test void failsFastWhenSelectedNotificationExecutorBeanDoesNotExist() { contextRunner - .withPropertyValues( - "jsonrpc.notification-executor-enabled=true", - "jsonrpc.notification-executor-bean-name=missingExecutor" - ) - .withUserConfiguration(NotificationExecutorConfig.class) - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues( + "jsonrpc.notification-executor-enabled=true", + "jsonrpc.notification-executor-bean-name=missingExecutor" + ) + .withUserConfiguration(NotificationExecutorConfig.class) + .run(context -> assertNotNull(context.getStartupFailure())); } @Configuration(proxyBeanMethods = false) static class AnnotatedMethodConfig { + @Bean AnnotatedHandler annotatedHandler() { return new AnnotatedHandler(); @@ -831,6 +831,7 @@ AnnotatedHandler annotatedHandler() { @Configuration(proxyBeanMethods = false) static class AnnotatedPositionalMethodConfig { + @Bean AnnotatedPositionalHandler annotatedPositionalHandler() { return new AnnotatedPositionalHandler(); @@ -839,6 +840,7 @@ AnnotatedPositionalHandler annotatedPositionalHandler() { @Configuration(proxyBeanMethods = false) static class AnnotatedNamedMethodConfig { + @Bean AnnotatedNamedHandler annotatedNamedHandler() { return new AnnotatedNamedHandler(); @@ -847,6 +849,7 @@ AnnotatedNamedHandler annotatedNamedHandler() { @Configuration(proxyBeanMethods = false) static class AnnotatedNamedWithoutParamAnnotationConfig { + @Bean AnnotatedNamedWithoutParamAnnotationHandler annotatedNamedWithoutParamAnnotationHandler() { return new AnnotatedNamedWithoutParamAnnotationHandler(); @@ -855,6 +858,7 @@ AnnotatedNamedWithoutParamAnnotationHandler annotatedNamedWithoutParamAnnotation @Configuration(proxyBeanMethods = false) static class InterceptorConfig { + @Bean CountingInterceptor countingInterceptor() { return new CountingInterceptor(); @@ -863,6 +867,7 @@ CountingInterceptor countingInterceptor() { @Configuration(proxyBeanMethods = false) static class ThrowingInterceptorConfig { + @Bean JsonRpcInterceptor throwingInterceptor() { return new JsonRpcInterceptor() { @@ -876,6 +881,7 @@ public void beforeInvoke(JsonRpcRequest request) { @Configuration(proxyBeanMethods = false) static class NotificationExecutorConfig { + @Bean CountingExecutor countingExecutor() { return new CountingExecutor(); @@ -884,6 +890,7 @@ CountingExecutor countingExecutor() { @Configuration(proxyBeanMethods = false) static class MultipleNotificationExecutorConfig { + @Bean Executor firstExecutor() { return new CountingExecutor(); @@ -897,6 +904,7 @@ Executor secondExecutor() { @Configuration(proxyBeanMethods = false) static class AsyncNotificationExecutorConfig { + @Bean Executor applicationTaskExecutor() { return new AsyncCountingExecutor(); @@ -904,6 +912,7 @@ Executor applicationTaskExecutor() { } static class AnnotatedHandler { + @JsonRpcMethod("hello") public String hello(NameParams params) { return "hello " + params.name(); @@ -911,6 +920,7 @@ public String hello(NameParams params) { } static class AnnotatedPositionalHandler { + @JsonRpcMethod("sum") public int sum(int left, int right) { return left + right; @@ -918,6 +928,7 @@ public int sum(int left, int right) { } static class AnnotatedNamedHandler { + @JsonRpcMethod("concat") public String concat(@JsonRpcParam("left") String left, @JsonRpcParam("right") String right) { return left + right; @@ -925,6 +936,7 @@ public String concat(@JsonRpcParam("left") String left, @JsonRpcParam("right") S } static class AnnotatedNamedWithoutParamAnnotationHandler { + @JsonRpcMethod("join") public String join(String left, String right) { return left + right; @@ -932,9 +944,11 @@ public String join(String left, String right) { } record NameParams(String name) { + } static class CountingInterceptor implements JsonRpcInterceptor { + int beforeInvokeCount; int afterInvokeCount; @@ -950,6 +964,7 @@ public void afterInvoke(JsonRpcRequest request, JsonNode result) { } static class CountingExecutor implements Executor { + private final AtomicInteger executeCount = new AtomicInteger(); @Override @@ -960,6 +975,7 @@ public void execute(Runnable command) { } static class AsyncCountingExecutor implements Executor { + private final AtomicInteger executeCount = new AtomicInteger(); private final CountDownLatch completion = new CountDownLatch(1); diff --git a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcMethodAccessInterceptorTest.java b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcMethodAccessInterceptorTest.java index a744e3b..c0b439f 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcMethodAccessInterceptorTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcMethodAccessInterceptorTest.java @@ -1,18 +1,17 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.node.IntNode; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcException; import com.limehee.jsonrpc.core.JsonRpcRequest; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcMethodAccessInterceptor; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import tools.jackson.databind.node.IntNode; class JsonRpcMethodAccessInterceptorTest { diff --git a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebAutoConfigurationTest.java b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebAutoConfigurationTest.java index b96beec..7b93f6a 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebAutoConfigurationTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebAutoConfigurationTest.java @@ -1,8 +1,16 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.limehee.jsonrpc.core.JsonRpcResponse; import com.limehee.jsonrpc.spring.webmvc.JsonRpcHttpStatusStrategy; import com.limehee.jsonrpc.spring.webmvc.JsonRpcWebMvcEndpoint; +import java.nio.charset.StandardCharsets; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -11,58 +19,51 @@ import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - class JsonRpcWebAutoConfigurationTest { private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JsonRpcAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(JsonRpcAutoConfiguration.class)); @Test void createsWebMvcEndpointWhenEnabled() { webContextRunner.run(context -> - assertTrue(context.containsBean("jsonRpcWebMvcEndpoint"))); + assertTrue(context.containsBean("jsonRpcWebMvcEndpoint"))); } @Test void doesNotCreateWebMvcEndpointWhenDisabled() { webContextRunner - .withPropertyValues("jsonrpc.enabled=false") - .run(context -> assertFalse(context.containsBean("jsonRpcWebMvcEndpoint"))); + .withPropertyValues("jsonrpc.enabled=false") + .run(context -> assertFalse(context.containsBean("jsonRpcWebMvcEndpoint"))); } @Test void exposesJsonRpcWebMvcEndpointType() { webContextRunner.run(context -> - assertTrue(context.getBean("jsonRpcWebMvcEndpoint") instanceof JsonRpcWebMvcEndpoint)); + assertInstanceOf(JsonRpcWebMvcEndpoint.class, context.getBean("jsonRpcWebMvcEndpoint"))); } @Test void usesCustomHttpStatusStrategyBean() { webContextRunner - .withUserConfiguration(CustomHttpStatusStrategyConfig.class) - .run(context -> { - JsonRpcWebMvcEndpoint endpoint = context.getBean(JsonRpcWebMvcEndpoint.class); - HttpStatusCode status = endpoint.invoke("{".getBytes(StandardCharsets.UTF_8)).getStatusCode(); - assertEquals(HttpStatus.BAD_REQUEST.value(), status.value()); - }); + .withUserConfiguration(CustomHttpStatusStrategyConfig.class) + .run(context -> { + JsonRpcWebMvcEndpoint endpoint = context.getBean(JsonRpcWebMvcEndpoint.class); + HttpStatusCode status = endpoint.invoke("{".getBytes(StandardCharsets.UTF_8)).getStatusCode(); + assertEquals(HttpStatus.BAD_REQUEST.value(), status.value()); + }); } @Test void rejectsMaxRequestBytesLessThanOne() { webContextRunner - .withPropertyValues("jsonrpc.max-request-bytes=0") - .run(context -> assertNotNull(context.getStartupFailure())); + .withPropertyValues("jsonrpc.max-request-bytes=0") + .run(context -> assertNotNull(context.getStartupFailure())); } @Configuration(proxyBeanMethods = false) static class CustomHttpStatusStrategyConfig { + @Bean JsonRpcHttpStatusStrategy customJsonRpcHttpStatusStrategy() { return new JsonRpcHttpStatusStrategy() { diff --git a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebMvcMetricsObserverTest.java b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebMvcMetricsObserverTest.java index 4fff2f2..b97157b 100644 --- a/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebMvcMetricsObserverTest.java +++ b/jsonrpc-spring-boot-autoconfigure/src/test/java/com/limehee/jsonrpc/spring/boot/autoconfigure/JsonRpcWebMvcMetricsObserverTest.java @@ -1,16 +1,15 @@ package com.limehee.jsonrpc.spring.boot.autoconfigure; -import tools.jackson.databind.node.IntNode; -import tools.jackson.databind.node.StringNode; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.limehee.jsonrpc.core.JsonRpcResponse; import com.limehee.jsonrpc.spring.boot.autoconfigure.support.JsonRpcWebMvcMetricsObserver; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.junit.jupiter.api.Test; - import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.StringNode; class JsonRpcWebMvcMetricsObserverTest { @@ -18,9 +17,9 @@ class JsonRpcWebMvcMetricsObserverTest { void recordsTransportAndBatchMetrics() { MeterRegistry meterRegistry = new SimpleMeterRegistry(); JsonRpcWebMvcMetricsObserver observer = new JsonRpcWebMvcMetricsObserver( - meterRegistry, - false, - new double[0] + meterRegistry, + false, + new double[0] ); observer.onParseError(); @@ -28,41 +27,41 @@ void recordsTransportAndBatchMetrics() { observer.onNotificationOnly(false, 1); observer.onNotificationOnly(true, 2); observer.onBatchResponse(3, List.of( - JsonRpcResponse.success(IntNode.valueOf(1), StringNode.valueOf("ok")), - JsonRpcResponse.error(IntNode.valueOf(2), -32601, "Method not found") + JsonRpcResponse.success(IntNode.valueOf(1), StringNode.valueOf("ok")), + JsonRpcResponse.error(IntNode.valueOf(2), -32601, "Method not found") )); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.transport.errors", - "reason", "parse_error" + "jsonrpc.server.transport.errors", + "reason", "parse_error" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.transport.errors", - "reason", "request_too_large" + "jsonrpc.server.transport.errors", + "reason", "request_too_large" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.transport.notifications", - "mode", "single" + "jsonrpc.server.transport.notifications", + "mode", "single" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.transport.notifications", - "mode", "batch" + "jsonrpc.server.transport.notifications", + "mode", "batch" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.batch.requests", - "outcome", "mixed" + "jsonrpc.server.batch.requests", + "outcome", "mixed" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.batch.entries", - "outcome", "success" + "jsonrpc.server.batch.entries", + "outcome", "success" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.batch.entries", - "outcome", "error" + "jsonrpc.server.batch.entries", + "outcome", "error" ).count()); assertEquals(1.0, meterRegistry.counter( - "jsonrpc.server.batch.entries", - "outcome", "notification" + "jsonrpc.server.batch.entries", + "outcome", "notification" ).count()); assertEquals(1L, meterRegistry.summary("jsonrpc.server.batch.size").count()); assertEquals(3.0, meterRegistry.summary("jsonrpc.server.batch.size").totalAmount()); diff --git a/jsonrpc-spring-boot-starter/src/main/java/com/limehee/jsonrpc/spring/boot/starter/JsonRpcStarterMarker.java b/jsonrpc-spring-boot-starter/src/main/java/com/limehee/jsonrpc/spring/boot/starter/JsonRpcStarterMarker.java index d619ecd..e764965 100644 --- a/jsonrpc-spring-boot-starter/src/main/java/com/limehee/jsonrpc/spring/boot/starter/JsonRpcStarterMarker.java +++ b/jsonrpc-spring-boot-starter/src/main/java/com/limehee/jsonrpc/spring/boot/starter/JsonRpcStarterMarker.java @@ -3,8 +3,8 @@ /** * Marker type exposed by the starter module. *

- * This class allows tools and tests to verify starter dependency presence without referencing - * auto-configuration internals. + * This class allows tools and tests to verify starter dependency presence without referencing auto-configuration + * internals. *

*/ public final class JsonRpcStarterMarker { diff --git a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategy.java b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategy.java index ea84d83..6627ce0 100644 --- a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategy.java +++ b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategy.java @@ -1,17 +1,16 @@ package com.limehee.jsonrpc.spring.webmvc; import com.limehee.jsonrpc.core.JsonRpcResponse; -import org.springframework.http.HttpStatus; - import java.util.List; +import org.springframework.http.HttpStatus; /** * Default HTTP status strategy for JSON-RPC over HTTP. *

- * This implementation intentionally returns {@code 200 OK} for all JSON-RPC response payloads, - * including error payloads, and returns {@code 204 NO_CONTENT} for notification-only requests. - * This behavior aligns with common JSON-RPC-over-HTTP conventions where protocol-level errors are - * represented inside the JSON-RPC response body rather than by transport status codes. + * This implementation intentionally returns {@code 200 OK} for all JSON-RPC response payloads, including error + * payloads, and returns {@code 204 NO_CONTENT} for notification-only requests. This behavior aligns with common + * JSON-RPC-over-HTTP conventions where protocol-level errors are represented inside the JSON-RPC response body rather + * than by transport status codes. *

*/ public class DefaultJsonRpcHttpStatusStrategy implements JsonRpcHttpStatusStrategy { diff --git a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcHttpStatusStrategy.java b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcHttpStatusStrategy.java index fae0518..77b09a8 100644 --- a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcHttpStatusStrategy.java +++ b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcHttpStatusStrategy.java @@ -1,16 +1,14 @@ package com.limehee.jsonrpc.spring.webmvc; import com.limehee.jsonrpc.core.JsonRpcResponse; -import org.springframework.http.HttpStatus; - import java.util.List; +import org.springframework.http.HttpStatus; /** * Resolves HTTP status codes for JSON-RPC responses generated by the WebMVC transport layer. *

- * JSON-RPC 2.0 itself is transport-agnostic and does not mandate specific HTTP status values. - * Implementations can therefore customize status selection while keeping the JSON-RPC payload - * compliant with the specification. + * JSON-RPC 2.0 itself is transport-agnostic and does not mandate specific HTTP status values. Implementations can + * therefore customize status selection while keeping the JSON-RPC payload compliant with the specification. *

*/ public interface JsonRpcHttpStatusStrategy { @@ -32,8 +30,8 @@ public interface JsonRpcHttpStatusStrategy { HttpStatus statusForBatch(List responses); /** - * Resolves the HTTP status when the request produced no JSON-RPC response payload - * (for example, notification-only requests). + * Resolves the HTTP status when the request produced no JSON-RPC response payload (for example, notification-only + * requests). * * @return HTTP status for notification-only outcomes */ diff --git a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpoint.java b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpoint.java index 7b73aca..2bf71e5 100644 --- a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpoint.java +++ b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpoint.java @@ -1,27 +1,26 @@ package com.limehee.jsonrpc.spring.webmvc; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; import com.limehee.jsonrpc.core.JsonRpcDispatchResult; import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcResponse; +import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; - -import java.util.List; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; /** * HTTP endpoint that exposes JSON-RPC 2.0 over Spring WebMVC. *

* The endpoint accepts {@code application/json} POST payloads, delegates request processing to - * {@link JsonRpcDispatcher}, and serializes protocol-compliant JSON-RPC response payloads. - * Notification-only requests return an HTTP response without a body. + * {@link JsonRpcDispatcher}, and serializes protocol-compliant JSON-RPC response payloads. Notification-only requests + * return an HTTP response without a body. *

*/ @RestController @@ -36,41 +35,41 @@ public class JsonRpcWebMvcEndpoint { /** * Creates an endpoint with a no-op observer. * - * @param dispatcher dispatcher that performs JSON-RPC parsing, validation, and invocation - * @param objectMapper mapper used to parse request payloads and serialize responses + * @param dispatcher dispatcher that performs JSON-RPC parsing, validation, and invocation + * @param objectMapper mapper used to parse request payloads and serialize responses * @param httpStatusStrategy strategy that maps JSON-RPC outcomes to HTTP status codes - * @param maxRequestBytes maximum accepted request payload size in bytes + * @param maxRequestBytes maximum accepted request payload size in bytes */ public JsonRpcWebMvcEndpoint( - JsonRpcDispatcher dispatcher, - ObjectMapper objectMapper, - JsonRpcHttpStatusStrategy httpStatusStrategy, - int maxRequestBytes + JsonRpcDispatcher dispatcher, + ObjectMapper objectMapper, + JsonRpcHttpStatusStrategy httpStatusStrategy, + int maxRequestBytes ) { this( - dispatcher, - objectMapper, - httpStatusStrategy, - maxRequestBytes, - JsonRpcWebMvcObserver.noOp() + dispatcher, + objectMapper, + httpStatusStrategy, + maxRequestBytes, + JsonRpcWebMvcObserver.noOp() ); } /** * Creates an endpoint with an explicit transport observer. * - * @param dispatcher dispatcher that performs JSON-RPC parsing, validation, and invocation - * @param objectMapper mapper used to parse request payloads and serialize responses + * @param dispatcher dispatcher that performs JSON-RPC parsing, validation, and invocation + * @param objectMapper mapper used to parse request payloads and serialize responses * @param httpStatusStrategy strategy that maps JSON-RPC outcomes to HTTP status codes - * @param maxRequestBytes maximum accepted request payload size in bytes - * @param observer observer receiving transport-level event callbacks + * @param maxRequestBytes maximum accepted request payload size in bytes + * @param observer observer receiving transport-level event callbacks */ public JsonRpcWebMvcEndpoint( - JsonRpcDispatcher dispatcher, - ObjectMapper objectMapper, - JsonRpcHttpStatusStrategy httpStatusStrategy, - int maxRequestBytes, - JsonRpcWebMvcObserver observer + JsonRpcDispatcher dispatcher, + ObjectMapper objectMapper, + JsonRpcHttpStatusStrategy httpStatusStrategy, + int maxRequestBytes, + JsonRpcWebMvcObserver observer ) { this.dispatcher = dispatcher; this.objectMapper = objectMapper; @@ -82,18 +81,18 @@ public JsonRpcWebMvcEndpoint( /** * Handles JSON-RPC HTTP requests. *

- * Parsing errors, oversized payloads, and whitespace-only payloads produce a single JSON-RPC - * error response. Notification-only handling returns an empty HTTP response with a transport - * status from {@link JsonRpcHttpStatusStrategy#statusForNotificationOnly()}. + * Parsing errors, oversized payloads, and whitespace-only payloads produce a single JSON-RPC error response. + * Notification-only handling returns an empty HTTP response with a transport status from + * {@link JsonRpcHttpStatusStrategy#statusForNotificationOnly()}. *

* * @param body raw HTTP request payload bytes; may be {@code null} when request body is absent * @return HTTP response entity containing either serialized JSON-RPC payload or empty body */ @PostMapping( - value = "${jsonrpc.path:/jsonrpc}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE + value = "${jsonrpc.path:/jsonrpc}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE ) public ResponseEntity invoke(@RequestBody(required = false) byte[] body) { if (body == null || body.length == 0) { @@ -103,9 +102,9 @@ public ResponseEntity invoke(@RequestBody(required = false) byte[] body) if (body.length > maxRequestBytes) { observer.onRequestTooLarge(body.length, maxRequestBytes); JsonRpcResponse response = JsonRpcResponse.error( - null, - JsonRpcErrorCode.INVALID_REQUEST, - "Request payload too large"); + null, + JsonRpcErrorCode.INVALID_REQUEST, + "Request payload too large"); return singleErrorResponse(response, httpStatusStrategy.statusForRequestTooLarge()); } if (isJsonWhitespaceOnly(body)) { @@ -146,7 +145,7 @@ public ResponseEntity invoke(@RequestBody(required = false) byte[] body) * Creates a single-error transport response. * * @param response JSON-RPC error response payload - * @param status HTTP status selected for that payload + * @param status HTTP status selected for that payload * @return HTTP response entity containing serialized JSON-RPC error payload */ private ResponseEntity singleErrorResponse(JsonRpcResponse response, HttpStatus status) { @@ -156,15 +155,15 @@ private ResponseEntity singleErrorResponse(JsonRpcResponse response, Htt /** * Serializes the given payload and builds an HTTP response entity. * - * @param status HTTP status to apply + * @param status HTTP status to apply * @param payload payload object to serialize as JSON * @return HTTP response with JSON content type and serialized body */ private ResponseEntity jsonResponse(HttpStatus status, Object payload) { return ResponseEntity - .status(status) - .contentType(MediaType.APPLICATION_JSON) - .body(toJson(payload)); + .status(status) + .contentType(MediaType.APPLICATION_JSON) + .body(toJson(payload)); } /** diff --git a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcObserver.java b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcObserver.java index 42e77d3..1262e38 100644 --- a/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcObserver.java +++ b/jsonrpc-spring-webmvc/src/main/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcObserver.java @@ -1,14 +1,13 @@ package com.limehee.jsonrpc.spring.webmvc; import com.limehee.jsonrpc.core.JsonRpcResponse; - import java.util.List; /** * Observer hook interface for transport-level JSON-RPC events emitted by the WebMVC endpoint. *

- * Implementations can collect metrics, auditing information, or diagnostics without changing - * dispatch behavior. All methods are optional and default to no-op. + * Implementations can collect metrics, auditing information, or diagnostics without changing dispatch behavior. All + * methods are optional and default to no-op. *

*/ public interface JsonRpcWebMvcObserver { @@ -38,7 +37,7 @@ default void onParseError() { * Called when the request payload exceeds configured transport limits. * * @param actualBytes actual body size in bytes - * @param maxBytes configured maximum accepted body size in bytes + * @param maxBytes configured maximum accepted body size in bytes */ default void onRequestTooLarge(int actualBytes, int maxBytes) { } @@ -55,7 +54,7 @@ default void onSingleResponse(JsonRpcResponse response) { * Called when a batch request produced one or more JSON-RPC responses. * * @param requestCount number of entries in the incoming batch payload - * @param responses response payload entries emitted for that batch + * @param responses response payload entries emitted for that batch */ default void onBatchResponse(int requestCount, List responses) { } @@ -63,7 +62,7 @@ default void onBatchResponse(int requestCount, List responses) /** * Called when request handling produced no JSON-RPC payload (notification-only path). * - * @param batch {@code true} when the incoming payload was a batch array + * @param batch {@code true} when the incoming payload was a batch array * @param requestCount number of requests in the incoming payload */ default void onNotificationOnly(boolean batch, int requestCount) { diff --git a/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategyTest.java b/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategyTest.java index e8bba7b..1374b03 100644 --- a/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategyTest.java +++ b/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/DefaultJsonRpcHttpStatusStrategyTest.java @@ -1,15 +1,14 @@ package com.limehee.jsonrpc.spring.webmvc; -import tools.jackson.databind.node.IntNode; -import tools.jackson.databind.node.StringNode; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcResponse; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.StringNode; class DefaultJsonRpcHttpStatusStrategyTest { @@ -27,8 +26,8 @@ void statusForSingleAlwaysReturnsOk() { @Test void statusForBatchReturnsOk() { List responses = List.of( - JsonRpcResponse.success(IntNode.valueOf(1), StringNode.valueOf("ok")), - JsonRpcResponse.error(IntNode.valueOf(2), JsonRpcErrorCode.METHOD_NOT_FOUND, "not found") + JsonRpcResponse.success(IntNode.valueOf(1), StringNode.valueOf("ok")), + JsonRpcResponse.error(IntNode.valueOf(2), JsonRpcErrorCode.METHOD_NOT_FOUND, "not found") ); assertEquals(HttpStatus.OK, strategy.statusForBatch(responses)); diff --git a/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpointTest.java b/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpointTest.java index ff5ef01..7643724 100644 --- a/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpointTest.java +++ b/jsonrpc-spring-webmvc/src/test/java/com/limehee/jsonrpc/spring/webmvc/JsonRpcWebMvcEndpointTest.java @@ -1,26 +1,25 @@ package com.limehee.jsonrpc.spring.webmvc; -import org.springframework.http.HttpStatus; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.limehee.jsonrpc.core.JsonRpcDispatcher; import com.limehee.jsonrpc.core.JsonRpcErrorCode; import com.limehee.jsonrpc.core.JsonRpcResponse; -import tools.jackson.databind.node.StringNode; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.StringNode; class JsonRpcWebMvcEndpointTest { @@ -34,10 +33,10 @@ void setUp() { dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - new DefaultJsonRpcHttpStatusStrategy(), - 1024 * 1024 + dispatcher, + OBJECT_MAPPER, + new DefaultJsonRpcHttpStatusStrategy(), + 1024 * 1024 ); mockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); @@ -46,48 +45,52 @@ void setUp() { @Test void returnsParseErrorForInvalidJson() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{")) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content("{")) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals(JsonRpcErrorCode.PARSE_ERROR, response.error().code()); } @Test void returnsParseErrorForEmptyBody() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(new byte[0])) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(new byte[0])) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals(JsonRpcErrorCode.PARSE_ERROR, response.error().code()); } @Test void returnsParseErrorForWhitespaceOnlyBody() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(" ")) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(" ")) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals(JsonRpcErrorCode.PARSE_ERROR, response.error().code()); } @Test void returnsSingleSuccessResponseForRequest() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals("pong", response.result().asString()); assertEquals(1, response.id().asInt()); } @@ -95,37 +98,37 @@ void returnsSingleSuccessResponseForRequest() throws Exception { @Test void returnsNoContentForNotification() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}")) - .andExpect(status().isNoContent()); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}")) + .andExpect(status().isNoContent()); } @Test void returnsNoContentForNotificationOnlyBatch() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - [ - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"ping"} - ] - """)) - .andExpect(status().isNoContent()); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + [ + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"ping"} + ] + """)) + .andExpect(status().isNoContent()); } @Test void returnsBatchResponseWithoutNotifications() throws Exception { MvcResult result = mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"missing","id":2} - ] - """)) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"missing","id":2} + ] + """)) + .andExpect(status().isOk()) + .andReturn(); JsonNode response = OBJECT_MAPPER.readTree(result.getResponse().getContentAsByteArray()); assertTrue(response.isArray()); @@ -139,20 +142,21 @@ void returnsInvalidRequestWhenPayloadTooLarge() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); dispatcher.register("ping", params -> StringNode.valueOf("pong")); JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - new DefaultJsonRpcHttpStatusStrategy(), - 8 + dispatcher, + OBJECT_MAPPER, + new DefaultJsonRpcHttpStatusStrategy(), + 8 ); MockMvc localMockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); MvcResult result = localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, response.error().code()); } @@ -160,29 +164,30 @@ void returnsInvalidRequestWhenPayloadTooLarge() throws Exception { void returnsInvalidRequestWhenWhitespacePayloadExceedsLimit() throws Exception { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(); JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - new DefaultJsonRpcHttpStatusStrategy(), - 2 + dispatcher, + OBJECT_MAPPER, + new DefaultJsonRpcHttpStatusStrategy(), + 2 ); MockMvc localMockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); MvcResult result = localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(" ")) - .andExpect(status().isOk()) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(" ")) + .andExpect(status().isOk()) + .andReturn(); - JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), JsonRpcResponse.class); + JsonRpcResponse response = OBJECT_MAPPER.readValue(result.getResponse().getContentAsByteArray(), + JsonRpcResponse.class); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, response.error().code()); } @Test void rejectsNonJsonContentType() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.TEXT_PLAIN) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(status().isUnsupportedMediaType()); + .contentType(MediaType.TEXT_PLAIN) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(status().isUnsupportedMediaType()); } @Test @@ -217,22 +222,22 @@ public HttpStatus statusForRequestTooLarge() { }; JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - strategy, - 8 + dispatcher, + OBJECT_MAPPER, + strategy, + 8 ); MockMvc localMockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{")) - .andExpect(status().isBadRequest()); + .contentType(MediaType.APPLICATION_JSON) + .content("{")) + .andExpect(status().isBadRequest()); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(result -> assertEquals(413, result.getResponse().getStatus())); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(result -> assertEquals(413, result.getResponse().getStatus())); } @Test @@ -241,28 +246,28 @@ void notifiesObserverForParseErrorsRequestTooLargeAndNotificationOnly() throws E dispatcher.register("ping", params -> StringNode.valueOf("pong")); RecordingObserver observer = new RecordingObserver(); JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - new DefaultJsonRpcHttpStatusStrategy(), - 64, - observer + dispatcher, + OBJECT_MAPPER, + new DefaultJsonRpcHttpStatusStrategy(), + 64, + observer ); MockMvc localMockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{")) - .andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON) + .content("{")) + .andExpect(status().isOk()); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"ping","params":{"value":"abcdefghijklmnopqrstuvwxyz"},"id":1} - """)) - .andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"ping","params":{"value":"abcdefghijklmnopqrstuvwxyz"},"id":1} + """)) + .andExpect(status().isOk()); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}")) - .andExpect(status().isNoContent()); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}")) + .andExpect(status().isNoContent()); assertEquals(1, observer.parseErrors); assertEquals(1, observer.requestTooLarge); @@ -276,28 +281,28 @@ void notifiesObserverForSingleAndBatchResponses() throws Exception { dispatcher.register("ping", params -> StringNode.valueOf("pong")); RecordingObserver observer = new RecordingObserver(); JsonRpcWebMvcEndpoint endpoint = new JsonRpcWebMvcEndpoint( - dispatcher, - OBJECT_MAPPER, - new DefaultJsonRpcHttpStatusStrategy(), - 1024 * 1024, - observer + dispatcher, + OBJECT_MAPPER, + new DefaultJsonRpcHttpStatusStrategy(), + 1024 * 1024, + observer ); MockMvc localMockMvc = MockMvcBuilders.standaloneSetup(endpoint).build(); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(status().isOk()); localMockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"missing","id":2} - ] - """)) - .andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"missing","id":2} + ] + """)) + .andExpect(status().isOk()); assertEquals(1, observer.singleResponses); assertEquals(1, observer.batchResponses); @@ -306,6 +311,7 @@ void notifiesObserverForSingleAndBatchResponses() throws Exception { } private static final class RecordingObserver implements JsonRpcWebMvcObserver { + int parseErrors; int requestTooLarge; int notificationOnly; diff --git a/samples/pure-java-demo/settings.gradle b/samples/pure-java-demo/settings.gradle index 1486c5c..9429afc 100644 --- a/samples/pure-java-demo/settings.gradle +++ b/samples/pure-java-demo/settings.gradle @@ -9,6 +9,6 @@ dependencyResolutionManagement { includeBuild("../..") { dependencySubstitution { substitute(module("io.github.limehee:jsonrpc-core")) - .using(project(":jsonrpc-core")) + .using(project(":jsonrpc-core")) } } diff --git a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExample.java b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExample.java index 7780927..2217df0 100644 --- a/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExample.java +++ b/samples/pure-java-demo/src/main/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExample.java @@ -58,14 +58,14 @@ public void onError(JsonRpcRequest request, Throwable throwable, JsonRpcError ma }; JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(recording, noisyOnError) + new InMemoryJsonRpcMethodRegistry(), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(recording, noisyOnError) ); dispatcher.register("ping", params -> StringNode.valueOf("pong")); @@ -79,5 +79,6 @@ public void onError(JsonRpcRequest request, Throwable throwable, JsonRpcError ma } public record Result(List events, JsonRpcDispatchResult dispatchResult) { + } } 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 aa2d44b..76ef3cf 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 @@ -34,24 +34,24 @@ public static void main(String[] args) throws JacksonException { JsonRpcDispatcher dispatcher = createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); print("single success", handle(dispatcher, """ - {"jsonrpc":"2.0","method":"ping","id":1} - """)); + {"jsonrpc":"2.0","method":"ping","id":1} + """)); print("notification", handle(dispatcher, """ - {"jsonrpc":"2.0","method":"ping"} - """)); + {"jsonrpc":"2.0","method":"ping"} + """)); print("mixed batch", handle(dispatcher, """ - [ - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"core"},"id":2}, - {"jsonrpc":"2.0","method":"typed.tags"}, - {"jsonrpc":"2.0","method":"missing","id":3} - ] - """)); + [ + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"core"},"id":2}, + {"jsonrpc":"2.0","method":"typed.tags"}, + {"jsonrpc":"2.0","method":"missing","id":3} + ] + """)); print("parse error", handle(dispatcher, "{")); JsonRpcDispatcher strictDispatcher = createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_REQUEST); print("strict params shape policy", handle(strictDispatcher, """ - {"jsonrpc":"2.0","method":"typed.upper","params":"invalid-shape","id":9} - """)); + {"jsonrpc":"2.0","method":"typed.upper","params":"invalid-shape","id":9} + """)); } static String handle(JsonRpcDispatcher dispatcher, String rawJson) throws JacksonException { @@ -72,25 +72,25 @@ static String handle(JsonRpcDispatcher dispatcher, String rawJson) throws Jackso static JsonRpcDispatcher createDispatcher(JsonRpcParamsTypeViolationCodePolicy policy) { JsonRpcDispatcher dispatcher = new JsonRpcDispatcher( - new InMemoryJsonRpcMethodRegistry(JsonRpcMethodRegistrationConflictPolicy.REJECT), - new DefaultJsonRpcRequestParser(), - new DefaultJsonRpcRequestValidator(policy), - new DefaultJsonRpcMethodInvoker(), - new DefaultJsonRpcExceptionResolver(false), - new DefaultJsonRpcResponseComposer(), - 100, - List.of(), - new DirectJsonRpcNotificationExecutor() + new InMemoryJsonRpcMethodRegistry(JsonRpcMethodRegistrationConflictPolicy.REJECT), + new DefaultJsonRpcRequestParser(), + new DefaultJsonRpcRequestValidator(policy), + new DefaultJsonRpcMethodInvoker(), + new DefaultJsonRpcExceptionResolver(false), + new DefaultJsonRpcResponseComposer(), + 100, + List.of(), + new DirectJsonRpcNotificationExecutor() ); JsonRpcTypedMethodHandlerFactory typedFactory = new DefaultJsonRpcTypedMethodHandlerFactory( - new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), - new JacksonJsonRpcResultWriter(OBJECT_MAPPER) + new JacksonJsonRpcParameterBinder(OBJECT_MAPPER), + new JacksonJsonRpcResultWriter(OBJECT_MAPPER) ); dispatcher.register("ping", params -> StringNode.valueOf("pong")); dispatcher.register("typed.upper", typedFactory.unary(UpperInput.class, - input -> new UpperOutput(input.value() == null ? "" : input.value().toUpperCase()))); + input -> new UpperOutput(input.value() == null ? "" : input.value().toUpperCase()))); dispatcher.register("typed.tags", typedFactory.noParams(() -> List.of("alpha", "beta"))); return dispatcher; } @@ -102,8 +102,10 @@ private static void print(String title, String payload) { } public record UpperInput(String value) { + } public record UpperOutput(String value) { + } } 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 fd32311..d144e48 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 @@ -49,5 +49,6 @@ public Result inspect(String rawMessage) throws JacksonException { } public record Result(JsonRpcEnvelopeType envelopeType, List responses) { + } } diff --git a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExampleTest.java b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExampleTest.java index 76ccde8..157b6c3 100644 --- a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExampleTest.java +++ b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/InterceptorFlowExampleTest.java @@ -10,8 +10,8 @@ class InterceptorFlowExampleTest { @Test void recordsExpectedInterceptorOrderForSuccess() throws Exception { InterceptorFlowExample.Result result = InterceptorFlowExample.execute(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals(3, result.events().size()); assertEquals("beforeValidate", result.events().get(0)); @@ -23,8 +23,8 @@ void recordsExpectedInterceptorOrderForSuccess() throws Exception { @Test void recordsOnErrorAndKeepsResponseWhenHandlerFails() throws Exception { InterceptorFlowExample.Result result = InterceptorFlowExample.execute(""" - {"jsonrpc":"2.0","method":"explode","id":2} - """); + {"jsonrpc":"2.0","method":"explode","id":2} + """); assertTrue(result.events().contains("onError:-32603")); assertEquals(-32603, result.dispatchResult().singleResponse().orElseThrow().error().code()); @@ -33,8 +33,8 @@ void recordsOnErrorAndKeepsResponseWhenHandlerFails() throws Exception { @Test void recordsOnErrorForMethodResolutionFailure() throws Exception { InterceptorFlowExample.Result result = InterceptorFlowExample.execute(""" - {"jsonrpc":"2.0","method":"missing","id":3} - """); + {"jsonrpc":"2.0","method":"missing","id":3} + """); assertTrue(result.events().contains("onError:-32601")); assertEquals(-32601, result.dispatchResult().singleResponse().orElseThrow().error().code()); diff --git a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplicationTest.java b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplicationTest.java index 150f04c..636ded4 100644 --- a/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplicationTest.java +++ b/samples/pure-java-demo/src/test/java/com/limehee/jsonrpc/sample/purejava/PureJavaDemoApplicationTest.java @@ -17,11 +17,12 @@ class PureJavaDemoApplicationTest { @Test void returnsExpectedResultForSingleRequest() throws JacksonException { - JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); + JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher( + JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); JsonNode response = parse(PureJavaDemoApplication.handle(dispatcher, """ - {"jsonrpc":"2.0","method":"ping","id":1} - """)); + {"jsonrpc":"2.0","method":"ping","id":1} + """)); assertEquals("pong", response.get("result").asString()); assertEquals(1, response.get("id").asInt()); @@ -29,26 +30,28 @@ void returnsExpectedResultForSingleRequest() throws JacksonException { @Test void returnsNoBodyForNotificationRequest() throws JacksonException { - JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); + JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher( + JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); String responseBody = PureJavaDemoApplication.handle(dispatcher, """ - {"jsonrpc":"2.0","method":"ping"} - """); + {"jsonrpc":"2.0","method":"ping"} + """); assertTrue(responseBody.isEmpty()); } @Test void returnsBatchWithOnlyNonNotificationResponses() throws JacksonException { - JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); + JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher( + JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); JsonNode batch = parse(PureJavaDemoApplication.handle(dispatcher, """ - [ - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"demo"},"id":1}, - {"jsonrpc":"2.0","method":"typed.tags"}, - {"jsonrpc":"2.0","method":"missing","id":2} - ] - """)); + [ + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"demo"},"id":1}, + {"jsonrpc":"2.0","method":"typed.tags"}, + {"jsonrpc":"2.0","method":"missing","id":2} + ] + """)); assertTrue(batch.isArray()); assertEquals(2, batch.size()); @@ -58,7 +61,8 @@ void returnsBatchWithOnlyNonNotificationResponses() throws JacksonException { @Test void returnsParseErrorForInvalidJson() throws JacksonException { - JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); + JsonRpcDispatcher dispatcher = PureJavaDemoApplication.createDispatcher( + JsonRpcParamsTypeViolationCodePolicy.INVALID_PARAMS); JsonNode response = parse(PureJavaDemoApplication.handle(dispatcher, "{")); @@ -68,11 +72,12 @@ void returnsParseErrorForInvalidJson() throws JacksonException { @Test void appliesConfigurableParamsTypeViolationCodePolicy() throws JacksonException { - JsonRpcDispatcher strict = PureJavaDemoApplication.createDispatcher(JsonRpcParamsTypeViolationCodePolicy.INVALID_REQUEST); + JsonRpcDispatcher strict = PureJavaDemoApplication.createDispatcher( + JsonRpcParamsTypeViolationCodePolicy.INVALID_REQUEST); JsonNode response = parse(PureJavaDemoApplication.handle(strict, """ - {"jsonrpc":"2.0","method":"typed.upper","params":"invalid-shape","id":9} - """)); + {"jsonrpc":"2.0","method":"typed.upper","params":"invalid-shape","id":9} + """)); assertEquals(-32600, response.get("error").get("code").asInt()); } 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 d2efada..a4db4ea 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 @@ -14,12 +14,12 @@ class ResponseSideUtilitiesExampleTest { @Test void classifiesAndValidatesSingleResponse() throws Exception { ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( - JsonRpcResponseValidationOptions.defaults() + JsonRpcResponseValidationOptions.defaults() ); ResponseSideUtilitiesExample.Result result = example.inspect(""" - {"jsonrpc":"2.0","id":1,"result":"pong"} - """); + {"jsonrpc":"2.0","id":1,"result":"pong"} + """); assertEquals(JsonRpcEnvelopeType.RESPONSE, result.envelopeType()); assertEquals(1, result.responses().size()); @@ -29,12 +29,12 @@ void classifiesAndValidatesSingleResponse() throws Exception { @Test void classifiesRequestWithoutParsingAsResponse() throws Exception { ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( - JsonRpcResponseValidationOptions.defaults() + JsonRpcResponseValidationOptions.defaults() ); ResponseSideUtilitiesExample.Result result = example.inspect(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals(JsonRpcEnvelopeType.REQUEST, result.envelopeType()); assertTrue(result.responses().isEmpty()); @@ -43,15 +43,15 @@ void classifiesRequestWithoutParsingAsResponse() throws Exception { @Test void validatesBatchResponses() throws Exception { ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( - JsonRpcResponseValidationOptions.defaults() + JsonRpcResponseValidationOptions.defaults() ); ResponseSideUtilitiesExample.Result result = example.inspect(""" - [ - {"jsonrpc":"2.0","id":1,"result":"one"}, - {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"Method not found"}} - ] - """); + [ + {"jsonrpc":"2.0","id":1,"result":"one"}, + {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"Method not found"}} + ] + """); assertEquals(JsonRpcEnvelopeType.RESPONSE, result.envelopeType()); assertEquals(2, result.responses().size()); @@ -62,11 +62,11 @@ void validatesBatchResponses() throws Exception { @Test void failsValidationForMalformedErrorObject() { ResponseSideUtilitiesExample example = new ResponseSideUtilitiesExample( - JsonRpcResponseValidationOptions.defaults() + JsonRpcResponseValidationOptions.defaults() ); assertThrows(JsonRpcException.class, () -> example.inspect(""" - {"jsonrpc":"2.0","id":1,"error":{"code":"bad","message":1}} - """)); + {"jsonrpc":"2.0","id":1,"error":{"code":"bad","message":1}} + """)); } } diff --git a/samples/spring-boot-demo/settings.gradle b/samples/spring-boot-demo/settings.gradle index 33edfee..4c61d23 100644 --- a/samples/spring-boot-demo/settings.gradle +++ b/samples/spring-boot-demo/settings.gradle @@ -14,6 +14,6 @@ dependencyResolutionManagement { includeBuild("../..") { dependencySubstitution { substitute(module("io.github.limehee:jsonrpc-spring-boot-starter")) - .using(project(":jsonrpc-spring-boot-starter")) + .using(project(":jsonrpc-spring-boot-starter")) } } diff --git a/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/GreetingRpcService.java b/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/GreetingRpcService.java index 70fa2c8..e31e879 100644 --- a/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/GreetingRpcService.java +++ b/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/GreetingRpcService.java @@ -23,5 +23,6 @@ public int sum(@JsonRpcParam("left") int left, @JsonRpcParam("right") int right) } public record GreetParams(String name) { + } } diff --git a/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/SampleRegistrationConfig.java b/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/SampleRegistrationConfig.java index ef7dc1a..0e7bc43 100644 --- a/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/SampleRegistrationConfig.java +++ b/samples/spring-boot-demo/src/main/java/com/limehee/jsonrpc/sample/SampleRegistrationConfig.java @@ -19,22 +19,24 @@ JsonRpcMethodRegistration manualEchoRegistration() { @Bean JsonRpcMethodRegistration typedUpperRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of( - "typed.upper", - factory.unary(UpperInput.class, input -> new UpperOutput(input.value().toUpperCase())) + "typed.upper", + factory.unary(UpperInput.class, input -> new UpperOutput(input.value().toUpperCase())) ); } @Bean JsonRpcMethodRegistration typedTagsRegistration(JsonRpcTypedMethodHandlerFactory factory) { return JsonRpcMethodRegistration.of( - "typed.tags", - factory.noParams(() -> List.of("alpha", "beta")) + "typed.tags", + factory.noParams(() -> List.of("alpha", "beta")) ); } record UpperInput(String value) { + } record UpperOutput(String value) { + } } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/AbstractJsonRpcIntegrationSupport.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/AbstractJsonRpcIntegrationSupport.java index 88cb0cb..af862de 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/AbstractJsonRpcIntegrationSupport.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/AbstractJsonRpcIntegrationSupport.java @@ -34,10 +34,10 @@ protected JsonNode invokeJsonRpc(String requestJson) throws Exception { protected JsonNode invokeJsonRpc(String path, String requestJson, int expectedStatus) throws Exception { MvcResult result = mockMvc.perform(post(path) - .contentType(MediaType.APPLICATION_JSON) - .content(requestJson)) - .andExpect(status().is(expectedStatus)) - .andReturn(); + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().is(expectedStatus)) + .andReturn(); byte[] responseBody = result.getResponse().getContentAsByteArray(); if (responseBody.length == 0) { diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAccessPrecedenceIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAccessPrecedenceIntegrationTest.java index ec34e43..4cd9b3b 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAccessPrecedenceIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAccessPrecedenceIntegrationTest.java @@ -8,16 +8,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest(properties = { - "jsonrpc.method-allowlist[0]=ping", - "jsonrpc.method-denylist[0]=ping" + "jsonrpc.method-allowlist[0]=ping", + "jsonrpc.method-denylist[0]=ping" }) class GreetingRpcServiceAccessPrecedenceIntegrationTest extends AbstractJsonRpcIntegrationSupport { @Test void denylistTakesPrecedenceOverAllowlist() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals(JsonRpcErrorCode.METHOD_NOT_FOUND, body.get("error").get("code").asInt()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAllowlistIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAllowlistIntegrationTest.java index 254fd4f..f47bea0 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAllowlistIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceAllowlistIntegrationTest.java @@ -8,15 +8,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest(properties = { - "jsonrpc.method-allowlist[0]=ping" + "jsonrpc.method-allowlist[0]=ping" }) class GreetingRpcServiceAllowlistIntegrationTest extends AbstractJsonRpcIntegrationSupport { @Test void allowlistPermitsConfiguredMethod() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals("pong", body.get("result").asString()); } @@ -24,8 +24,8 @@ void allowlistPermitsConfiguredMethod() throws Exception { @Test void allowlistBlocksMethodsOutsideAllowlist() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} - """); + {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} + """); assertEquals(JsonRpcErrorCode.METHOD_NOT_FOUND, body.get("error").get("code").asInt()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceConflictPolicyIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceConflictPolicyIntegrationTest.java index 364fa87..615cb0c 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceConflictPolicyIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceConflictPolicyIntegrationTest.java @@ -38,8 +38,8 @@ void usesLastRegistrationWhenConflictPolicyIsReplace() throws Exception { try (ConfigurableApplicationContext context = runContext("REPLACE")) { JsonRpcDispatcher dispatcher = context.getBean(JsonRpcDispatcher.class); JsonRpcDispatchResult result = dispatcher.dispatch(OBJECT_MAPPER.readTree(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """)); + {"jsonrpc":"2.0","method":"ping","id":1} + """)); assertEquals("pong", result.singleResponse().orElseThrow().result().asString()); } @@ -47,11 +47,11 @@ void usesLastRegistrationWhenConflictPolicyIsReplace() throws Exception { private ConfigurableApplicationContext runContext(String conflictPolicy) { return new SpringApplicationBuilder(ConflictPolicyTestApplication.class) - .properties( - "spring.main.web-application-type=none", - "jsonrpc.method-registration-conflict-policy=" + conflictPolicy - ) - .run(); + .properties( + "spring.main.web-application-type=none", + "jsonrpc.method-registration-conflict-policy=" + conflictPolicy + ) + .run(); } private Throwable rootCause(Throwable throwable) { @@ -66,6 +66,7 @@ private Throwable rootCause(Throwable throwable) { @EnableAutoConfiguration @ComponentScan(basePackageClasses = GreetingRpcService.class) static class ConflictPolicyTestApplication { + @Bean JsonRpcMethodRegistration conflictingPingRegistration() { return JsonRpcMethodRegistration.of("ping", params -> StringNode.valueOf("manual-ping")); diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomExceptionResolverIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomExceptionResolverIntegrationTest.java index 98a962d..ac0a221 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomExceptionResolverIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomExceptionResolverIntegrationTest.java @@ -19,8 +19,8 @@ class GreetingRpcServiceCustomExceptionResolverIntegrationTest extends AbstractJ @Test void mapsDomainExceptionWithCustomResolver() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"domain.fail","id":91} - """); + {"jsonrpc":"2.0","method":"domain.fail","id":91} + """); assertEquals(-32051, body.get("error").get("code").asInt()); assertEquals("custom-domain-error", body.get("error").get("message").asString()); @@ -28,6 +28,7 @@ void mapsDomainExceptionWithCustomResolver() throws Exception { @TestConfiguration(proxyBeanMethods = false) static class CustomResolverConfig { + @Bean JsonRpcExceptionResolver jsonRpcExceptionResolver() { return throwable -> { diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomPathIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomPathIntegrationTest.java index 864b833..6b32042 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomPathIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceCustomPathIntegrationTest.java @@ -10,15 +10,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(properties = { - "jsonrpc.path=/rpc" + "jsonrpc.path=/rpc" }) class GreetingRpcServiceCustomPathIntegrationTest extends AbstractJsonRpcIntegrationSupport { @Test void servesJsonRpcOnConfiguredPath() throws Exception { JsonNode body = invokeJsonRpc("/rpc", """ - {"jsonrpc":"2.0","method":"ping","id":1} - """, 200); + {"jsonrpc":"2.0","method":"ping","id":1} + """, 200); assertEquals("pong", body.get("result").asString()); } @@ -26,10 +26,10 @@ void servesJsonRpcOnConfiguredPath() throws Exception { @Test void defaultPathIsNotMappedWhenCustomPathConfigured() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """)) - .andExpect(status().isNotFound()); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"ping","id":1} + """)) + .andExpect(status().isNotFound()); } } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDefaultParamsPolicyIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDefaultParamsPolicyIntegrationTest.java index d781052..826de88 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDefaultParamsPolicyIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDefaultParamsPolicyIntegrationTest.java @@ -13,8 +13,8 @@ class GreetingRpcServiceDefaultParamsPolicyIntegrationTest extends AbstractJsonR @Test void mapsParamsTypeViolationToInvalidParamsByDefault() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":42} - """); + {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":42} + """); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, body.get("error").get("code").asInt()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDenylistIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDenylistIntegrationTest.java index 3cd2f55..0c53c0e 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDenylistIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceDenylistIntegrationTest.java @@ -8,15 +8,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest(properties = { - "jsonrpc.method-denylist[0]=ping" + "jsonrpc.method-denylist[0]=ping" }) class GreetingRpcServiceDenylistIntegrationTest extends AbstractJsonRpcIntegrationSupport { @Test void denylistBlocksConfiguredMethod() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals(JsonRpcErrorCode.METHOD_NOT_FOUND, body.get("error").get("code").asInt()); } @@ -24,8 +24,8 @@ void denylistBlocksConfiguredMethod() throws Exception { @Test void denylistStillAllowsOtherMethods() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} - """); + {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} + """); assertEquals("hello developer", body.get("result").asString()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceErrorDataExposureIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceErrorDataExposureIntegrationTest.java index e6837c9..ed57c45 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceErrorDataExposureIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceErrorDataExposureIntegrationTest.java @@ -19,8 +19,8 @@ class GreetingRpcServiceErrorDataExposureIntegrationTest extends AbstractJsonRpc @Test void includesErrorDataWhenConfigured() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"boom.with-data","id":81} - """); + {"jsonrpc":"2.0","method":"boom.with-data","id":81} + """); assertEquals(-32011, body.get("error").get("code").asInt()); assertEquals("sensitive-context", body.get("error").get("data").asString()); @@ -28,6 +28,7 @@ void includesErrorDataWhenConfigured() throws Exception { @TestConfiguration(proxyBeanMethods = false) static class BoomMethodConfig { + @Bean JsonRpcMethodRegistration boomWithDataMethod() { return JsonRpcMethodRegistration.of("boom.with-data", params -> { diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceExplicitInvalidParamsPolicyIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceExplicitInvalidParamsPolicyIntegrationTest.java index e610944..fdd4772 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceExplicitInvalidParamsPolicyIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceExplicitInvalidParamsPolicyIntegrationTest.java @@ -13,8 +13,8 @@ class GreetingRpcServiceExplicitInvalidParamsPolicyIntegrationTest extends Abstr @Test void mapsParamsTypeViolationToInvalidParamsWhenConfigured() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":43} - """); + {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":43} + """); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, body.get("error").get("code").asInt()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceIntegrationTest.java index 87f03cb..9857e56 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceIntegrationTest.java @@ -40,14 +40,14 @@ void registersGreetingRpcServiceBeanAndAnnotatedMethods() throws Exception { assertNotNull(greetingRpcService); JsonRpcResponse ping = dispatchSingle(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); JsonRpcResponse greet = dispatchSingle(""" - {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} - """); + {"jsonrpc":"2.0","method":"greet","params":{"name":"developer"},"id":2} + """); JsonRpcResponse sum = dispatchSingle(""" - {"jsonrpc":"2.0","method":"sum","params":{"left":2,"right":3},"id":3} - """); + {"jsonrpc":"2.0","method":"sum","params":{"left":2,"right":3},"id":3} + """); assertEquals("pong", ping.result().asString()); assertEquals("hello developer", greet.result().asString()); @@ -57,8 +57,8 @@ void registersGreetingRpcServiceBeanAndAnnotatedMethods() throws Exception { @Test void returnsExpectedSuccessJsonForPingRequest() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":10} - """); + {"jsonrpc":"2.0","method":"ping","id":10} + """); assertEquals("2.0", body.get("jsonrpc").asString()); assertEquals(10, body.get("id").asInt()); @@ -69,11 +69,11 @@ void returnsExpectedSuccessJsonForPingRequest() throws Exception { @Test void bindsObjectAndNamedParamsAndReturnsExpectedJson() throws Exception { JsonNode greetBody = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"greet","params":{"name":"spring"},"id":11} - """); + {"jsonrpc":"2.0","method":"greet","params":{"name":"spring"},"id":11} + """); JsonNode sumBody = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":{"left":7,"right":5},"id":12} - """); + {"jsonrpc":"2.0","method":"sum","params":{"left":7,"right":5},"id":12} + """); assertEquals("hello spring", greetBody.get("result").asString()); assertEquals(12, sumBody.get("result").asInt()); @@ -82,8 +82,8 @@ void bindsObjectAndNamedParamsAndReturnsExpectedJson() throws Exception { @Test void returnsJsonRpcErrorForUnknownMethod() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"unknown","id":99} - """); + {"jsonrpc":"2.0","method":"unknown","id":99} + """); assertEquals("2.0", body.get("jsonrpc").asString()); assertEquals(99, body.get("id").asInt()); @@ -104,8 +104,8 @@ void returnsParseErrorForMalformedJson() throws Exception { @Test void returnsInvalidRequestForRequestShapeError() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","params":[]} - """); + {"jsonrpc":"2.0","params":[]} + """); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, body.get("error").get("code").asInt()); assertTrue(body.get("id").isNull()); @@ -114,8 +114,8 @@ void returnsInvalidRequestForRequestShapeError() throws Exception { @Test void returnsInvalidParamsForBindingMismatch() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":{"left":2},"id":15} - """); + {"jsonrpc":"2.0","method":"sum","params":{"left":2},"id":15} + """); assertEquals(15, body.get("id").asInt()); assertEquals(JsonRpcErrorCode.INVALID_PARAMS, body.get("error").get("code").asInt()); @@ -124,8 +124,8 @@ void returnsInvalidParamsForBindingMismatch() throws Exception { @Test void returnsNullIdWhenIdTypeIsInvalid() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":{"nested":1}} - """); + {"jsonrpc":"2.0","method":"ping","id":{"nested":1}} + """); assertTrue(body.get("id").isNull()); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, body.get("error").get("code").asInt()); @@ -134,13 +134,13 @@ void returnsNullIdWhenIdTypeIsInvalid() throws Exception { @Test void returnsBatchResponseForMixedBatch() throws Exception { JsonNode body = invokeJsonRpc(""" - [ - {"jsonrpc":"2.0","method":"ping","id":1}, - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"unknown","id":2}, - 1 - ] - """); + [ + {"jsonrpc":"2.0","method":"ping","id":1}, + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"unknown","id":2}, + 1 + ] + """); assertTrue(body.isArray()); assertEquals(3, body.size()); @@ -162,41 +162,41 @@ void returnsInvalidRequestForEmptyBatch() throws Exception { @Test void returnsNoContentForNotificationOnlyBatch() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - [ - {"jsonrpc":"2.0","method":"ping"}, - {"jsonrpc":"2.0","method":"ping"} - ] - """)) - .andExpect(status().isNoContent()) - .andExpect(content().string("")); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + [ + {"jsonrpc":"2.0","method":"ping"}, + {"jsonrpc":"2.0","method":"ping"} + ] + """)) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); } @Test void returnsNoContentForNotificationRequest() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"ping"} - """)) - .andExpect(status().isNoContent()) - .andExpect(content().string("")); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"ping"} + """)) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); } @Test void rejectsUnsupportedMediaType() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.TEXT_PLAIN) - .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) - .andExpect(status().isUnsupportedMediaType()); + .contentType(MediaType.TEXT_PLAIN) + .content("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}")) + .andExpect(status().isUnsupportedMediaType()); } @Test void hidesErrorDataByDefault() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"boom","id":70} - """); + {"jsonrpc":"2.0","method":"boom","id":70} + """); assertEquals(-32001, body.get("error").get("code").asInt()); assertNull(body.get("error").get("data")); @@ -209,6 +209,7 @@ private JsonRpcResponse dispatchSingle(String requestJson) throws Exception { @TestConfiguration(proxyBeanMethods = false) static class BoomMethodConfig { + @Bean JsonRpcMethodRegistration boomMethod() { return JsonRpcMethodRegistration.of("boom", params -> { diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorConfigurationFailureTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorConfigurationFailureTest.java index 3aec2eb..4be4de4 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorConfigurationFailureTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorConfigurationFailureTest.java @@ -12,18 +12,18 @@ class GreetingRpcServiceNotificationExecutorConfigurationFailureTest { void failsStartupWhenConfiguredNotificationExecutorBeanIsMissing() { try { new SpringApplicationBuilder(DemoApplication.class) - .properties( - "spring.main.web-application-type=none", - "jsonrpc.notification-executor-enabled=true", - "jsonrpc.notification-executor-bean-name=missingExecutor" - ) - .run() - .close(); + .properties( + "spring.main.web-application-type=none", + "jsonrpc.notification-executor-enabled=true", + "jsonrpc.notification-executor-bean-name=missingExecutor" + ) + .run() + .close(); fail("Expected startup failure"); } catch (Exception ex) { Throwable root = rootCause(ex); assertTrue(root.getMessage().contains( - "jsonrpc.notification-executor-bean-name points to missing Executor bean: missingExecutor")); + "jsonrpc.notification-executor-bean-name points to missing Executor bean: missingExecutor")); } } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorIntegrationTest.java index 11f9034..bdb6588 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceNotificationExecutorIntegrationTest.java @@ -22,8 +22,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(properties = { - "jsonrpc.notification-executor-enabled=true", - "jsonrpc.notification-executor-bean-name=sampleNotificationExecutor" + "jsonrpc.notification-executor-enabled=true", + "jsonrpc.notification-executor-bean-name=sampleNotificationExecutor" }) @Import(GreetingRpcServiceNotificationExecutorIntegrationTest.NotificationExecutorTestConfig.class) class GreetingRpcServiceNotificationExecutorIntegrationTest extends AbstractJsonRpcIntegrationSupport { @@ -34,12 +34,12 @@ class GreetingRpcServiceNotificationExecutorIntegrationTest extends AbstractJson @Test void usesConfiguredExecutorForNotificationRequests() throws Exception { mockMvc.perform(post("/jsonrpc") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"jsonrpc":"2.0","method":"notify.mark"} - """)) - .andExpect(status().isNoContent()) - .andExpect(content().string("")); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"jsonrpc":"2.0","method":"notify.mark"} + """)) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); assertTrue(probe.latch.await(1, TimeUnit.SECONDS)); assertEquals(1, probe.executorDispatchCount.get()); @@ -47,6 +47,7 @@ void usesConfiguredExecutorForNotificationRequests() throws Exception { } static final class NotificationProbe { + private final AtomicInteger executorDispatchCount = new AtomicInteger(); private final AtomicInteger handlerInvocationCount = new AtomicInteger(); private final CountDownLatch latch = new CountDownLatch(1); @@ -54,6 +55,7 @@ static final class NotificationProbe { @TestConfiguration(proxyBeanMethods = false) static class NotificationExecutorTestConfig { + @Bean("sampleNotificationExecutor") Executor sampleNotificationExecutor(NotificationProbe probe) { return command -> { diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceParamsPolicyIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceParamsPolicyIntegrationTest.java index 93776c5..db84554 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceParamsPolicyIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceParamsPolicyIntegrationTest.java @@ -13,8 +13,8 @@ class GreetingRpcServiceParamsPolicyIntegrationTest extends AbstractJsonRpcInteg @Test void mapsParamsTypeViolationToInvalidRequestWhenConfigured() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":41} - """); + {"jsonrpc":"2.0","method":"sum","params":"invalid-shape","id":41} + """); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, body.get("error").get("code").asInt()); } diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceRequestLimitIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceRequestLimitIntegrationTest.java index a2f8ef4..fb8f536 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceRequestLimitIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceRequestLimitIntegrationTest.java @@ -9,15 +9,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(properties = { - "jsonrpc.max-request-bytes=8" + "jsonrpc.max-request-bytes=8" }) class GreetingRpcServiceRequestLimitIntegrationTest extends AbstractJsonRpcIntegrationSupport { @Test void returnsInvalidRequestWhenPayloadIsTooLarge() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"ping","id":1} - """); + {"jsonrpc":"2.0","method":"ping","id":1} + """); assertEquals(JsonRpcErrorCode.INVALID_REQUEST, body.get("error").get("code").asInt()); assertTrue(body.get("id").isNull()); diff --git a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceScenarioCoverageIntegrationTest.java b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceScenarioCoverageIntegrationTest.java index 32bf266..2239eed 100644 --- a/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceScenarioCoverageIntegrationTest.java +++ b/samples/spring-boot-demo/src/test/java/com/limehee/jsonrpc/sample/GreetingRpcServiceScenarioCoverageIntegrationTest.java @@ -13,8 +13,8 @@ class GreetingRpcServiceScenarioCoverageIntegrationTest extends AbstractJsonRpcI @Test void supportsManualRegistrationScenario() throws Exception { JsonNode body = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"manual.echo","id":31} - """); + {"jsonrpc":"2.0","method":"manual.echo","id":31} + """); assertEquals("echo", body.get("result").asString()); assertEquals(31, body.get("id").asInt()); @@ -23,11 +23,11 @@ void supportsManualRegistrationScenario() throws Exception { @Test void supportsTypedRegistrationScenarios() throws Exception { JsonNode upper = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"spring"},"id":32} - """); + {"jsonrpc":"2.0","method":"typed.upper","params":{"value":"spring"},"id":32} + """); JsonNode tags = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"typed.tags","id":33} - """); + {"jsonrpc":"2.0","method":"typed.tags","id":33} + """); assertEquals("SPRING", upper.get("result").get("value").asString()); assertTrue(tags.get("result").isArray()); @@ -38,15 +38,15 @@ void supportsTypedRegistrationScenarios() throws Exception { @Test void supportsPositionalParamsAndMixedBatchFlow() throws Exception { JsonNode sum = invokeJsonRpc(""" - {"jsonrpc":"2.0","method":"sum","params":[4,5],"id":34} - """); + {"jsonrpc":"2.0","method":"sum","params":[4,5],"id":34} + """); JsonNode batch = invokeJsonRpc(""" - [ - {"jsonrpc":"2.0","method":"manual.echo","id":35}, - {"jsonrpc":"2.0","method":"typed.tags"}, - {"jsonrpc":"2.0","method":"missing","id":36} - ] - """); + [ + {"jsonrpc":"2.0","method":"manual.echo","id":35}, + {"jsonrpc":"2.0","method":"typed.tags"}, + {"jsonrpc":"2.0","method":"missing","id":36} + ] + """); assertEquals(9, sum.get("result").asInt()); assertTrue(batch.isArray());