Skip to content

Commit ee26249

Browse files
SentryManrbygrave
andauthored
[Jex-Generator] Jsonb Type generation (#522)
* jsonb jex generation * fix json * Use jex 3.0-RC6 (with RouteBuilder change) * Use jex 3.0-RC7 (with RouteBuilder change) --------- Co-authored-by: Rob Bygrave <[email protected]>
1 parent 2a78984 commit ee26249

File tree

12 files changed

+152
-89
lines changed

12 files changed

+152
-89
lines changed

http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java

+38-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import io.avaje.http.generator.core.*;
6+
import io.avaje.http.generator.core.openapi.MediaType;
67

78
import javax.lang.model.type.TypeMirror;
89

@@ -17,22 +18,25 @@ class ControllerMethodWriter {
1718
private final Append writer;
1819
private final ControllerReader reader;
1920
private final WebMethod webMethod;
21+
private final boolean useJsonB;
2022
private final boolean instrumentContext;
2123
private final boolean isFilter;
2224

23-
ControllerMethodWriter(MethodReader method, Append writer, ControllerReader reader) {
25+
ControllerMethodWriter(
26+
MethodReader method, Append writer, ControllerReader reader, boolean useJsonB) {
2427
this.method = method;
2528
this.writer = writer;
2629
this.reader = reader;
30+
this.useJsonB = useJsonB;
2731
this.webMethod = method.webMethod();
2832
this.instrumentContext = method.instrumentContext();
2933
this.isFilter = webMethod == CoreWebMethod.FILTER;
3034
if (isFilter) {
31-
validateMethod();
35+
validateFilter();
3236
}
3337
}
3438

35-
private void validateMethod() {
39+
private void validateFilter() {
3640
if (method.params().stream().map(MethodParam::shortType).noneMatch("HttpFilter.FilterChain"::equals)) {
3741
logError(method.element(), "Filters must contain a FilterChain parameter");
3842
}
@@ -128,9 +132,7 @@ private boolean isInputStream(TypeMirror type) {
128132
}
129133

130134
private boolean producesJson() {
131-
return // useJsonB
132-
!disabledDirectWrites()
133-
&& !"byte[]".equals(method.returnType().toString())
135+
return !"byte[]".equals(method.returnType().toString())
134136
&& (method.produces() == null || method.produces().toLowerCase().contains("json"));
135137
}
136138

@@ -200,8 +202,7 @@ private void write(boolean requestScoped) {
200202
writer.append(");").eol();
201203
writer.append(" var cacheContent = contentCache.content(key);").eol();
202204
writer.append(" if (cacheContent != null) {").eol();
203-
writeContextReturn(responseMode);
204-
writer.append(" res.send(cacheContent);").eol();
205+
writeContextReturn(responseMode, "cacheContent");
205206
writer.append(" return;").eol();
206207
writer.append(" }").eol();
207208
}
@@ -251,27 +252,47 @@ private void write(boolean requestScoped) {
251252
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
252253
}
253254
writer.append(indent);
254-
writeContextReturn(responseMode);
255-
writer.append("content);").eol();
255+
writeContextReturn(responseMode, "content");
256256
} else {
257257
writer.append(indent);
258-
writeContextReturn(responseMode);
259-
writer.append("result);").eol();
258+
writeContextReturn(responseMode, "result");
259+
writer.eol();
260260
}
261261
if (includeNoContent) {
262262
writer.append(" }").eol();
263263
}
264264
}
265265
}
266266

267-
private void writeContextReturn(ResponseMode responseMode) {
267+
private void writeContextReturn(ResponseMode responseMode, String resultVariable) {
268+
final UType type = UType.parse(method.returnType());
269+
if ("java.util.concurrent.CompletableFuture".equals(type.mainType())) {
270+
logError(method.element(), "CompletableFuture is not a supported return type.");
271+
writer.append("; //ERROR");
272+
return;
273+
}
274+
268275
final var produces = method.produces();
269276
switch (responseMode) {
270277
case Void -> {}
271-
case Json -> writer.append("ctx.json(");
272-
case Text -> writer.append("ctx.text(");
273-
case Templating -> writer.append("ctx.html(");
274-
default -> writer.append("ctx.contentType(\"%s\").write(", produces);
278+
case Json -> writeJsonReturn(produces);
279+
case Text -> writer.append("ctx.text(%s);", resultVariable);
280+
case Templating -> writer.append("ctx.html(%s);", resultVariable);
281+
default -> writer.append("ctx.contentType(\"%s\").write(%s);", produces, resultVariable);
282+
}
283+
}
284+
285+
private void writeJsonReturn(String produces) {
286+
if (useJsonB) {
287+
var uType = UType.parse(method.returnType());
288+
if (produces == null) {
289+
produces = MediaType.APPLICATION_JSON.getValue();
290+
}
291+
writer.append(
292+
"%sJsonType.toJson(result, ctx.contentType(\"%s\").outputStream());",
293+
uType.shortName(), produces);
294+
} else {
295+
writer.append("ctx.json(result);");
275296
}
276297
}
277298

http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerWriter.java

+31-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.avaje.http.generator.core.*;
55

66
import java.io.IOException;
7+
import java.util.Map;
78
import java.util.Objects;
89

910
/**
@@ -14,9 +15,12 @@ class ControllerWriter extends BaseControllerWriter {
1415
private static final String AT_GENERATED = "@Generated(\"avaje-jex-generator\")";
1516
private static final String API_CONTEXT = "io.avaje.jex.Context";
1617
private static final String API_ROUTING = "io.avaje.jex.Routing";
18+
private final boolean useJsonB;
19+
private final Map<String, UType> jsonTypes;
1720

18-
ControllerWriter(ControllerReader reader) throws IOException {
21+
ControllerWriter(ControllerReader reader, boolean jsonb) throws IOException {
1922
super(reader);
23+
this.useJsonB = jsonb;
2024
reader.addImportType(API_CONTEXT);
2125
reader.addImportType(API_ROUTING);
2226
reader.addImportType("java.io.IOException");
@@ -36,6 +40,15 @@ class ControllerWriter extends BaseControllerWriter {
3640
reader.addImportType("io.avaje.jex.htmx.TemplateContentCache");
3741
}
3842
}
43+
if (useJsonB) {
44+
reader.addImportType("io.avaje.jsonb.Jsonb");
45+
reader.addImportType("io.avaje.jsonb.JsonType");
46+
reader.addImportType("io.avaje.jsonb.Types");
47+
this.jsonTypes = JsonBUtil.jsonTypes(reader);
48+
jsonTypes.values().stream().map(UType::importTypes).forEach(reader::addImportTypes);
49+
} else {
50+
this.jsonTypes = Map.of();
51+
}
3952
}
4053

4154
void write() {
@@ -61,7 +74,7 @@ private void writeAddRoutes() {
6174
private void writeHandlers() {
6275
for (MethodReader method : reader.methods()) {
6376
if (method.isWebMethod()) {
64-
new ControllerMethodWriter(method, writer, reader).writeHandler(isRequestScoped());
77+
new ControllerMethodWriter(method, writer, reader, useJsonB).writeHandler(isRequestScoped());
6578
if (!reader.isDocHidden()) {
6679
method.buildApiDocumentation();
6780
}
@@ -70,7 +83,7 @@ private void writeHandlers() {
7083
}
7184

7285
private void writeRouting(MethodReader method) {
73-
new ControllerMethodWriter(method, writer, reader).writeRouting();
86+
new ControllerMethodWriter(method, writer, reader, useJsonB).writeRouting();
7487
}
7588

7689
private void writeClassStart() {
@@ -93,18 +106,28 @@ private void writeClassStart() {
93106
if (instrumentContext) {
94107
writer.append(" private final RequestContextResolver resolver;").eol();
95108
}
109+
96110
if (reader.html()) {
97111
writer.append(" private final TemplateRender renderer;").eol();
98112
if (reader.hasContentCache()) {
99113
writer.append(" private final TemplateContentCache contentCache;").eol();
100114
}
101115
}
116+
117+
for (final UType type : jsonTypes.values()) {
118+
final var typeString = PrimitiveUtil.wrap(type.shortType()).replace(",", ", ");
119+
writer.append(" private final JsonType<%s> %sJsonType;", typeString, type.shortName()).eol();
120+
}
121+
102122
writer.eol();
103123

104124
writer.append(" public %s$Route(%s %s", shortName, controllerType, controllerName);
105125
if (reader.isIncludeValidator()) {
106126
writer.append(", Validator validator");
107127
}
128+
if (useJsonB) {
129+
writer.append(", Jsonb jsonb");
130+
}
108131
if (instrumentContext) {
109132
writer.append(", RequestContextResolver resolver");
110133
}
@@ -128,6 +151,11 @@ private void writeClassStart() {
128151
writer.append(" this.contentCache = contentCache;").eol();
129152
}
130153
}
154+
if (useJsonB) {
155+
for (final UType type : jsonTypes.values()) {
156+
JsonBUtil.writeJsonbType(type, writer);
157+
}
158+
}
131159
writer.append(" }").eol().eol();
132160
}
133161

http-generator-jex/src/main/java/io/avaje/http/generator/jex/JexProcessor.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.avaje.http.generator.core.BaseProcessor;
44
import io.avaje.http.generator.core.ControllerReader;
55
import io.avaje.http.generator.core.PlatformAdapter;
6+
import io.avaje.http.generator.core.ProcessingContext;
67
import io.avaje.prism.AnnotationProcessor;
78

89
import java.io.IOException;
@@ -17,6 +18,6 @@ protected PlatformAdapter providePlatformAdapter() {
1718

1819
@Override
1920
public void writeControllerAdapter(ControllerReader reader) throws IOException {
20-
new ControllerWriter(reader).write();
21+
new ControllerWriter(reader, ProcessingContext.useJsonb()).write();
2122
}
2223
}

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<nexus.staging.autoReleaseAfterClose>true</nexus.staging.autoReleaseAfterClose>
2222
<swagger.version>2.2.26</swagger.version>
2323
<jackson.version>2.14.2</jackson.version>
24-
<jex.version>3.0-SNAPSHOT</jex.version>
24+
<jex.version>3.0-RC7</jex.version>
2525
<avaje.prisms.version>1.35</avaje.prisms.version>
2626
<project.build.outputTimestamp>2024-11-27T10:39:59Z</project.build.outputTimestamp>
2727
<module-info.shade>${project.build.directory}${file.separator}module-info.shade</module-info.shade>

tests/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<junit.version>5.11.3</junit.version>
1616
<assertj.version>3.26.3</assertj.version>
1717
<jackson.version>2.18.1</jackson.version>
18-
<jex.version>3.0-RC3</jex.version>
18+
<jex.version>3.0-RC7</jex.version>
1919
<avaje-inject.version>11.0</avaje-inject.version>
2020
<nima.version>4.1.4</nima.version>
2121
<javalin.version>6.3.0</javalin.version>

tests/test-client-generation/pom.xml

+11-5
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@
1818
<dependencies>
1919

2020
<dependency>
21-
<groupId>org.avaje</groupId>
22-
<artifactId>logback</artifactId>
23-
<version>1.0</version>
21+
<groupId>org.slf4j</groupId>
22+
<artifactId>slf4j-jdk-platform-logging</artifactId>
23+
<version>2.0.16</version>
24+
</dependency>
25+
26+
<dependency>
27+
<groupId>ch.qos.logback</groupId>
28+
<artifactId>logback-classic</artifactId>
29+
<version>1.5.12</version>
2430
</dependency>
2531

2632
<dependency>
@@ -90,7 +96,7 @@
9096
<version>${assertj.version}</version>
9197
<scope>test</scope>
9298
</dependency>
93-
99+
94100
<!-- needed for mvnd parallel builds-->
95101
<dependency>
96102
<groupId>io.avaje</groupId>
@@ -102,7 +108,7 @@
102108
<artifactId>avaje-http-jex-generator</artifactId>
103109
<version>${project.version}</version>
104110
</dependency>
105-
111+
106112
</dependencies>
107113

108114
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
4+
<level>TRACE</level>
5+
</filter>
6+
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
7+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="INFO">
12+
<appender-ref ref="STDOUT"/>
13+
</root>
14+
15+
<logger name="io.avaje.jex" level="TRACE"/>
16+
<logger name="io.avaje.http.client" level="TRACE"/>
17+
18+
</configuration>

tests/test-jex/src/main/java/org/example/web/HelloDto.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.example.web;
22

3+
import io.avaje.jsonb.Json;
34
import io.avaje.validation.constraints.NotNull;
45
import io.avaje.validation.constraints.Valid;
56

67
@Valid
8+
@Json
79
public class HelloDto {
810
public int id;
911
@NotNull

tests/test-jex/src/main/java/org/example/web/myapp/WebController.java

+8-23
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ String getPlainMessage() {
6464
@Deprecated
6565
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
6666
@Get("/:id/:date")
67-
HelloDto hello(int id, LocalDate date, String otherParam) {
68-
return new HelloDto(id, date.toString(), otherParam);
67+
WebHelloDto hello(int id, LocalDate date, String otherParam) {
68+
return new WebHelloDto(id, date.toString(), otherParam);
6969
}
7070

7171
/**
@@ -77,7 +77,7 @@ HelloDto hello(int id, LocalDate date, String otherParam) {
7777
*/
7878
@Roles(AppRoles.ADMIN)
7979
@Get("/findbyname/{name}")
80-
List<HelloDto> findByName(String name, @QueryParam("my-param") @Default("one") String myParam) {
80+
List<WebHelloDto> findByName(String name, @QueryParam("my-param") @Default("one") String myParam) {
8181
return new ArrayList<>();
8282
}
8383

@@ -86,7 +86,7 @@ List<HelloDto> findByName(String name, @QueryParam("my-param") @Default("one") S
8686
*/
8787
@Produces(MediaType.APPLICATION_JSON_PATCH_JSON)
8888
@Post
89-
HelloDto post(HelloDto dto) {
89+
WebHelloDto post(WebHelloDto dto) {
9090
dto.name = "posted";
9191
return dto;
9292
}
@@ -99,7 +99,7 @@ HelloDto post(HelloDto dto) {
9999
*/
100100
// @Roles({ADMIN})
101101
@Post("/savebean/:foo")
102-
void saveBean(String foo, HelloDto dto, Context context) {
102+
void saveBean(String foo, WebHelloDto dto, Context context) {
103103
// save hello data ...
104104
System.out.println("save " + foo + " dto:" + dto);
105105
requireNonNull(foo);
@@ -130,8 +130,8 @@ void saveForm2(String name, String email, String url) {
130130

131131
@Post("saveform3")
132132
@Form
133-
HelloDto saveForm3(HelloForm helloForm) {
134-
return new HelloDto(52, helloForm.name, helloForm.email);
133+
WebHelloDto saveForm3(HelloForm helloForm) {
134+
return new WebHelloDto(52, helloForm.name, helloForm.email);
135135
}
136136

137137
@Produces("text/plain")
@@ -142,25 +142,10 @@ String getGetBeanForm(@BeanParam GetBeanForm bean) {
142142

143143
@Hidden
144144
@Get
145-
List<HelloDto> getAll() {
145+
List<WebHelloDto> getAll() {
146146
return myService.findAll();
147147
}
148148

149-
@Get("/async")
150-
CompletableFuture<List<HelloDto>> getAllAsync() {
151-
return CompletableFuture.supplyAsync(() -> {
152-
// Simulate a delay as if an actual IO operation is being executed.
153-
// This also helps ensure that we aren't just getting lucky with timings.
154-
try {
155-
Thread.sleep(10L);
156-
} catch (InterruptedException e) {
157-
throw new RuntimeException(e);
158-
}
159-
160-
return myService.findAll();
161-
}, Executors.newSingleThreadExecutor()); // Example of how to use a custom executor.
162-
}
163-
164149
// @Hidden
165150
@Delete(":id")
166151
void deleteById(int id) {

0 commit comments

Comments
 (0)