Skip to content

Commit 1a8a492

Browse files
committed
- support reactive responses on tRPC
- more code cleanup
1 parent c96182e commit 1a8a492

5 files changed

Lines changed: 128 additions & 15 deletions

File tree

modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ public boolean nonBlocking(TypeMirror returnType) {
127127
return entry != null;
128128
}
129129

130+
public ReactiveType getReactiveType(TypeMirror type) {
131+
return findMappingHandler(type);
132+
}
133+
130134
private ReactiveType findMappingHandler(TypeMirror type) {
131135
for (var e : reactiveTypeMap.entrySet()) {
132136
var that = e.getKey();

modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,33 @@ public List<String> generateHandlerCall(boolean kt) {
265265
returnTypeString = Types.PROJECTED + "<" + returnType + ">";
266266
}
267267

268-
// Add the TrpcResponse generic wrapper for the generated method return type
268+
var reactive = isTrpc ? context.getReactiveType(returnType.getRawType()) : null;
269+
var isReactiveVoid = false;
270+
var innerReactiveType = "Object";
271+
272+
// 1. Resolve Target Signature
269273
var methodReturnTypeString = returnTypeString;
270274
if (isTrpc) {
271-
methodReturnTypeString =
272-
"io.jooby.trpc.TrpcResponse<"
273-
+ (returnType.isVoid() ? (kt ? "Unit" : "Void") : returnTypeString)
274-
+ ">";
275+
if (reactive != null) {
276+
var rawReactiveType = type(kt, returnType.getRawType().toString());
277+
if (!returnType.getArguments().isEmpty()) {
278+
innerReactiveType = type(kt, returnType.getArguments().get(0).getRawType().toString());
279+
if (innerReactiveType.equals("java.lang.Void") || innerReactiveType.equals("Void")) {
280+
isReactiveVoid = true;
281+
innerReactiveType = kt ? "Unit" : "Void";
282+
}
283+
} else if (rawReactiveType.contains("Completable")) {
284+
isReactiveVoid = true;
285+
innerReactiveType = kt ? "Unit" : "Void";
286+
}
287+
methodReturnTypeString =
288+
rawReactiveType + "<io.jooby.trpc.TrpcResponse<" + innerReactiveType + ">>";
289+
} else {
290+
methodReturnTypeString =
291+
"io.jooby.trpc.TrpcResponse<"
292+
+ (returnType.isVoid() ? (kt ? "Unit" : "Void") : returnTypeString)
293+
+ ">";
294+
}
275295
}
276296

277297
var nullable =
@@ -366,6 +386,7 @@ public List<String> generateHandlerCall(boolean kt) {
366386

367387
controllerVar(kt, buffer, controllerIndent);
368388

389+
// 2. Resolve Return Flow
369390
if (returnType.isVoid()) {
370391
String statusCode =
371392
annotationMap.size() == 1
@@ -451,7 +472,6 @@ public List<String> generateHandlerCall(boolean kt) {
451472
? ""
452473
: customReturnType.getArgumentsString(kt, false, Set.of(TypeKind.TYPEVAR));
453474

454-
// Ensure projections are ignored by the kotlin hardcast, as they handle their own wrapper
455475
var needsCast =
456476
!castStr.isEmpty()
457477
|| (kt
@@ -480,7 +500,6 @@ public List<String> generateHandlerCall(boolean kt) {
480500
indent(controllerIndent),
481501
"return io.jooby.trpc.TrpcResponse.of(",
482502
projected,
483-
kt && nullable ? "!!" : "",
484503
")",
485504
semicolon(kt)));
486505
} else {
@@ -493,7 +512,62 @@ public List<String> generateHandlerCall(boolean kt) {
493512
semicolon(kt)));
494513
}
495514
} else {
496-
if (isTrpc) {
515+
516+
if (isTrpc && reactive != null) {
517+
if (isReactiveVoid) {
518+
// Ensure empty void streams systematically resolve into an empty TrpcResponse
519+
var handler = reactive.handlerType();
520+
if (handler.contains("Reactor")) {
521+
buffer.add(
522+
statement(
523+
indent(controllerIndent),
524+
"return ",
525+
call,
526+
".then(reactor.core.publisher.Mono.just(io.jooby.trpc.TrpcResponse.empty()))",
527+
semicolon(kt)));
528+
} else if (handler.contains("Mutiny")) {
529+
buffer.add(
530+
statement(
531+
indent(controllerIndent),
532+
"return ",
533+
call,
534+
".replaceWith(io.jooby.trpc.TrpcResponse.empty())",
535+
semicolon(kt)));
536+
} else if (handler.contains("ReactiveSupport")) {
537+
buffer.add(
538+
statement(
539+
indent(controllerIndent),
540+
"return ",
541+
call,
542+
".thenApply(x -> io.jooby.trpc.TrpcResponse.empty())",
543+
semicolon(kt)));
544+
} else if (handler.contains("Reactivex")) {
545+
buffer.add(
546+
statement(
547+
indent(controllerIndent),
548+
"return ",
549+
call,
550+
".toSingleDefault(io.jooby.trpc.TrpcResponse.empty())",
551+
semicolon(kt)));
552+
} else {
553+
buffer.add(
554+
statement(
555+
indent(controllerIndent),
556+
"return ",
557+
call,
558+
".map(x -> io.jooby.trpc.TrpcResponse.empty())",
559+
semicolon(kt)));
560+
}
561+
} else {
562+
buffer.add(
563+
statement(
564+
indent(controllerIndent),
565+
"return ",
566+
call,
567+
reactive.mapOperator(),
568+
semicolon(kt)));
569+
}
570+
} else if (isTrpc) {
497571
buffer.add(
498572
statement(
499573
indent(controllerIndent),

modules/jooby-apt/src/main/java/io/jooby/internal/apt/ReactiveType.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ public class ReactiveType {
1313
private final String handlerType;
1414
private final String handler;
1515
private final Set<String> reactiveTypes;
16+
private final String mapOperator;
1617

17-
private ReactiveType(String handlerType, String handler, Set<String> reactiveTypes) {
18+
private ReactiveType(
19+
String handlerType, String handler, Set<String> reactiveTypes, String mapOperator) {
1820
this.handlerType = handlerType;
1921
this.handler = handler;
2022
this.reactiveTypes = reactiveTypes;
23+
this.mapOperator = mapOperator;
2124
}
2225

2326
public Set<String> reactiveTypes() {
@@ -32,36 +35,45 @@ public String handler() {
3235
return handler;
3336
}
3437

38+
public String mapOperator() {
39+
return mapOperator;
40+
}
41+
3542
public static List<ReactiveType> supportedTypes() {
3643
return List.of(
3744
new ReactiveType(
3845
"io.jooby.ReactiveSupport",
3946
"concurrent",
40-
Set.of("java.util.concurrent.Flow", "java.util.concurrent.CompletionStage")),
47+
Set.of("java.util.concurrent.Flow", "java.util.concurrent.CompletionStage"),
48+
".thenApply(io.jooby.trpc.TrpcResponse::of)"),
4149
// Vertx
4250
new ReactiveType(
4351
"io.jooby.vertx.VertxHandler",
4452
"vertx",
45-
Set.of("io.vertx.core.Future", "io.vertx.core.Promise", "io.vertx.core.buffer.Buffer")),
53+
Set.of("io.vertx.core.Future", "io.vertx.core.Promise", "io.vertx.core.buffer.Buffer"),
54+
".map(io.jooby.trpc.TrpcResponse::of)"),
4655
// Mutiny
4756
new ReactiveType(
4857
"io.jooby.mutiny.Mutiny",
4958
"mutiny",
50-
Set.of("io.smallrye.mutiny.Uni", "io.smallrye.mutiny.Multi")),
59+
Set.of("io.smallrye.mutiny.Uni", "io.smallrye.mutiny.Multi"),
60+
".map(io.jooby.trpc.TrpcResponse::of)"),
5161
// Reactor
5262
new ReactiveType(
5363
"io.jooby.reactor.Reactor",
5464
"reactor",
55-
Set.of("reactor.core.publisher.Flux", "reactor.core.publisher.Mono")),
65+
Set.of("reactor.core.publisher.Flux", "reactor.core.publisher.Mono"),
66+
".map(io.jooby.trpc.TrpcResponse::of)"),
5667
// Rxjava
5768
new ReactiveType(
5869
"io.jooby.rxjava3.Reactivex",
5970
"rx",
6071
Set.of(
6172
"io.reactivex.rxjava3.core.Flowable",
6273
"io.reactivex.rxjava3.core.Maybe",
63-
"io.reactivex.rxjava3.core.Observable",
6474
"io.reactivex.rxjava3.core.Single",
65-
"io.reactivex.rxjava3.disposables.Disposable")));
75+
"io.reactivex.rxjava3.core.Observable",
76+
"io.reactivex.rxjava3.core.Completable"),
77+
".map(io.jooby.trpc.TrpcResponse::of)"));
6678
}
6779
}

tests/src/test/java/io/jooby/i3863/MovieService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.stream.Collectors;
1111

1212
import io.jooby.annotation.Trpc;
13+
import reactor.core.publisher.Mono;
1314

1415
@Trpc("movies")
1516
public class MovieService {
@@ -24,6 +25,13 @@ public Movie create(Movie movie) {
2425
return movie;
2526
}
2627

28+
/** Procedure: movies.create Takes a single complex object. */
29+
@Trpc.Mutation
30+
public Mono<Movie> createMono(Movie movie) {
31+
// In a real app, save to DB. For now, just return it.
32+
return Mono.just(movie);
33+
}
34+
2735
@Trpc.Mutation
2836
public void resetIndex() {}
2937

tests/src/test/java/io/jooby/i3863/TrpcProtocolTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ void assertProtocolData(WebClient http) {
101101
assertThat(JsonPath.<Integer>read(json, "$.result.data.year")).isEqualTo(1999);
102102
});
103103

104+
// reactive
105+
http.postJson(
106+
"/trpc/movies.createMono",
107+
"[{\"id\": 1, \"title\": \"The Matrix\", \"year\": 1999}]",
108+
rsp -> {
109+
String json = rsp.body().string();
110+
111+
// 4. Validating the tRPC envelope and data using JsonPath + AssertJ
112+
assertThat(rsp.code()).isEqualTo(200);
113+
114+
assertThat(JsonPath.<Integer>read(json, "$.result.data.id")).isEqualTo(1);
115+
assertThat(JsonPath.<String>read(json, "$.result.data.title")).isEqualTo("The Matrix");
116+
assertThat(JsonPath.<Integer>read(json, "$.result.data.year")).isEqualTo(1999);
117+
});
118+
104119
http.post(
105120
"/trpc/movies.resetIndex",
106121
rsp -> {

0 commit comments

Comments
 (0)