Skip to content

Commit 60e2a4c

Browse files
committed
- replace next action, by a proper chain pattern
1 parent 2abc32d commit 60e2a4c

6 files changed

Lines changed: 137 additions & 62 deletions

File tree

modules/jooby-jsonrpc/src/main/java/io/jooby/internal/jsonrpc/JsonRpcExecutor.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Map;
99
import java.util.Optional;
1010

11+
import org.jspecify.annotations.NonNull;
1112
import org.slf4j.Logger;
1213

1314
import io.jooby.Context;
@@ -17,7 +18,8 @@
1718
/**
1819
* The internal execution engine and "final invoker" for JSON-RPC requests.
1920
*
20-
* <p>This class is responsible for the final stages of the JSON-RPC lifecycle:
21+
* <p>This class acts as the terminal end of the {@link JsonRpcChain}. It is responsible for the
22+
* final stages of the JSON-RPC lifecycle:
2123
*
2224
* <ul>
2325
* <li>Validating the parsed request envelope.
@@ -27,10 +29,8 @@
2729
* compliant {@link JsonRpcResponse} objects.
2830
* </ul>
2931
*/
30-
public class JsonRpcExecutor implements SneakyThrows.Supplier<Optional<JsonRpcResponse>> {
32+
public class JsonRpcExecutor implements JsonRpcChain {
3133
private final Map<String, JsonRpcService> services;
32-
private final Context ctx;
33-
private final JsonRpcRequest request;
3434
private final Map<Class<?>, Logger> loggers;
3535
private final JsonRpcExceptionTranslator exceptionTranslator;
3636
private final Exception parseError;
@@ -40,25 +40,19 @@ public class JsonRpcExecutor implements SneakyThrows.Supplier<Optional<JsonRpcRe
4040
*
4141
* @param loggers A map of loggers keyed by service class.
4242
* @param services A map of registered JSON-RPC services keyed by method name.
43-
* @param ctx The current HTTP context.
4443
* @param exceptionTranslator The translator used to map standard Throwables to JSON-RPC error
4544
* codes.
46-
* @param request The incoming JSON-RPC request.
4745
* @param parseError Any exception that occurred during the initial JSON parsing phase.
4846
*/
4947
public JsonRpcExecutor(
5048
Map<Class<?>, Logger> loggers,
5149
Map<String, JsonRpcService> services,
52-
Context ctx,
5350
JsonRpcExceptionTranslator exceptionTranslator,
54-
JsonRpcRequest request,
5551
Exception parseError) {
5652
this.services = services;
57-
this.ctx = ctx;
53+
this.loggers = loggers;
5854
this.exceptionTranslator = exceptionTranslator;
59-
this.request = request;
6055
this.parseError = parseError;
61-
this.loggers = loggers;
6256
}
6357

6458
/**
@@ -68,13 +62,14 @@ public JsonRpcExecutor(
6862
* response generation. It will return {@link Optional#empty()} for Notifications, unless a
6963
* fundamental Parse Error or Invalid Request error occurs, which always require a response.
7064
*
65+
* @param ctx The current HTTP context passed down the chain.
66+
* @param request The incoming JSON-RPC request passed down the chain.
7167
* @return An Optional containing the JSON-RPC response, or empty if the request was a valid
7268
* Notification.
73-
* @throws Exception Only thrown if a fatal JVM error occurs (e.g., OutOfMemoryError) that cannot
74-
* be recovered.
7569
*/
7670
@Override
77-
public Optional<JsonRpcResponse> tryGet() throws Exception {
71+
public @NonNull Optional<JsonRpcResponse> proceed(
72+
@NonNull Context ctx, @NonNull JsonRpcRequest request) {
7873
var log = loggers.get(JsonRpcService.class);
7974
try {
8075
if (parseError != null) {

modules/jooby-jsonrpc/src/main/java/io/jooby/internal/jsonrpc/JsonRpcHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ public JsonRpcHandler(
7070
}
7171

7272
var responses = new ArrayList<JsonRpcResponse>();
73+
var executor = new JsonRpcExecutor(loggers, services, exceptionTranslator, parseError);
7374

7475
// Look up all generated *Rpc classes registered in the service registry
7576
for (var request : input) {
76-
var target =
77-
new JsonRpcExecutor(loggers, services, ctx, exceptionTranslator, request, parseError);
78-
var response = invoker == null ? target.get() : invoker.invoke(ctx, request, target);
77+
var response =
78+
invoker == null ? executor.proceed(ctx, request) : invoker.invoke(ctx, request, executor);
7979
response.ifPresent(responses::add);
8080
}
8181

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.jsonrpc;
7+
8+
import java.util.Optional;
9+
10+
import io.jooby.Context;
11+
12+
/**
13+
* Represents the remaining execution pipeline for a JSON-RPC request.
14+
*
15+
* <p>This interface is a core component of the JSON-RPC middleware architecture (Chain of
16+
* Responsibility). When a {@link JsonRpcInvoker} executes, it is provided an instance of this
17+
* chain, which represents all subsequent middleware components and the final execution target.
18+
*
19+
* <p>A typical middleware implementation will:
20+
*
21+
* <ol>
22+
* <li>Perform pre-processing (e.g., start a timer, evaluate security constraints).
23+
* <li>Call {@link #proceed(Context, JsonRpcRequest)} to delegate execution to the next link in
24+
* the chain.
25+
* <li>Inspect or log the returned {@link JsonRpcResponse}.
26+
* <li>Return the response back up the chain.
27+
* </ol>
28+
*
29+
* <p>The terminal implementation of this chain is the internal execution engine, which guarantees
30+
* that exceptions are caught and safely translated into standard JSON-RPC error responses.
31+
* Therefore, callers of {@code proceed} generally do not need to wrap the call in a try-catch block
32+
* for application logic.
33+
*/
34+
public interface JsonRpcChain {
35+
36+
/**
37+
* Passes control to the next element in the pipeline, or executes the target JSON-RPC method if
38+
* this is the end of the chain.
39+
*
40+
* <p>Because the request and context are passed explicitly, a middleware component is free to
41+
* modify, wrap, or sanitize them before passing them down the line.
42+
*
43+
* @param ctx The current HTTP context.
44+
* @param request The parsed JSON-RPC request envelope.
45+
* @return An {@link Optional} containing the final JSON-RPC response. This will be {@link
46+
* Optional#empty()} if the request was a valid Notification (which explicitly forbids a
47+
* response).
48+
*/
49+
Optional<JsonRpcResponse> proceed(Context ctx, JsonRpcRequest request);
50+
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcInvoker.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.util.Optional;
1010

1111
import io.jooby.Context;
12-
import io.jooby.SneakyThrows;
1312

1413
/**
1514
* Interceptor or middleware for processing JSON-RPC requests.
@@ -47,22 +46,22 @@
4746
public interface JsonRpcInvoker {
4847

4948
/**
50-
* Invokes the JSON-RPC request, passing control to the next invoker in the chain or to the final
49+
* Invokes the JSON-RPC request, passing control to the next element in the chain or to the final
5150
* target method.
5251
*
5352
* <p>Because the final invoker automatically catches exceptions and converts them into error
54-
* responses, you do not need to wrap the {@code action} in a try-catch block. Instead, if your
55-
* middleware needs to react to a failure (e.g., to record an error metric), you can execute the
56-
* action and check for an error by evaluating {@code response.get().getError() != null}.
53+
* responses, you do not need to wrap the call to {@code next.proceed()} in a try-catch block.
54+
* Instead, if your middleware needs to react to a failure (e.g., to record an error metric), you
55+
* can proceed with the chain and check for an error by evaluating {@code
56+
* response.get().getError() != null}.
5757
*
5858
* @param ctx The current HTTP context.
5959
* @param request The incoming JSON-RPC request.
60-
* @param action The next step in the invocation chain (or the final method execution).
60+
* @param next The remaining execution pipeline, ending in the final method execution.
6161
* @return An {@link Optional} containing the response for a standard method call, or {@link
6262
* Optional#empty()} if the incoming request was a notification.
6363
*/
64-
Optional<JsonRpcResponse> invoke(
65-
Context ctx, JsonRpcRequest request, SneakyThrows.Supplier<Optional<JsonRpcResponse>> action);
64+
Optional<JsonRpcResponse> invoke(Context ctx, JsonRpcRequest request, JsonRpcChain next);
6665

6766
/**
6867
* Chains this invoker with another one to form a middleware pipeline.
@@ -74,7 +73,7 @@ Optional<JsonRpcResponse> invoke(
7473
*/
7574
default JsonRpcInvoker then(JsonRpcInvoker next) {
7675
Objects.requireNonNull(next, "next invoker is required");
77-
return (ctx, request, action) ->
78-
JsonRpcInvoker.this.invoke(ctx, request, () -> next.invoke(ctx, request, action));
76+
return (ctx, request, chain) ->
77+
JsonRpcInvoker.this.invoke(ctx, request, (c, r) -> next.invoke(c, r, chain));
7978
}
8079
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcModule.java

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import java.util.*;
99

1010
import org.jspecify.annotations.Nullable;
11-
import org.slf4j.Logger;
12-
import org.slf4j.LoggerFactory;
1311

1412
import io.jooby.*;
1513
import io.jooby.exception.MissingValueException;
@@ -19,54 +17,83 @@
1917
import io.jooby.jsonrpc.instrumentation.OtelJsonRcpTracing;
2018

2119
/**
22-
* Global Tier 1 Dispatcher for JSON-RPC 2.0 requests.
20+
* Jooby Extension module for integrating JSON-RPC 2.0 capabilities.
2321
*
24-
* <p>This dispatcher acts as the central entry point for all JSON-RPC traffic. It manages the
25-
* lifecycle of a request by:
22+
* <p>This module acts as the central configuration point for setting up a JSON-RPC endpoint. It
23+
* registers the target {@link JsonRpcService} instances, configures the route path, maps standard
24+
* framework exceptions to JSON-RPC error codes, and installs the underlying request handler into
25+
* the Jooby application.
26+
*
27+
* <h3>Middleware Pipeline (Invoker / Chain API)</h3>
28+
*
29+
* <p>This module allows you to configure a pipeline of interceptors using the {@link
30+
* JsonRpcInvoker} API. By adding invokers, you create a {@link JsonRpcChain} that wraps the final
31+
* method execution. This is the standard way to apply cross-cutting concerns to your RPC endpoints,
32+
* such as:
2633
*
2734
* <ul>
28-
* <li>Parsing the incoming body into a {@link JsonRpcRequest} (supporting both single and batch
29-
* shapes).
30-
* <li>Iterating through registered {@link JsonRpcService} instances to find a matching namespace.
31-
* <li>Handling <strong>Notifications</strong> (requests without an {@code id}) by suppressing
32-
* responses.
33-
* <li>Unifying batch results into a single JSON array or a single object response as per the
34-
* spec.
35+
* <li>Logging request payloads and execution times.
36+
* <li>Enforcing security and authorization rules.
37+
* <li>Gathering metrics and OpenTelemetry tracing.
3538
* </ul>
3639
*
37-
* <p>*
38-
*
39-
* <p>Usage:
40+
* <h3>Usage:</h3>
4041
*
4142
* <pre>{@code
43+
* {
4244
* install(new Jackson3Module());
43-
*
4445
* install(new JsonRpcJackson3Module());
45-
*
46-
* install(new JsonRpcModule(new MyServiceRpc_()));
47-
*
46+
* * install(new JsonRpcModule(new MyServiceRpc_())
47+
* .invoker(new MyJsonRpcMiddleware()));
48+
* }
4849
* }</pre>
4950
*
5051
* @author Edgar Espina
5152
* @since 4.0.17
5253
*/
5354
public class JsonRpcModule implements Extension {
54-
private final Logger log = LoggerFactory.getLogger(JsonRpcService.class);
5555
private final Map<String, JsonRpcService> services = new HashMap<>();
5656
private final String path;
5757
private @Nullable JsonRpcInvoker invoker;
5858
private @Nullable OtelJsonRcpTracing head;
5959

60+
/**
61+
* Creates a new JSON-RPC module at a custom HTTP path.
62+
*
63+
* @param path The HTTP path where the JSON-RPC endpoint will be mounted (e.g., {@code
64+
* "/api/rpc"}).
65+
* @param service The primary {@link JsonRpcService} containing the RPC methods to expose.
66+
* @param services Additional {@link JsonRpcService} instances to expose on the same endpoint.
67+
*/
6068
public JsonRpcModule(String path, JsonRpcService service, JsonRpcService... services) {
6169
this.path = path;
6270
registry(service);
6371
Arrays.stream(services).forEach(this::registry);
6472
}
6573

74+
/**
75+
* Creates a new JSON-RPC module mounted at the default {@code "/rpc"} HTTP path.
76+
*
77+
* @param service The primary {@link JsonRpcService} containing the RPC methods to expose.
78+
* @param services Additional {@link JsonRpcService} instances to expose on the same endpoint.
79+
*/
6680
public JsonRpcModule(JsonRpcService service, JsonRpcService... services) {
6781
this("/rpc", service, services);
6882
}
6983

84+
/**
85+
* Adds a {@link JsonRpcInvoker} middleware to the execution pipeline.
86+
*
87+
* <p>Middlewares are composed together to form a {@link JsonRpcChain}. When multiple invokers are
88+
* registered, they wrap around each other, meaning the first added invoker will execute first.
89+
*
90+
* <p><strong>Tracing Priority:</strong> If the provided invoker is an instance of {@link
91+
* OtelJsonRcpTracing}, it is automatically promoted to the absolute head of the pipeline. This
92+
* guarantees that OpenTelemetry spans encompass all other middlewares and the final execution.
93+
*
94+
* @param invoker The middleware interceptor to add to the pipeline.
95+
* @return This module instance for fluent configuration chaining.
96+
*/
7097
public JsonRpcModule invoker(JsonRpcInvoker invoker) {
7198
if (invoker instanceof OtelJsonRcpTracing otel) {
7299
// otel goes first:
@@ -88,10 +115,15 @@ private void registry(JsonRpcService service) {
88115
}
89116

90117
/**
91-
* Installs the JSON-RPC handler at the default {@code /rpc} endpoint.
118+
* Installs the JSON-RPC handler into the Jooby application.
119+
*
120+
* <p>This method is invoked automatically by Jooby during application startup. It resolves the
121+
* final middleware chain, registers the HTTP POST route at the configured path, and sets up
122+
* default exception mappings for standard Jooby routing errors (like missing or mismatched
123+
* parameters).
92124
*
93125
* @param app The Jooby application instance.
94-
* @throws Exception If registration fails.
126+
* @throws Exception If route registration or configuration fails.
95127
*/
96128
@Override
97129
public void install(Jooby app) throws Exception {

0 commit comments

Comments
 (0)