Skip to content

Commit beb4370

Browse files
committed
feat(jsonrpc): support dual generation for mixed REST and JSON-RPC controllers
- Fix an issue where mixing standard REST annotations (e.g., `@GET`, `@Path`) with `@JsonRpc` on the same controller caused the APT processor to exit early, skipping the standard MVC router generation. - Update `JoobyProcessor` to perform two distinct file generation passes, allowing a single controller to output both REST and JSON-RPC routing files simultaneously. - Introduce `hasRestRoutes()` and `hasJsonRpcRoutes()` condition checks to `MvcRouter`. - Refactor the monolithic `toSourceCode()` method into strictly isolated `getRestSourceCode()` and `getRpcSourceCode()` methods. - Ensure standard routes generate `[Name]_` files and JSON-RPC routes generate `[Name]Rpc_` files without filename or class name conflicts. - Fine-tune `CodeBlock` formatting in the JSON-RPC generator, removing manual newline hacks in favor of native block indentation.
1 parent 3b2f017 commit beb4370

File tree

8 files changed

+258
-140
lines changed

8 files changed

+258
-140
lines changed

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import io.jooby.internal.MutedServer;
3636
import io.jooby.internal.RegistryRef;
3737
import io.jooby.internal.RouterImpl;
38+
import io.jooby.jsonrpc.JsonRpcDispatcher;
39+
import io.jooby.jsonrpc.JsonRpcService;
3840
import io.jooby.output.OutputFactory;
3941
import io.jooby.problem.ProblemDetailsHandler;
4042
import io.jooby.value.ValueFactory;
@@ -102,6 +104,8 @@ public class Jooby implements Router, Registry {
102104

103105
private List<Locale> locales;
104106

107+
private Map<String, JsonRpcDispatcher> dispatchers;
108+
105109
private boolean lateInit;
106110

107111
private String name;
@@ -383,7 +387,7 @@ public String getContextPath() {
383387
* @param factory Application factory.
384388
* @return This application.
385389
*/
386-
@NonNull public Jooby install(
390+
public Jooby install(
387391
@NonNull String path,
388392
@NonNull Predicate<Context> predicate,
389393
@NonNull SneakyThrows.Supplier<Jooby> factory) {
@@ -478,8 +482,7 @@ public Route.Set mount(@NonNull Predicate<Context> predicate, @NonNull Router su
478482
@Override
479483
public Route.Set mount(@NonNull String path, @NonNull Router router) {
480484
var rs = this.router.mount(path, router);
481-
if (router instanceof Jooby) {
482-
Jooby child = (Jooby) router;
485+
if (router instanceof Jooby child) {
483486
child.registry = this.registry;
484487
}
485488
return rs;
@@ -490,19 +493,46 @@ public Route.Set mount(@NonNull Router router) {
490493
return mount("/", router);
491494
}
492495

496+
public Jooby jsonRpc(String path, @NonNull JsonRpcService service) {
497+
if (dispatchers == null) {
498+
dispatchers = new HashMap<>();
499+
}
500+
dispatchers
501+
.computeIfAbsent(
502+
Router.normalizePath(path),
503+
normalizedPath -> {
504+
var dispatcher = new JsonRpcDispatcher(normalizedPath);
505+
install(dispatcher);
506+
return dispatcher;
507+
})
508+
.add(service);
509+
return this;
510+
}
511+
512+
public Jooby jsonRpc(@NonNull JsonRpcService service) {
513+
return jsonRpc("/rpc", service);
514+
}
515+
493516
/**
494517
* Add controller routes.
495518
*
496519
* @param router Mvc extension.
497520
* @return Route set.
498521
*/
499-
@NonNull public Route.Set mvc(@NonNull Extension router) {
500-
try {
501-
int start = this.router.getRoutes().size();
502-
router.install(this);
503-
return new Route.Set(this.router.getRoutes().subList(start, this.router.getRoutes().size()));
504-
} catch (Exception cause) {
505-
throw SneakyThrows.propagate(cause);
522+
public Route.Set mvc(@NonNull Extension router) {
523+
if (router instanceof JsonRpcService jsonRpcService) {
524+
jsonRpc(jsonRpcService);
525+
// NOOP
526+
return new Route.Set(Collections.emptyList());
527+
} else {
528+
try {
529+
int start = this.router.getRoutes().size();
530+
router.install(this);
531+
return new Route.Set(
532+
this.router.getRoutes().subList(start, this.router.getRoutes().size()));
533+
} catch (Exception cause) {
534+
throw SneakyThrows.propagate(cause);
535+
}
506536
}
507537
}
508538

@@ -1455,5 +1485,6 @@ private static void copyState(Jooby source, Jooby dest) {
14551485
dest.readyCallbacks = source.readyCallbacks;
14561486
dest.startingCallbacks = source.startingCallbacks;
14571487
dest.stopCallbacks = source.stopCallbacks;
1488+
dest.dispatchers = source.dispatchers;
14581489
}
14591490
}

jooby/src/main/java/io/jooby/Router.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ static String leadingSlash(@Nullable String path) {
908908
* @param path Path to process.
909909
* @return Path without trailing slashes.
910910
*/
911-
static @NonNull String noTrailingSlash(@NonNull String path) {
911+
static String noTrailingSlash(@NonNull String path) {
912912
StringBuilder buff = new StringBuilder(path);
913913
int i = buff.length() - 1;
914914
while (i > 0 && buff.charAt(i) == '/') {
@@ -927,7 +927,7 @@ static String leadingSlash(@Nullable String path) {
927927
* @param path Path to process.
928928
* @return Safe path pattern.
929929
*/
930-
static @NonNull String normalizePath(@Nullable String path) {
930+
static String normalizePath(@Nullable String path) {
931931
if (path == null || path.length() == 0 || path.equals("/")) {
932932
return "/";
933933
}

jooby/src/main/java/io/jooby/jsonrpc/JsonRpcDispatcher.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@
4141
* @since 4.0.17
4242
*/
4343
public class JsonRpcDispatcher implements Extension {
44-
4544
private final Map<String, JsonRpcService> services = new HashMap<>();
45+
private final String path;
4646

47-
public JsonRpcDispatcher(JsonRpcService... services) {
48-
for (JsonRpcService service : services) {
49-
for (String method : service.getMethods()) {
50-
this.services.put(method, service);
51-
}
47+
public JsonRpcDispatcher(String path) {
48+
this.path = path;
49+
}
50+
51+
public void add(JsonRpcService service) {
52+
for (var method : service.getMethods()) {
53+
this.services.put(method, service);
5254
}
5355
}
5456

@@ -60,7 +62,7 @@ public JsonRpcDispatcher(JsonRpcService... services) {
6062
*/
6163
@Override
6264
public void install(Jooby app) throws Exception {
63-
app.post("/rpc", this::handle);
65+
app.post(path, this::handle);
6466
}
6567

6668
/**
@@ -74,7 +76,7 @@ public void install(Jooby app) throws Exception {
7476
* @return A single {@link JsonRpcResponse}, a {@code List} of responses for batches, or an empty
7577
* string for notifications.
7678
*/
77-
public Object handle(Context ctx) {
79+
private Object handle(Context ctx) {
7880
JsonRpcRequest input;
7981
try {
8082
input = ctx.body(JsonRpcRequest.class);

modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -129,32 +129,71 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
129129
try {
130130
if (roundEnv.processingOver()) {
131131
context.debug("Output:");
132-
context.getRouters().forEach(it -> context.debug(" %s", it.getGeneratedType()));
132+
// Print all generated types for both REST and RPC
133+
context
134+
.getRouters()
135+
.forEach(
136+
it -> {
137+
if (it.hasRestRoutes()) {
138+
context.debug(" %s", it.getRestGeneratedType());
139+
}
140+
if (it.hasJsonRpcRoutes()) {
141+
context.debug(" %s", it.getRpcGeneratedType());
142+
}
143+
});
133144
return false;
134145
} else {
135146
var routeMap = buildRouteRegistry(annotations, roundEnv);
136147
verifyBeanValidationDependency(routeMap.values());
137148
for (var router : routeMap.values()) {
138149
try {
139-
// Track the router unconditionally so JSON-RPC routes are available in processingOver
150+
// Track the router unconditionally so routes are available in processingOver
140151
context.add(router);
141152

142-
var sourceCode = router.toSourceCode(null);
143-
if (sourceCode != null) {
144-
var sourceLocation = router.getGeneratedFilename();
145-
onGeneratedSource(
146-
router.getGeneratedType(), toJavaFileObject(sourceLocation, sourceCode));
147-
context.debug("router %s: %s", router.getTargetType(), router.getGeneratedType());
148-
router.getRoutes().forEach(it -> context.debug(" %s", it));
149-
writeSource(
150-
router.isKt(),
151-
router.getGeneratedType(),
152-
sourceLocation,
153-
sourceCode,
154-
router.getTargetType());
155-
} else if (router.isJsonRpc()) {
156-
context.debug("jsonrpc router %s", router.getTargetType());
153+
// 1. Generate Standard REST/tRPC File (e.g., MovieService_.java)
154+
if (router.hasRestRoutes()) {
155+
var restSource = router.getRestSourceCode(null);
156+
if (restSource != null) {
157+
var sourceLocation = router.getRestGeneratedFilename();
158+
var generatedType = router.getRestGeneratedType();
159+
onGeneratedSource(generatedType, toJavaFileObject(sourceLocation, restSource));
160+
161+
context.debug("router %s: %s", router.getTargetType(), generatedType);
162+
router.getRoutes().stream()
163+
.filter(it -> !it.isJsonRpc())
164+
.forEach(it -> context.debug(" %s", it));
165+
166+
writeSource(
167+
router.isKt(),
168+
generatedType,
169+
sourceLocation,
170+
restSource,
171+
router.getTargetType());
172+
}
157173
}
174+
175+
// 2. Generate JSON-RPC File (e.g., MovieServiceRpc_.java)
176+
if (router.hasJsonRpcRoutes()) {
177+
var rpcSource = router.getRpcSourceCode(null);
178+
if (rpcSource != null) {
179+
var sourceLocation = router.getRpcGeneratedFilename();
180+
var generatedType = router.getRpcGeneratedType();
181+
onGeneratedSource(generatedType, toJavaFileObject(sourceLocation, rpcSource));
182+
183+
context.debug("jsonrpc router %s: %s", router.getTargetType(), generatedType);
184+
router.getRoutes().stream()
185+
.filter(MvcRoute::isJsonRpc)
186+
.forEach(it -> context.debug(" %s", it));
187+
188+
writeSource(
189+
router.isKt(),
190+
generatedType,
191+
sourceLocation,
192+
rpcSource,
193+
router.getTargetType());
194+
}
195+
}
196+
158197
} catch (IOException cause) {
159198
throw new RuntimeException("Unable to generate: " + router.getTargetType(), cause);
160199
}

0 commit comments

Comments
 (0)