Skip to content

[Jex-Generator] Jsonb Type generation #522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;

import io.avaje.http.generator.core.*;
import io.avaje.http.generator.core.openapi.MediaType;

import javax.lang.model.type.TypeMirror;

Expand All @@ -17,22 +18,25 @@ class ControllerMethodWriter {
private final Append writer;
private final ControllerReader reader;
private final WebMethod webMethod;
private final boolean useJsonB;
private final boolean instrumentContext;
private final boolean isFilter;

ControllerMethodWriter(MethodReader method, Append writer, ControllerReader reader) {
ControllerMethodWriter(
MethodReader method, Append writer, ControllerReader reader, boolean useJsonB) {
this.method = method;
this.writer = writer;
this.reader = reader;
this.useJsonB = useJsonB;
this.webMethod = method.webMethod();
this.instrumentContext = method.instrumentContext();
this.isFilter = webMethod == CoreWebMethod.FILTER;
if (isFilter) {
validateMethod();
validateFilter();
}
}

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

private boolean producesJson() {
return // useJsonB
!disabledDirectWrites()
&& !"byte[]".equals(method.returnType().toString())
return !"byte[]".equals(method.returnType().toString())
&& (method.produces() == null || method.produces().toLowerCase().contains("json"));
}

Expand Down Expand Up @@ -200,8 +202,7 @@ private void write(boolean requestScoped) {
writer.append(");").eol();
writer.append(" var cacheContent = contentCache.content(key);").eol();
writer.append(" if (cacheContent != null) {").eol();
writeContextReturn(responseMode);
writer.append(" res.send(cacheContent);").eol();
writeContextReturn(responseMode, "cacheContent");
writer.append(" return;").eol();
writer.append(" }").eol();
}
Expand Down Expand Up @@ -251,27 +252,47 @@ private void write(boolean requestScoped) {
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
}
writer.append(indent);
writeContextReturn(responseMode);
writer.append("content);").eol();
writeContextReturn(responseMode, "content");
} else {
writer.append(indent);
writeContextReturn(responseMode);
writer.append("result);").eol();
writeContextReturn(responseMode, "result");
writer.eol();
}
if (includeNoContent) {
writer.append(" }").eol();
}
}
}

