add core view extension with JTE and Thymeleaf support
This commit is contained in:
+79
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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 {}
|
||||
}
|
||||
+69
@@ -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));
|
||||
}
|
||||
}
|
||||
+70
@@ -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()));
|
||||
}
|
||||
}
|
||||
+3
@@ -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>
|
||||
Reference in New Issue
Block a user