diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseControllerWriter.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseControllerWriter.java index 7f9053116..25d708af4 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseControllerWriter.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseControllerWriter.java @@ -73,7 +73,9 @@ protected void writeImports() { writer.append("import static %s;", type).eol(); } writer.eol(); - for (String type : reader.importTypes()) { + var importTypes = reader.importTypes(); + importTypes.removeIf(i -> i.substring(0, i.lastIndexOf(".")).equals(packageName)); + for (String type : importTypes) { writer.append("import %s;", type).eol(); } writer.eol(); diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java index 2e5cd9dcd..bc8139b93 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java @@ -59,6 +59,7 @@ public final class ControllerReader { private boolean requestScope; private boolean docHidden; private final boolean hasInstrument; + private boolean hasJstache; public ControllerReader(TypeElement beanType) { this(beanType, ""); @@ -249,6 +250,7 @@ public void read(boolean withSingleton) { } } deriveIncludeValidation(); + jstacheImport(); addImports(withSingleton); } @@ -266,6 +268,25 @@ private boolean anyMethodHasValid() { return false; } + private void jstacheImport() { + for (final MethodReader method : methods) { + final var asTypeElement = APContext.asTypeElement(method.returnType()); + if (ProcessingContext.isJstacheTemplate(method.returnType())) { + if ("JStachio.render".equals(ProcessingContext.jstacheRenderer(method.returnType()))) { + addImportType("io.jstach.jstachio.JStachio"); + } else { + // jstachio generated classes don't have the parent type in the name + addImportType( + APContext.elements().getPackageOf(asTypeElement).getQualifiedName().toString() + + "." + + asTypeElement.getSimpleName() + + "Renderer"); + } + this.hasJstache = true; + } + } + } + private boolean anyMethodHasContentCache() { for (final MethodReader method : methods) { if (method.hasContentCache()) { @@ -383,6 +404,10 @@ public boolean hasInstrument() { return hasInstrument; } + public boolean hasJstache() { + return hasJstache; + } + public static String sanitizeImports(String type) { final int pos = type.indexOf("@"); if (pos == -1) { diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/JStacheConfigPrism.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/JStacheConfigPrism.java new file mode 100644 index 000000000..372b15980 --- /dev/null +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/JStacheConfigPrism.java @@ -0,0 +1,203 @@ +package io.avaje.http.generator.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.processing.Generated; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.ElementFilter; + +/** A Prism representing a {@link io.jstach.jstache.JStacheConfig @JStacheConfig} annotation. */ +@Generated("avaje-prism-generator") +final class JStacheConfigPrism { + + /** store prism value of type */ + private final String _type; + + public static final String PRISM_TYPE = "io.jstach.jstache.JStacheConfig"; + + /** + * An instance of the Values inner class whose methods return the AnnotationValues used to build + * this prism. Primarily intended to support using Messager. + */ + final Values values; + + /** + * Returns true if the mirror is an instance of {@link + * io.jstach.jstache.JStacheConfig @JStacheConfig} is present on the element, else false. + * + * @param mirror mirror. + * @return true if prism is present. + */ + static boolean isInstance(AnnotationMirror mirror) { + return getInstance(mirror) != null; + } + + /** + * Returns true if {@link io.jstach.jstache.JStacheConfig @JStacheConfig} is present on the + * element, else false. + * + * @param element element. + * @return true if annotation is present on the element. + */ + static boolean isPresent(Element element) { + return getInstanceOn(element) != null; + } + + /** + * Return a prism representing the {@link io.jstach.jstache.JStacheConfig @JStacheConfig} + * annotation present on the given element. similar to {@code + * element.getAnnotation(JStacheConfig.class)} except that an instance of this class rather than + * an instance of {@link io.jstach.jstache.JStacheConfig @JStacheConfig} is returned. + * + * @param element element. + * @return prism on element or null if no annotation is found. + */ + static JStacheConfigPrism getInstanceOn(Element element) { + final var mirror = getMirror(element); + if (mirror == null) return null; + return getInstance(mirror); + } + + /** + * Return a Optional representing a nullable {@link + * io.jstach.jstache.JStacheConfig @JStacheConfig} annotation on the given element. similar to + * {@code element.getAnnotation(io.jstach.jstache.JStacheConfig.class)} except that an Optional of + * this class rather than an instance of {@link io.jstach.jstache.JStacheConfig} is returned. + * + * @param element element. + * @return prism optional for element. + */ + static Optional getOptionalOn(Element element) { + final var mirror = getMirror(element); + if (mirror == null) return Optional.empty(); + return getOptional(mirror); + } + + /** + * Return a prism of the {@link io.jstach.jstache.JStacheConfig @JStacheConfig} annotation from an + * annotation mirror. + * + * @param mirror mirror. + * @return prism for mirror or null if mirror is an incorrect type. + */ + static JStacheConfigPrism getInstance(AnnotationMirror mirror) { + if (mirror == null || !PRISM_TYPE.equals(mirror.getAnnotationType().toString())) return null; + + return new JStacheConfigPrism(mirror); + } + + /** + * Return an Optional representing a nullable {@link JStacheConfigPrism @JStacheConfigPrism} from + * an annotation mirror. similar to {@code e.getAnnotation(io.jstach.jstache.JStacheConfig.class)} + * except that an Optional of this class rather than an instance of {@link + * io.jstach.jstache.JStacheConfig @JStacheConfig} is returned. + * + * @param mirror mirror. + * @return prism optional for mirror. + */ + static Optional getOptional(AnnotationMirror mirror) { + if (mirror == null || !PRISM_TYPE.equals(mirror.getAnnotationType().toString())) + return Optional.empty(); + + return Optional.of(new JStacheConfigPrism(mirror)); + } + + private JStacheConfigPrism(AnnotationMirror mirror) { + for (final ExecutableElement key : mirror.getElementValues().keySet()) { + memberValues.put(key.getSimpleName().toString(), mirror.getElementValues().get(key)); + } + for (final ExecutableElement member : + ElementFilter.methodsIn(mirror.getAnnotationType().asElement().getEnclosedElements())) { + defaults.put(member.getSimpleName().toString(), member.getDefaultValue()); + } + VariableElement typeMirror = getValue("type", VariableElement.class); + valid = valid && typeMirror != null; + _type = typeMirror == null ? null : typeMirror.getSimpleName().toString(); + this.values = new Values(memberValues); + this.mirror = mirror; + this.isValid = valid; + } + + /** + * Returns a String representing the value of the {@code io.jstach.jstache.JStacheType type()} + * member of the Annotation. + * + * @see io.jstach.jstache.JStacheConfig#type() + */ + public String type() { + return _type; + } + + /** + * Determine whether the underlying AnnotationMirror has no errors. True if the underlying + * AnnotationMirror has no errors. When true is returned, none of the methods will return null. + * When false is returned, a least one member will either return null, or another prism that is + * not valid. + */ + final boolean isValid; + + /** + * The underlying AnnotationMirror of the annotation represented by this Prism. Primarily intended + * to support using Messager. + */ + final AnnotationMirror mirror; + + /** + * A class whose members correspond to those of {@link + * io.jstach.jstache.JStacheConfig @JStacheConfig} but which each return the AnnotationValue + * corresponding to that member in the model of the annotations. Returns null for defaulted + * members. Used for Messager, so default values are not useful. + */ + static final class Values { + private final Map values; + + private Values(Map values) { + this.values = values; + } + + AnnotationValue type() { + return values.get("type"); + } + } + + private final Map defaults = new HashMap<>(10); + private final Map memberValues = + new HashMap<>(10); + private boolean valid = true; + + private T getValue(String name, Class clazz) { + final T result = JStacheConfigPrism.getValue(memberValues, defaults, name, clazz); + if (result == null) valid = false; + return result; + } + + private static AnnotationMirror getMirror(Element target) { + for (final var m : target.getAnnotationMirrors()) { + final CharSequence mfqn = + ((TypeElement) m.getAnnotationType().asElement()).getQualifiedName(); + if (PRISM_TYPE.contentEquals(mfqn)) return m; + } + return null; + } + + private static T getValue( + Map memberValues, + Map defaults, + String name, + Class clazz) { + AnnotationValue av = memberValues.get(name); + if (av == null) av = defaults.get(name); + if (av == null) { + return null; + } + if (clazz.isInstance(av.getValue())) return clazz.cast(av.getValue()); + return null; + } +} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/JStachePrism.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/JStachePrism.java new file mode 100644 index 000000000..d5d5a40cf --- /dev/null +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/JStachePrism.java @@ -0,0 +1,94 @@ +package io.avaje.http.generator.core; + +import javax.annotation.processing.Generated; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +/** A Prism representing a {@link io.jstach.jstache.JStache @JStache} annotation. */ +@Generated("avaje-prism-generator") +public final class JStachePrism { + + public static final String PRISM_TYPE = "io.jstach.jstache.JStache"; + + /** + * Returns true if the mirror is an instance of {@link io.jstach.jstache.JStache @JStache} is + * present on the element, else false. + * + * @param mirror mirror. + * @return true if prism is present. + */ + public static boolean isInstance(AnnotationMirror mirror) { + return getInstance(mirror) != null; + } + + /** + * Returns true if {@link io.jstach.jstache.JStache @JStache} is present on the element, else + * false. + * + * @param element element. + * @return true if annotation is present on the element. + */ + public static boolean isPresent(Element element) { + return getInstanceOn(element) != null; + } + + /** + * Return a prism representing the {@link io.jstach.jstache.JStache @JStache} annotation present + * on the given element. similar to {@code element.getAnnotation(JStache.class)} except that an + * instance of this class rather than an instance of {@link io.jstach.jstache.JStache @JStache} is + * returned. + * + * @param element element. + * @return prism on element or null if no annotation is found. + */ + static JStachePrism getInstanceOn(Element element) { + final var mirror = getMirror(element); + if (mirror == null) return null; + return getInstance(mirror); + } + + /** + * Return a prism of the {@link io.jstach.jstache.JStache @JStache} annotation from an annotation + * mirror. + * + * @param mirror mirror. + * @return prism for mirror or null if mirror is an incorrect type. + */ + static JStachePrism getInstance(AnnotationMirror mirror) { + if (mirror == null || !PRISM_TYPE.equals(mirror.getAnnotationType().toString())) return null; + + return new JStachePrism(mirror); + } + + private JStachePrism(AnnotationMirror mirror) { + + this.mirror = mirror; + this.isValid = valid; + } + + /** + * Determine whether the underlying AnnotationMirror has no errors. True if the underlying + * AnnotationMirror has no errors. When true is returned, none of the methods will return null. + * When false is returned, a least one member will either return null, or another prism that is + * not valid. + */ + final boolean isValid; + + /** + * The underlying AnnotationMirror of the annotation represented by this Prism. Primarily intended + * to support using Messager. + */ + final AnnotationMirror mirror; + + private final boolean valid = true; + + private static AnnotationMirror getMirror(Element target) { + for (final var m : target.getAnnotationMirrors()) { + final CharSequence mfqn = + ((TypeElement) m.getAnnotationType().asElement()).getQualifiedName(); + if (PRISM_TYPE.contentEquals(mfqn)) return m; + } + return null; + } +} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java index 2a33c04a9..f6c2a5a67 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java @@ -29,9 +29,9 @@ public static Map jsonTypes(ControllerReader reader) { if (!methodReader.isErrorMethod()) { addJsonBodyType(methodReader, addToMap); } - if (!methodReader.isVoid()) { + final var asTypeElement = APContext.asTypeElement(methodReader.returnType()); + if (!methodReader.isVoid() && (asTypeElement == null || !JStachePrism.isPresent(asTypeElement))) { var uType = UType.parse(methodReader.returnType()); - if ("java.util.concurrent.CompletableFuture".equals(uType.mainType())) { uType = uType.paramRaw(); } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java index c865caa14..c3fcbf2ea 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java @@ -3,8 +3,10 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Paths; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -34,6 +36,9 @@ public final class ProcessingContext { private static final ThreadLocal CTX = new ThreadLocal<>(); + private static final boolean ZERO_JSTACHIO = APContext.typeElement("io.jstach.jstachio.JStachio") == null; + private static final Map jstacheRenderers = new HashMap<>(); + private ProcessingContext() {} private static final class Ctx { @@ -142,7 +147,6 @@ public static JavaFileObject createWriter(String cls, Element origin) throws IOE /** Create a file writer for the META-INF services file. */ public static FileObject createMetaInfWriter(String target) throws IOException { - return filer().createResource(StandardLocation.CLASS_OUTPUT, "", target); } @@ -287,4 +291,39 @@ private static boolean resourceExists(String relativeName) { public static void addClientComponent(String clientFQN) { CTX.get().clientFQN.add(clientFQN); } + + public static String jstacheRenderer(TypeMirror typeMirror) { + final var typeElement = APContext.asTypeElement(typeMirror); + final var typeName = typeElement.getSimpleName().toString(); + return jstacheRenderers.computeIfAbsent(typeName, k -> determineJstacheRenderer(typeElement)); + } + + private static String determineJstacheRenderer(TypeElement typeElement) { + return ZERO_JSTACHIO + ? jstacheTypeRenderer(typeElement) + : checkJstacheConfig(typeElement, typeElement); + } + + private static String checkJstacheConfig(Element element, TypeElement typeElement) { + if (element == null) { + return "JStachio.render"; + } + var config = JStacheConfigPrism.getInstanceOn(element); + if (config != null && "STACHE".equals(config.type())) { + return jstacheTypeRenderer(typeElement); + } else if (config != null && "JSTACHIO".equals(config.type())) { + return "JStachio.render"; + } + + return checkJstacheConfig(element.getEnclosingElement(), typeElement); + } + + private static String jstacheTypeRenderer(TypeElement typeElement) { + return typeElement.getSimpleName() + "Renderer.of().execute"; + } + + public static boolean isJstacheTemplate(TypeMirror mirror) { + final var typeElement = APContext.asTypeElement(mirror); + return typeElement != null && JStachePrism.isPresent(typeElement); + } } diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java index 993ea6c64..a89aa596a 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java @@ -61,13 +61,15 @@ final class ControllerMethodWriter { private final boolean instrumentContext; private final boolean isFilter; private final ControllerReader reader; + private final boolean useJstachio; ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB, ControllerReader reader) { this.reader = reader; this.method = method; this.writer = writer; this.webMethod = method.webMethod(); - this.useJsonB = useJsonB; + this.useJstachio = ProcessingContext.isJstacheTemplate(method.returnType()); + this.useJsonB = !useJstachio && useJsonB; this.instrumentContext = method.instrumentContext(); this.isFilter = webMethod == CoreWebMethod.FILTER; if (isFilter) { @@ -246,6 +248,12 @@ void writeHandler(boolean requestScoped) { writeContextReturn(indent); writer.append(indent).append("res.send(content);").eol(); + } else if (responseMode == ResponseMode.Jstachio) { + var renderer = ProcessingContext.jstacheRenderer(method.returnType()); + writer.append(indent).append("var content = %s(result);", renderer).eol(); + writeContextReturn(indent); + writer.append(indent).append("res.send(content);").eol(); + } else { writeContextReturn(indent); if (responseMode == ResponseMode.InputStream) { @@ -272,6 +280,7 @@ void writeHandler(boolean requestScoped) { enum ResponseMode { Void, Json, + Jstachio, Templating, InputStream, Other @@ -290,6 +299,9 @@ ResponseMode responseMode() { if (useTemplating()) { return ResponseMode.Templating; } + if (useJstachio) { + return ResponseMode.Jstachio; + } return ResponseMode.Other; } @@ -361,10 +373,13 @@ private boolean usesFormParams() { private void writeContextReturn(String indent) { final var producesOp = Optional.ofNullable(method.produces()); - if (producesOp.isEmpty() && !useJsonB) { + if (producesOp.isEmpty() && !useJsonB && !useJstachio) { return; } - final var produces = producesOp.map(MediaType::parse).orElse(MediaType.APPLICATION_JSON); + final var produces = + producesOp + .map(MediaType::parse) + .orElse(useJstachio ? MediaType.HTML_UTF8 : MediaType.APPLICATION_JSON); final var contentTypeString = "res.headers().contentType(MediaTypes."; writer.append(indent); switch (produces) { diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java index 4e5d61bd8..3c8d2c726 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java @@ -137,7 +137,7 @@ private void writeClassStart() { if (reader.isIncludeValidator()) { writer.append(" private static final HeaderName HEADER_ACCEPT_LANGUAGE = HeaderNames.create(\"Accept-Language\");").eol(); } - if (reader.html()) { + if (reader.html() || reader.hasJstache()) { writer.append(" private static final io.helidon.common.media.type.MediaType HTML_UTF8 = MediaTypes.create(\"text/html;charset=UTF8\");").eol(); } diff --git a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java index 799d67763..00336283e 100644 --- a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java +++ b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java @@ -16,13 +16,15 @@ class ControllerMethodWriter { private final boolean useJsonB; private final boolean instrumentContext; private final boolean customMethod; + private final boolean useJstachio; ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB) { this.method = method; this.writer = writer; final var webM = method.webMethod(); this.webMethod = webM == CoreWebMethod.FILTER ? JavalinWebMethod.BEFORE : webM; - this.useJsonB = useJsonB && !disabledDirectWrites(); + this.useJstachio = ProcessingContext.isJstacheTemplate(method.returnType()); + this.useJsonB = !useJstachio && useJsonB && !disabledDirectWrites(); this.instrumentContext = method.instrumentContext(); customMethod = !(webMethod instanceof CoreWebMethod); } @@ -152,6 +154,11 @@ private void writeContextReturn() { private void writeContextReturn(final String resultVariableName) { var produces = method.produces(); + + if (useJstachio && produces == null) { + produces = MediaType.TEXT_HTML.getValue(); + } + boolean applicationJson = produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces); if (applicationJson || JsonBUtil.isJsonMimeType(produces)) { if (useJsonB) { @@ -170,13 +177,14 @@ private void writeContextReturn(final String resultVariableName) { if (isfuture || method.isErrorMethod()) { writer.append(" } catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }"); } + } else if (applicationJson) { + writer.append(" ctx.json(%s);", resultVariableName); } else { - if (applicationJson) { - writer.append(" ctx.json(%s);", resultVariableName); - } else { - writer.append(" ctx.contentType(\"%s\").json(%s);", produces, resultVariableName); - } + writer.append(" ctx.contentType(\"%s\").json(%s);", produces, resultVariableName); } + } else if (useJstachio) { + var renderer = ProcessingContext.jstacheRenderer(method.returnType()); + writer.append(" ctx.contentType(\"%s\").result(%s(%s));", produces, renderer, resultVariableName); } else if (MediaType.TEXT_HTML.getValue().equalsIgnoreCase(produces)) { writer.append(" ctx.html(%s);", resultVariableName); } else if (MediaType.TEXT_PLAIN.getValue().equalsIgnoreCase(produces)) { diff --git a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java index bdff607aa..60557b0c3 100644 --- a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java +++ b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java @@ -9,16 +9,14 @@ import static io.avaje.http.generator.core.ProcessingContext.*; -/** - * Write code to register Web route for a given controller method. - */ +/** Write code to register Web route for a given controller method. */ class ControllerMethodWriter { - private final MethodReader method; private final Append writer; private final ControllerReader reader; private final WebMethod webMethod; private final boolean useJsonB; + private final boolean useJstachio; private final boolean instrumentContext; private final boolean isFilter; @@ -27,7 +25,8 @@ class ControllerMethodWriter { this.method = method; this.writer = writer; this.reader = reader; - this.useJsonB = useJsonB; + this.useJstachio = ProcessingContext.isJstacheTemplate(method.returnType()); + this.useJsonB = !useJstachio && useJsonB; this.webMethod = method.webMethod(); this.instrumentContext = method.instrumentContext(); this.isFilter = webMethod == CoreWebMethod.FILTER; @@ -102,6 +101,7 @@ enum ResponseMode { Void, Json, Text, + Jstachio, Templating, InputStream, Other @@ -120,6 +120,9 @@ ResponseMode responseMode() { if (useTemplating()) { return ResponseMode.Templating; } + if (useJstachio) { + return ResponseMode.Jstachio; + } if (producesText()) { return ResponseMode.Text; } @@ -132,11 +135,12 @@ private boolean isInputStream(TypeMirror type) { private boolean producesJson() { return !"byte[]".equals(method.returnType().toString()) + && !useJstachio && (method.produces() == null || method.produces().toLowerCase().contains("json")); } private boolean producesText() { - return (method.produces() != null && method.produces().toLowerCase().contains("text")); + return (method.produces() != null && method.produces().toLowerCase().contains("text")); } private boolean useContentCache() { @@ -245,16 +249,25 @@ private void write(boolean requestScoped) { if (includeNoContent) { writer.append(" if (result != null) {").eol(); } - if (responseMode == ResponseMode.Templating) { - writer.append(indent).append("var content = renderer.render(result);").eol(); - if (withContentCache) { - writer.append(indent).append("contentCache.contentPut(key, content);").eol(); + switch (responseMode) { + case Templating -> { + writer.append(indent).append("var content = renderer.render(result);").eol(); + if (withContentCache) { + writer.append(indent).append("contentCache.contentPut(key, content);").eol(); + } + writer.append(indent); + writeContextReturn(responseMode, "content"); + } + case Jstachio -> { + var renderer = ProcessingContext.jstacheRenderer(method.returnType()); + writer.append(indent).append("var content = %s(result);", renderer).eol(); + writer.append(indent); + writeContextReturn(responseMode, "content"); + } + default -> { + writer.append(indent); + writeContextReturn(responseMode, "result"); } - writer.append(indent); - writeContextReturn(responseMode, "content"); - } else { - writer.append(indent); - writeContextReturn(responseMode, "result"); } if (includeNoContent) { writer.append(" }").eol(); @@ -270,7 +283,11 @@ private void writeContextReturn(ResponseMode responseMode, String resultVariable return; } - final var produces = method.produces(); + var produces = method.produces(); + if (produces == null && useJstachio) { + writer.append("ctx.html(%s);", resultVariable).eol(); + return; + } switch (responseMode) { case Void -> {} case Json -> writeJsonReturn(produces); diff --git a/tests/test-jex/src/main/java/org/example/web/jstache/JstacheController.java b/tests/test-jex/src/main/java/org/example/web/jstache/JstacheController.java new file mode 100644 index 000000000..47b32e959 --- /dev/null +++ b/tests/test-jex/src/main/java/org/example/web/jstache/JstacheController.java @@ -0,0 +1,77 @@ +package org.example.web.jstache; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import io.avaje.http.api.Controller; +import io.avaje.http.api.Get; +import io.jstach.jstache.JStache; +import io.jstach.jstache.JStacheConfig; +import io.jstach.jstache.JStacheLambda; +import io.jstach.jstache.JStacheType; + +@Controller("/jstache") +public class JstacheController { + + @Get("/hello") + public HelloWorldZeroDependency hello() { + Person rick = new Person("Rick", LocalDate.now().minusYears(70)); + Person morty = new Person("Morty", LocalDate.now().minusYears(14)); + Person beth = new Person("Beth", LocalDate.now().minusYears(35)); + Person jerry = new Person("Jerry", LocalDate.now().minusYears(35)); + return new HelloWorldZeroDependency("Hello alien", List.of(rick, morty, beth, jerry)); + } + + @Get("/helloRuntime") + public HelloWorld helloRuntime() { + Person rick = new Person("Rick", LocalDate.now().minusYears(70)); + Person morty = new Person("Morty", LocalDate.now().minusYears(14)); + Person beth = new Person("Beth", LocalDate.now().minusYears(35)); + Person jerry = new Person("Jerry", LocalDate.now().minusYears(35)); + return new HelloWorld("Hello alien", List.of(rick, morty, beth, jerry)); + } + + /* + * Annotate the root model with an inline mustache template + */ + @JStacheConfig(type = JStacheType.STACHE) + @JStache( + template = + """ + {{#people}} + {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old! + {{#-last}} + That is all for now! + {{/-last}} + {{/people}} + """) + public record HelloWorldZeroDependency(String message, List people) implements AgeLambdaSupport {} + + public record Person(String name, LocalDate birthday) {} + + public record AgeInfo(long age, String date) {} + + public interface AgeLambdaSupport { + @JStacheLambda + default AgeInfo ageInfo(Person person) { + long age = ChronoUnit.YEARS.between(person.birthday(), LocalDate.now()); + String date = person.birthday().format(DateTimeFormatter.ISO_DATE); + return new AgeInfo(age, date); + } + } + + @JStache( + template = + """ + {{#people}} + {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old! + {{#-last}} + That is all for now! + {{/-last}} + {{/people}} + """) + public record HelloWorld(String message, List people) implements AgeLambdaSupport {} + +} diff --git a/tests/test-jex/src/main/resources/public/openapi.json b/tests/test-jex/src/main/resources/public/openapi.json index e4b99b6d6..e31f0de26 100644 --- a/tests/test-jex/src/main/resources/public/openapi.json +++ b/tests/test-jex/src/main/resources/public/openapi.json @@ -1023,6 +1023,48 @@ "deprecated" : true } }, + "/jstache/hello" : { + "get" : { + "tags" : [ + + ], + "summary" : "", + "description" : "", + "responses" : { + "200" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HelloWorldZeroDependency" + } + } + } + } + } + } + }, + "/jstache/helloRuntime" : { + "get" : { + "tags" : [ + + ], + "summary" : "", + "description" : "", + "responses" : { + "200" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HelloWorld" + } + } + } + } + } + } + }, "/other/{name}" : { "get" : { "tags" : [ @@ -1516,6 +1558,34 @@ } } }, + "HelloWorld" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + }, + "people" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Person" + } + } + } + }, + "HelloWorldZeroDependency" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + }, + "people" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Person" + } + } + } + }, "Long>" : { "type" : "object", "properties" : { @@ -1532,6 +1602,18 @@ } } }, + "Person" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "birthday" : { + "type" : "string", + "format" : "date" + } + } + }, "String>" : { "type" : "object", "properties" : { diff --git a/tests/test-nima-jsonb/pom.xml b/tests/test-nima-jsonb/pom.xml index 8154fa6cd..2cd8cdb04 100644 --- a/tests/test-nima-jsonb/pom.xml +++ b/tests/test-nima-jsonb/pom.xml @@ -49,6 +49,12 @@ ${project.version} + + io.jstach + jstachio + 1.3.6 + + io.avaje junit @@ -92,6 +98,11 @@ avaje-validator-generator 2.7 + + io.jstach + jstachio-apt + 1.3.6 + diff --git a/tests/test-nima-jsonb/src/main/java/org/example/jstache/JstacheController.java b/tests/test-nima-jsonb/src/main/java/org/example/jstache/JstacheController.java new file mode 100644 index 000000000..43817e411 --- /dev/null +++ b/tests/test-nima-jsonb/src/main/java/org/example/jstache/JstacheController.java @@ -0,0 +1,77 @@ +package org.example.jstache; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import io.avaje.http.api.Controller; +import io.avaje.http.api.Get; +import io.jstach.jstache.JStache; +import io.jstach.jstache.JStacheConfig; +import io.jstach.jstache.JStacheLambda; +import io.jstach.jstache.JStacheType; + +@Controller("/jstache") +public class JstacheController { + + @Get("/hello") + public HelloWorldZeroDependency hello() { + Person rick = new Person("Rick", LocalDate.now().minusYears(70)); + Person morty = new Person("Morty", LocalDate.now().minusYears(14)); + Person beth = new Person("Beth", LocalDate.now().minusYears(35)); + Person jerry = new Person("Jerry", LocalDate.now().minusYears(35)); + return new HelloWorldZeroDependency("Hello alien", List.of(rick, morty, beth, jerry)); + } + + @Get("/helloRuntime") + public HelloWorld helloRuntime() { + Person rick = new Person("Rick", LocalDate.now().minusYears(70)); + Person morty = new Person("Morty", LocalDate.now().minusYears(14)); + Person beth = new Person("Beth", LocalDate.now().minusYears(35)); + Person jerry = new Person("Jerry", LocalDate.now().minusYears(35)); + return new HelloWorld("Hello alien", List.of(rick, morty, beth, jerry)); + } + + /* + * Annotate the root model with an inline mustache template + */ + @JStacheConfig(type = JStacheType.STACHE) + @JStache( + template = + """ + {{#people}} + {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old! + {{#-last}} + That is all for now! + {{/-last}} + {{/people}} + """) + public record HelloWorldZeroDependency(String message, List people) implements AgeLambdaSupport {} + + public record Person(String name, LocalDate birthday) {} + + public record AgeInfo(long age, String date) {} + + public interface AgeLambdaSupport { + @JStacheLambda + default AgeInfo ageInfo(Person person) { + long age = ChronoUnit.YEARS.between(person.birthday(), LocalDate.now()); + String date = person.birthday().format(DateTimeFormatter.ISO_DATE); + return new AgeInfo(age, date); + } + } + + @JStache( + template = + """ + {{#people}} + {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old! + {{#-last}} + That is all for now! + {{/-last}} + {{/people}} + """) + public record HelloWorld(String message, List people) implements AgeLambdaSupport {} + +}