private void writeContextReturn(ResponseMode responseMode) {
private void writeContextReturn(ResponseMode responseMode, String resultVariable) {
final UType type = UType.parse(method.returnType());
if ("java.util.concurrent.CompletableFuture".equals(type.mainType())) {
logError(method.element(), "CompletableFuture is not a supported return type.");
writer.append("; //ERROR");
return;
}

final var produces = method.produces();
switch (responseMode) {
case Void -> {}
case Json -> writer.append("ctx.json(");
case Text -> writer.append("ctx.text(");
case Templating -> writer.append("ctx.html(");
default -> writer.append("ctx.contentType(\"%s\").write(", produces);
case Json -> writeJsonReturn(produces);
case Text -> writer.append("ctx.text(%s);", resultVariable);
case Templating -> writer.append("ctx.html(%s);", resultVariable);
default -> writer.append("ctx.contentType(\"%s\").write(%s);", produces, resultVariable);
}
}

private void writeJsonReturn(String produces) {
if (useJsonB) {
var uType = UType.parse(method.returnType());
if (produces == null) {
produces = MediaType.APPLICATION_JSON.getValue();
}
writer.append(
"%sJsonType.toJson(result, ctx.contentType(\"%s\").outputStream());",
uType.shortName(), produces);
} else {
writer.append("ctx.json(result);");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.avaje.http.generator.core.*;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;

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

ControllerWriter(ControllerReader reader) throws IOException {
ControllerWriter(ControllerReader reader, boolean jsonb) throws IOException {
super(reader);
this.useJsonB = jsonb;
reader.addImportType(API_CONTEXT);
reader.addImportType(API_ROUTING);
reader.addImportType("java.io.IOException");
Expand All @@ -36,6 +40,15 @@ class ControllerWriter extends BaseControllerWriter {
reader.addImportType("io.avaje.jex.htmx.TemplateContentCache");
}
}
if (useJsonB) {
reader.addImportType("io.avaje.jsonb.Jsonb");
reader.addImportType("io.avaje.jsonb.JsonType");
reader.addImportType("io.avaje.jsonb.Types");
this.jsonTypes = JsonBUtil.jsonTypes(reader);
jsonTypes.values().stream().map(UType::importTypes).forEach(reader::addImportTypes);
} else {
this.jsonTypes = Map.of();
}
}

void write() {
Expand All @@ -61,7 +74,7 @@ private void writeAddRoutes() {
private void writeHandlers() {
for (MethodReader method : reader.methods()) {
if (method.isWebMethod()) {
new ControllerMethodWriter(method, writer, reader).writeHandler(isRequestScoped());
new ControllerMethodWriter(method, writer, reader, useJsonB).writeHandler(isRequestScoped());
if (!reader.isDocHidden()) {
method.buildApiDocumentation();
}
Expand All @@ -70,7 +83,7 @@ private void writeHandlers() {
}

private void writeRouting(MethodReader method) {
new ControllerMethodWriter(method, writer, reader).writeRouting();
new ControllerMethodWriter(method, writer, reader, useJsonB).writeRouting();
}

private void writeClassStart() {
Expand All @@ -93,18 +106,28 @@ private void writeClassStart() {
if (instrumentContext) {
writer.append(" private final RequestContextResolver resolver;").eol();
}

if (reader.html()) {
writer.append(" private final TemplateRender renderer;").eol();
if (reader.hasContentCache()) {
writer.append(" private final TemplateContentCache contentCache;").eol();
}
}

for (final UType type : jsonTypes.values()) {
final var typeString = PrimitiveUtil.wrap(type.shortType()).replace(",", ", ");
writer.append(" private final JsonType<%s> %sJsonType;", typeString, type.shortName()).eol();
}

writer.eol();

writer.append(" public %s$Route(%s %s", shortName, controllerType, controllerName);
if (reader.isIncludeValidator()) {
writer.append(", Validator validator");
}
if (useJsonB) {
writer.append(", Jsonb jsonb");
}
if (instrumentContext) {
writer.append(", RequestContextResolver resolver");
}
Expand All @@ -128,6 +151,11 @@ private void writeClassStart() {
writer.append(" this.contentCache = contentCache;").eol();
}
}
if (useJsonB) {
for (final UType type : jsonTypes.values()) {
JsonBUtil.writeJsonbType(type, writer);
}
}
writer.append(" }").eol().eol();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.avaje.http.generator.core.BaseProcessor;
import io.avaje.http.generator.core.ControllerReader;
import io.avaje.http.generator.core.PlatformAdapter;
import io.avaje.http.generator.core.ProcessingContext;
import io.avaje.prism.AnnotationProcessor;

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

@Override
public void writeControllerAdapter(ControllerReader reader) throws IOException {
new ControllerWriter(reader).write();
new ControllerWriter(reader, ProcessingContext.useJsonb()).write();
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<nexus.staging.autoReleaseAfterClose>true</nexus.staging.autoReleaseAfterClose>
<swagger.version>2.2.26</swagger.version>
<jackson.version>2.14.2</jackson.version>
<jex.version>3.0-SNAPSHOT</jex.version>
<jex.version>3.0-RC7</jex.version>
<avaje.prisms.version>1.35</avaje.prisms.version>
<project.build.outputTimestamp>2024-11-27T10:39:59Z</project.build.outputTimestamp>
<module-info.shade>${project.build.directory}${file.separator}module-info.shade</module-info.shade>
Expand Down
2 changes: 1 addition & 1 deletion tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<junit.version>5.11.3</junit.version>
<assertj.version>3.26.3</assertj.version>
<jackson.version>2.18.1</jackson.version>
<jex.version>3.0-RC3</jex.version>
<jex.version>3.0-RC7</jex.version>
<avaje-inject.version>11.0</avaje-inject.version>
<nima.version>4.1.4</nima.version>
<javalin.version>6.3.0</javalin.version>
Expand Down
16 changes: 11 additions & 5 deletions tests/test-client-generation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@
<dependencies>

<dependency>
<groupId>org.avaje</groupId>
<artifactId>logback</artifactId>
<version>1.0</version>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk-platform-logging</artifactId>
<version>2.0.16</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.12</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -90,7 +96,7 @@
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>

<!-- needed for mvnd parallel builds-->
<dependency>
<groupId>io.avaje</groupId>
Expand All @@ -102,7 +108,7 @@
<artifactId>avaje-http-jex-generator</artifactId>
<version>${project.version}</version>
</dependency>

</dependencies>

<build>
Expand Down
18 changes: 18 additions & 0 deletions tests/test-client-generation/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>TRACE</level>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="io.avaje.jex" level="TRACE"/>
<logger name="io.avaje.http.client" level="TRACE"/>

</configuration>
2 changes: 2 additions & 0 deletions tests/test-jex/src/main/java/org/example/web/HelloDto.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.example.web;

import io.avaje.jsonb.Json;
import io.avaje.validation.constraints.NotNull;
import io.avaje.validation.constraints.Valid;

@Valid
@Json
public class HelloDto {
public int id;
@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ String getPlainMessage() {
@Deprecated
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/:id/:date")
HelloDto hello(int id, LocalDate date, String otherParam) {
return new HelloDto(id, date.toString(), otherParam);
WebHelloDto hello(int id, LocalDate date, String otherParam) {
return new WebHelloDto(id, date.toString(), otherParam);
}

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

Expand All @@ -86,7 +86,7 @@ List<HelloDto> findByName(String name, @QueryParam("my-param") @Default("one") S
*/
@Produces(MediaType.APPLICATION_JSON_PATCH_JSON)
@Post
HelloDto post(HelloDto dto) {
WebHelloDto post(WebHelloDto dto) {
dto.name = "posted";
return dto;
}
Expand All @@ -99,7 +99,7 @@ HelloDto post(HelloDto dto) {
*/
// @Roles({ADMIN})
@Post("/savebean/:foo")
void saveBean(String foo, HelloDto dto, Context context) {
void saveBean(String foo, WebHelloDto dto, Context context) {
// save hello data ...
System.out.println("save " + foo + " dto:" + dto);
requireNonNull(foo);
Expand Down Expand Up @@ -130,8 +130,8 @@ void saveForm2(String name, String email, String url) {

@Post("saveform3")
@Form
HelloDto saveForm3(HelloForm helloForm) {
return new HelloDto(52, helloForm.name, helloForm.email);
WebHelloDto saveForm3(HelloForm helloForm) {
return new WebHelloDto(52, helloForm.name, helloForm.email);
}

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

@Hidden
@Get
List<HelloDto> getAll() {
List<WebHelloDto> getAll() {
return myService.findAll();
}

@Get("/async")
CompletableFuture<List<HelloDto>> getAllAsync() {
return CompletableFuture.supplyAsync(() -> {
// Simulate a delay as if an actual IO operation is being executed.
// This also helps ensure that we aren't just getting lucky with timings.
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

return myService.findAll();
}, Executors.newSingleThreadExecutor()); // Example of how to use a custom executor.
}

// @Hidden
@Delete(":id")
void deleteById(int id) {
Expand Down
Loading