Skip to content

Commit 81ad12c

Browse files
committed
build: apt: refactor: split code generation
- into: rest, jsonrpc, trpc and mcp
1 parent d5df0a1 commit 81ad12c

20 files changed

Lines changed: 3272 additions & 101 deletions

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

Lines changed: 58 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -129,87 +129,51 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
129129
try {
130130
if (roundEnv.processingOver()) {
131131
context.debug("Output:");
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-
});
132+
context.getRouters().forEach(it -> context.debug(" %s", it.getGeneratedType()));
144133
return false;
145134
} else {
146-
var routeMap = buildRouteRegistry(annotations, roundEnv);
147-
verifyBeanValidationDependency(routeMap.values());
148-
for (var router : routeMap.values()) {
135+
// 1. Discover all unique Controller classes
136+
Set<TypeElement> controllers = findControllers(annotations, roundEnv);
137+
138+
// 2. Factory Pattern: Build specific routers for each class based on method annotations
139+
List<WebRouter<?>> activeRouters = new ArrayList<>();
140+
for (TypeElement controller : controllers) {
141+
if (controller.getModifiers().contains(Modifier.ABSTRACT)) continue;
142+
143+
// These factory methods will scan the class methods and return a populated router
144+
// if it finds relevant annotations (@GET for Rest, @McpTool for MCP, etc.)
145+
// We will implement these factories inside the respective Router classes.
146+
147+
RestRouter restRouter = RestRouter.parse(context, controller);
148+
if (!restRouter.isEmpty()) activeRouters.add(restRouter);
149+
150+
JsonRpcRouter jsonRpcRouter = JsonRpcRouter.parse(context, controller);
151+
if (!jsonRpcRouter.isEmpty()) activeRouters.add(jsonRpcRouter);
152+
153+
McpRouter mcpRouter = McpRouter.parse(context, controller);
154+
if (!mcpRouter.isEmpty()) activeRouters.add(mcpRouter);
155+
156+
TrpcRouter trpcRouter = TrpcRouter.parse(context, controller);
157+
if (!trpcRouter.isEmpty()) activeRouters.add(trpcRouter);
158+
}
159+
160+
verifyBeanValidationDependency(activeRouters);
161+
162+
// 3. Generate Code Iteratively!
163+
for (WebRouter<?> router : activeRouters) {
149164
try {
150-
// Track the router unconditionally so routes are available in processingOver
151-
context.add(router);
152-
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-
}
173-
}
165+
context.add(router); // Track for processingOver output
174166

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-
// 3. Generate MCP Server File (e.g., WeatherServerMcp_.java)
197-
if (router.hasMcpRoutes()) {
198-
var mcpSource = router.getMcpSourceCode(null);
199-
if (mcpSource != null) {
200-
var sourceLocation = router.getMcpGeneratedFilename();
201-
var generatedType = router.getMcpGeneratedType();
202-
onGeneratedSource(generatedType, toJavaFileObject(sourceLocation, mcpSource));
203-
204-
context.debug("mcp router %s: %s", router.getTargetType(), generatedType);
205-
206-
writeSource(
207-
router.isKt(),
208-
generatedType,
209-
sourceLocation,
210-
mcpSource,
211-
router.getTargetType());
212-
}
167+
String sourceCode = router.getSourceCode(null);
168+
if (sourceCode != null) {
169+
String sourceLocation = router.getGeneratedFilename();
170+
String generatedType = router.getGeneratedType();
171+
172+
onGeneratedSource(generatedType, toJavaFileObject(sourceLocation, sourceCode));
173+
context.debug("router %s: %s", router.getTargetType(), generatedType);
174+
175+
writeSource(
176+
router.isKt(), generatedType, sourceLocation, sourceCode, router.getTargetType());
213177
}
214178

215179
} catch (IOException cause) {
@@ -225,6 +189,21 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
225189
}
226190
}
227191

192+
private Set<TypeElement> findControllers(
193+
Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
194+
Set<TypeElement> controllers = new LinkedHashSet<>();
195+
for (var annotation : annotations) {
196+
for (var element : roundEnv.getElementsAnnotatedWith(annotation)) {
197+
if (element instanceof TypeElement typeElement) {
198+
controllers.add(typeElement);
199+
} else if (element instanceof ExecutableElement method) {
200+
controllers.add((TypeElement) method.getEnclosingElement());
201+
}
202+
}
203+
}
204+
return controllers;
205+
}
206+
228207
private void writeSource(
229208
boolean isKt,
230209
String className,
@@ -492,8 +471,8 @@ private static <E extends Throwable> E sneakyThrow0(final Throwable x) throws E
492471
throw (E) x;
493472
}
494473

495-
private void verifyBeanValidationDependency(Collection<MvcRouter> routers) {
496-
var hasBeanValidation = routers.stream().anyMatch(MvcRouter::hasBeanValidation);
474+
private void verifyBeanValidationDependency(Collection<WebRouter<?>> routers) {
475+
var hasBeanValidation = routers.stream().anyMatch(WebRouter::hasBeanValidation);
497476
if (hasBeanValidation) {
498477
var missingDependency =
499478
Stream.of(
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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.internal.apt;
7+
8+
import static io.jooby.internal.apt.AnnotationSupport.VALUE;
9+
import static io.jooby.internal.apt.CodeBlock.*;
10+
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.StringJoiner;
14+
import java.util.function.Consumer;
15+
16+
import javax.lang.model.element.ExecutableElement;
17+
18+
public class JsonRpcRoute extends WebRoute {
19+
20+
public JsonRpcRoute(WebRouter<?> router, ExecutableElement method) {
21+
super(router, method);
22+
}
23+
24+
public String getJsonRpcMethodName() {
25+
var annotation = AnnotationSupport.findAnnotationByName(method, "io.jooby.annotation.JsonRpc");
26+
if (annotation != null) {
27+
var val =
28+
AnnotationSupport.findAnnotationValue(annotation, VALUE).stream().findFirst().orElse("");
29+
if (!val.isEmpty()) return val;
30+
}
31+
return getMethodName();
32+
}
33+
34+
public List<String> generateJsonRpcDispatchCase(boolean kt) {
35+
var buffer = new ArrayList<String>();
36+
var paramList = new StringJoiner(", ", "(", ")");
37+
38+
// Check if we have any parameters that actually need to be parsed from the JSON payload
39+
boolean needsReader =
40+
parameters.stream()
41+
.anyMatch(
42+
p -> {
43+
String type = p.getType().toString();
44+
return !type.equals("io.jooby.Context")
45+
&& !type.startsWith("kotlin.coroutines.Continuation");
46+
});
47+
48+
if (needsReader) {
49+
if (kt) {
50+
buffer.add(statement(indent(8), "parser.reader(req.params).use { reader ->"));
51+
} else {
52+
buffer.add(statement(indent(8), "try (var reader = parser.reader(req.getParams())) {"));
53+
}
54+
}
55+
56+
buffer.addAll(generateRpcParameter(kt, paramList::add));
57+
58+
int callIndent = needsReader ? 10 : 8;
59+
var call = CodeBlock.of("c.", getMethodName(), paramList.toString());
60+
61+
if (returnType.isVoid()) {
62+
buffer.add(statement(indent(callIndent), call, semicolon(kt)));
63+
buffer.add(statement(indent(callIndent), kt ? "null" : "return null", semicolon(kt)));
64+
} else {
65+
buffer.add(statement(indent(callIndent), kt ? call : "return " + call, semicolon(kt)));
66+
}
67+
68+
if (needsReader) {
69+
buffer.add(statement(indent(8), "}"));
70+
}
71+
72+
return buffer;
73+
}
74+
75+
private List<String> generateRpcParameter(boolean kt, Consumer<String> arguments) {
76+
var statements = new ArrayList<String>();
77+
var decoderInterface = "io.jooby.rpc.jsonrpc.JsonRpcDecoder";
78+
int baseIndent = 10;
79+
80+
for (var parameter : parameters) {
81+
var paramenterName = parameter.getName();
82+
var type = type(kt, parameter.getType().toString());
83+
boolean isNullable = parameter.isNullable(kt);
84+
85+
switch (parameter.getType().getRawType().toString()) {
86+
case "io.jooby.Context":
87+
arguments.accept("ctx");
88+
break;
89+
case "int",
90+
"long",
91+
"double",
92+
"boolean",
93+
"java.lang.String",
94+
"java.lang.Integer",
95+
"java.lang.Long",
96+
"java.lang.Double",
97+
"java.lang.Boolean":
98+
var simpleType = type.startsWith("java.lang.") ? type.substring(10) : type;
99+
if (simpleType.equals("Integer") || simpleType.equals("int")) simpleType = "Int";
100+
var readName =
101+
"next" + Character.toUpperCase(simpleType.charAt(0)) + simpleType.substring(1);
102+
103+
if (isNullable) {
104+
if (kt) {
105+
statements.add(
106+
statement(
107+
indent(baseIndent),
108+
"val ",
109+
paramenterName,
110+
" = if (reader.nextIsNull(",
111+
string(paramenterName),
112+
")) null else reader.",
113+
readName,
114+
"(",
115+
string(paramenterName),
116+
")"));
117+
} else {
118+
statements.add(
119+
statement(
120+
indent(baseIndent),
121+
var(kt),
122+
paramenterName,
123+
" = reader.nextIsNull(",
124+
string(paramenterName),
125+
") ? null : reader.",
126+
readName,
127+
"(",
128+
string(paramenterName),
129+
")",
130+
semicolon(kt)));
131+
}
132+
} else {
133+
statements.add(
134+
statement(
135+
indent(baseIndent),
136+
var(kt),
137+
paramenterName,
138+
" = reader.",
139+
readName,
140+
"(",
141+
string(paramenterName),
142+
")",
143+
semicolon(kt)));
144+
}
145+
arguments.accept(paramenterName);
146+
break;
147+
default:
148+
if (kt) {
149+
statements.add(
150+
statement(
151+
indent(baseIndent),
152+
"val ",
153+
paramenterName,
154+
"Decoder: ",
155+
decoderInterface,
156+
"<",
157+
type,
158+
"> = parser.decoder(",
159+
parameter.getType().toSourceCode(kt),
160+
")",
161+
semicolon(kt)));
162+
if (isNullable) {
163+
statements.add(
164+
statement(
165+
indent(baseIndent),
166+
"val ",
167+
paramenterName,
168+
" = if (reader.nextIsNull(",
169+
string(paramenterName),
170+
")) null else reader.nextObject(",
171+
string(paramenterName),
172+
", ",
173+
paramenterName,
174+
"Decoder)"));
175+
} else {
176+
statements.add(
177+
statement(
178+
indent(baseIndent),
179+
"val ",
180+
paramenterName,
181+
" = reader.nextObject(",
182+
string(paramenterName),
183+
", ",
184+
paramenterName,
185+
"Decoder)",
186+
semicolon(kt)));
187+
}
188+
} else {
189+
statements.add(
190+
statement(
191+
indent(baseIndent),
192+
decoderInterface,
193+
"<",
194+
type,
195+
"> ",
196+
paramenterName,
197+
"Decoder = parser.decoder(",
198+
parameter.getType().toSourceCode(kt),
199+
")",
200+
semicolon(kt)));
201+
if (isNullable) {
202+
statements.add(
203+
statement(
204+
indent(baseIndent),
205+
parameter.getType().toString(),
206+
" ",
207+
paramenterName,
208+
" = reader.nextIsNull(",
209+
string(paramenterName),
210+
") ? null : reader.nextObject(",
211+
string(paramenterName),
212+
", ",
213+
paramenterName,
214+
"Decoder)",
215+
semicolon(kt)));
216+
} else {
217+
statements.add(
218+
statement(
219+
indent(baseIndent),
220+
parameter.getType().toString(),
221+
" ",
222+
paramenterName,
223+
" = reader.nextObject(",
224+
string(paramenterName),
225+
", ",
226+
paramenterName,
227+
"Decoder)",
228+
semicolon(kt)));
229+
}
230+
}
231+
arguments.accept(paramenterName);
232+
break;
233+
}
234+
}
235+
return statements;
236+
}
237+
}

0 commit comments

Comments
 (0)