add core view extension with JTE and Thymeleaf support

This commit is contained in:
Relism
2026-04-21 00:32:09 +02:00
parent 34fd74068a
commit 9e19f439be
93 changed files with 2200 additions and 1404 deletions
@@ -0,0 +1,79 @@
package dev.relism.ext.view.jte;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.routing.GET;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
class JteExtensionTest {
@GET("/ok")
@Template("pages/home.jte")
static class ValidHandler extends JteHandler {
@Override
public dev.relism.ext.view.core.ViewModel render(Request req) {
return dev.relism.ext.view.core.ViewModel.empty();
}
}
@GET("/wrong")
@Template("pages/home.jte")
static class WrongBaseHandler extends RequestHandler {
@Override
public Object handle(Request request, Response response) {
return null;
}
}
@GET("/missing")
static class MissingTemplateHandler extends JteHandler {}
@Test
void constructor_requiresNonNullCustomizer() {
assertThrows(NullPointerException.class, () -> new JteExtension(null));
}
@Test
void validate_acceptsProperHandler() throws Exception {
JteExtension ext = new JteExtension();
invokeValidate(ext, ValidHandler.class);
}
@Test
void validate_rejectsTemplateWithoutJteHandlerBase() {
JteExtension ext = new JteExtension();
IllegalStateException ex = assertThrows(IllegalStateException.class,
() -> invokeValidate(ext, WrongBaseHandler.class));
assertTrue(ex.getMessage().contains("does not extend JteHandler"));
}
@Test
void validate_rejectsJteHandlerWithoutTemplate() {
JteExtension ext = new JteExtension();
IllegalStateException ex = assertThrows(IllegalStateException.class,
() -> invokeValidate(ext, MissingTemplateHandler.class));
assertTrue(ex.getMessage().contains("must declare @Template"));
}
private static void invokeValidate(JteExtension ext, Class<? extends RequestHandler> type) throws Exception {
Method m = JteExtension.class.getDeclaredMethod("validateHandlerClass", Class.class);
m.setAccessible(true);
try {
m.invoke(ext, type);
} catch (java.lang.reflect.InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException re) throw re;
if (cause instanceof Error err) throw err;
throw new RuntimeException(cause);
}
}
}
@@ -0,0 +1,68 @@
package dev.relism.ext.view.jte;
import dev.relism.ext.view.core.GlobalValue;
import dev.relism.ext.view.core.RenderedView;
import dev.relism.ext.view.core.ViewModel;
import dev.relism.ext.view.jte.model.HomePage;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.Response;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class JteRuntimeTest {
@Test
void render_template_rendersPojoAndGlobals() throws Exception {
JteRuntime runtime = new JteRuntime(
JteSettings.builder().templateRoot("/templates").developmentMode(true).build(),
List.of(new GlobalValue("appName", req -> "Flash"))
);
RenderedView out = runtime.render(new JteHandler() {
@Override
public ViewModel render(Request req) {
return ViewModel.empty()
.with("page", new HomePage("Flash + jte", "elorc"))
.with("build", "dev");
}
}, new JteTarget("pages/home.jte", ContentType.TEXT_HTML), null, new Response(200, ContentType.JSON));
assertEquals(ContentType.TEXT_HTML, out.contentType());
assertTrue(out.body().contains("Flash + jte"));
assertTrue(out.body().contains("elorc"));
assertTrue(out.body().contains("Flash"));
}
@Test
void render_rejectsReservedGlobalKeyInLocalModel() {
JteRuntime runtime = new JteRuntime(
JteSettings.builder().templateRoot("/templates").developmentMode(true).build(),
List.of(new GlobalValue("appName", req -> "Flash"))
);
IllegalStateException ex = assertThrows(IllegalStateException.class, () ->
runtime.render(new JteHandler() {
@Override
public ViewModel render(Request req) {
return ViewModel.of("global", "bad");
}
}, new JteTarget("pages/home.jte", ContentType.TEXT_HTML), null, new Response(200, ContentType.JSON))
);
assertTrue(ex.getMessage().contains("reserved"));
}
@Test
void resolve_missingTemplateOnHandler_failsFast() {
JteRuntime runtime = new JteRuntime(JteSettings.builder().templateRoot("/templates").developmentMode(true).build(), List.of());
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runtime.resolve(NoTemplateHandler.class));
assertTrue(ex.getMessage().contains("must declare @Template"));
}
static class NoTemplateHandler extends JteHandler {}
}
@@ -0,0 +1,69 @@
package dev.relism.ext.view.jte;
import dev.relism.Flash;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class JteSettingsTest {
@Test
void defaults_followFlashDev_andUseHtml() {
JteSettings settings = JteSettings.builder().build();
assertEquals("/templates", settings.templateRoot());
assertEquals(gg.jte.ContentType.Html, settings.contentType());
assertEquals(Flash.DEV, settings.developmentMode());
assertEquals(!Flash.DEV, settings.usePrecompiled());
assertFalse(settings.binaryStaticContent());
assertEquals(Path.of("jte-classes"), settings.dynamicClassesPath());
assertEquals(Path.of("jte-classes"), settings.precompiledClassesPath());
}
@Test
void explicitOverrides_winOverDerivedDefaults() {
JteSettings settings = JteSettings.builder()
.templateRoot("src/main/jte")
.contentType(gg.jte.ContentType.Plain)
.developmentMode(true)
.usePrecompiled(true)
.binaryStaticContent(true)
.dynamicClassesPath(Path.of("var", "jte-dev"))
.precompiledClassesPath(Path.of("var", "jte-prod"))
.build();
assertEquals("/src/main/jte", settings.templateRoot());
assertEquals(gg.jte.ContentType.Plain, settings.contentType());
assertTrue(settings.developmentMode());
assertTrue(settings.usePrecompiled());
assertTrue(settings.binaryStaticContent());
assertEquals(Path.of("var", "jte-dev"), settings.dynamicClassesPath());
assertEquals(Path.of("var", "jte-prod"), settings.precompiledClassesPath());
}
@Test
void usePrecompiled_derivesFromResolvedDevModeWhenMissing() {
JteSettings dev = JteSettings.builder().developmentMode(true).build();
JteSettings prod = JteSettings.builder().developmentMode(false).build();
assertFalse(dev.usePrecompiled());
assertTrue(prod.usePrecompiled());
}
@Test
void templateRoot_normalizesAndRejectsBlank() {
JteSettings normalized = JteSettings.builder().templateRoot("templates").build();
assertEquals("/templates", normalized.templateRoot());
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().templateRoot(" "));
}
@Test
void nulls_areRejectedForRequiredObjects() {
assertThrows(NullPointerException.class, () -> JteSettings.builder().contentType(null));
assertThrows(NullPointerException.class, () -> JteSettings.builder().dynamicClassesPath(null));
assertThrows(NullPointerException.class, () -> JteSettings.builder().precompiledClassesPath(null));
}
}
@@ -0,0 +1,70 @@
package dev.relism.ext.view.jte;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.routing.GET;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class JteTargetResolverTest {
@GET("/home")
@Template("pages/home.jte")
static class TemplateHandler extends RequestHandler {
@Override
public Object handle(Request request, Response response) {
return null;
}
}
@Template("pages/no-route.jte")
static class NoRouteHandler extends RequestHandler {
@Override
public Object handle(Request request, Response response) {
return null;
}
}
@GET("/empty")
@Template(" ")
static class EmptyTemplateHandler extends RequestHandler {
@Override
public Object handle(Request request, Response response) {
return null;
}
}
@Test
void resolve_template_returnsTarget_withMirroredHttpContentType() {
JteSettings htmlSettings = JteSettings.builder().contentType(gg.jte.ContentType.Html).build();
JteSettings plainSettings = JteSettings.builder().contentType(gg.jte.ContentType.Plain).build();
JteTarget html = JteTargetResolver.resolve(TemplateHandler.class, htmlSettings);
JteTarget plain = JteTargetResolver.resolve(TemplateHandler.class, plainSettings);
assertNotNull(html);
assertEquals("pages/home.jte", html.template());
assertEquals(ContentType.TEXT_HTML, html.contentType());
assertEquals(ContentType.TEXT_PLAIN, plain.contentType());
}
@Test
void resolve_withoutTemplate_returnsNull() {
assertNull(JteTargetResolver.resolve(RequestHandler.class, JteSettings.builder().build()));
}
@Test
void resolve_templateWithoutRoute_fails() {
assertThrows(IllegalStateException.class,
() -> JteTargetResolver.resolve(NoRouteHandler.class, JteSettings.builder().build()));
}
@Test
void resolve_emptyTemplate_fails() {
assertThrows(IllegalStateException.class,
() -> JteTargetResolver.resolve(EmptyTemplateHandler.class, JteSettings.builder().build()));
}
}
@@ -0,0 +1,3 @@
package dev.relism.ext.view.jte.model;
public record HomePage(String title, String author) {}
@@ -0,0 +1,10 @@
@import dev.relism.ext.view.jte.model.HomePage
@import java.util.Map
@param HomePage page
@param String build
@param Map<String, Object> global
<h1>${page.title()}</h1>
<p>${page.author()}</p>
<small>${build}</small>
<small>${(String) global.get("appName")}</small>