params) {
- if (params == null || params.isEmpty()) return url;
- StringBuilder qs = new StringBuilder();
- for (var e : params.entrySet()) {
- if (base.contains('{' + e.getKey() + '}') || e.getValue() == null) continue;
- qs.append(qs.isEmpty() ? '?' : '&')
- .append(encode(e.getKey()))
- .append('=')
- .append(encode(String.valueOf(e.getValue())));
- }
- return qs.isEmpty() ? url : url + qs;
- }
-
- private static String encode(String s) {
- return URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20");
- }
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineAdapter.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineAdapter.java
deleted file mode 100644
index d993784..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineAdapter.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.models.Request;
-import dev.relism.models.Response;
-
-import java.util.Map;
-
-/**
- * Rendering adapter contract used by the Flash SSR view runtime.
- *
- * Implementations must be thread-safe after construction because one instance is shared by
- * all requests.
- */
-public interface ViewEngineAdapter {
-
- /** Engine feature flags used for boot-time route/view validation. */
- EngineCapabilities capabilities();
-
- /**
- * Renders a page/partial target with the merged model for the current request.
- *
- * @param target resolved rendering target
- * @param model merged request model (globals first, local model last)
- * @param req current request
- * @param res current response
- * @return response body + optional explicit content type
- */
- RenderOutput render(ViewTarget target, Map model, Request req, Response res) throws Exception;
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineType.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineType.java
deleted file mode 100644
index 4d89926..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineType.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package dev.relism.ext.view;
-
-/**
- * Managed template engine types supported out-of-the-box by {@link ViewExtension}.
- *
- * Pass one of these constants to {@link ViewExtension#ViewExtension(ViewEngineType)}
- * for zero-boilerplate setup. The extension auto-configures the selected engine with
- * sensible defaults and validates that the required library is on the runtime classpath,
- * throwing a descriptive {@link IllegalStateException} at boot time if it is not.
- *
- *
{@code
- * // Zero-boilerplate — Thymeleaf auto-configured with defaults
- * app.install(new ViewExtension(ViewEngineType.THYMELEAF));
- * }
- *
- * Dev mode
- * Template caching is disabled when either:
- *
- * - the JVM property {@code flash.env} equals {@code dev} (case-insensitive), or
- * - the environment variable {@code FLASH_ENV} equals {@code dev}.
- *
- * In all other cases caching is enabled (production default).
- *
- * Adding your own engine
- * For unsupported engines, implement {@link ViewEngineAdapter} and pass it to
- * {@link ViewExtension#ViewExtension(ViewEngineAdapter)}.
- */
-public enum ViewEngineType {
-
- /**
- * Thymeleaf 3.x — natural HTML templates with server-side rendering.
- *
- * Required dependency (add to your {@code pom.xml}):
- *
{@code
- *
- * org.thymeleaf
- * thymeleaf
- * 3.1.2.RELEASE
- *
- * }
- *
- * Default resolver: classpath, prefix {@code /templates/}, suffix {@code .html},
- * mode {@code HTML}, encoding UTF-8.
- */
- THYMELEAF;
-
- // ── Factory ───────────────────────────────────────────────────────────────
-
- /**
- * Instantiates and configures the {@link ViewEngineAdapter} for this type.
- * Called once at extension setup time — never on the hot-path.
- *
- * @param cacheEnabled whether the engine should cache compiled templates
- * @throws IllegalStateException if the required library is not on the classpath
- */
- ViewEngineAdapter createAdapter(boolean cacheEnabled) {
- return switch (this) {
- case THYMELEAF -> createThymeleaf(cacheEnabled);
- };
- }
-
- // ── Engine factories ──────────────────────────────────────────────────────
-
- private static ViewEngineAdapter createThymeleaf(boolean cacheEnabled) {
- try {
- return new ThymeleafEngine(cacheEnabled);
- } catch (NoClassDefFoundError e) {
- throw new IllegalStateException("""
- Thymeleaf is not on the classpath. \
- Add the following dependency to your pom.xml:
-
-
- org.thymeleaf
- thymeleaf
- 3.1.2.RELEASE
-
- """, e);
- }
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewExtension.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewExtension.java
deleted file mode 100644
index 477be9e..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewExtension.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.Flash;
-import dev.relism.extension.FlashContext;
-import dev.relism.extension.FlashExtension;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.function.Function;
-
-/**
- * Installs server-side view rendering for class-based handlers.
- *
- * Strict contract: only {@link ViewHandler} subclasses may declare {@link Page}/{@link Partial}.
- * Annotating a plain {@link dev.relism.models.RequestHandler} fails fast at boot.
- */
-public final class ViewExtension implements FlashExtension {
-
- private final ViewEngineAdapter adapter;
- private final List globals = new ArrayList<>();
-
- public ViewExtension(ViewEngineType type) {
- this(type.createAdapter(!Flash.DEV));
- }
-
- public ViewExtension(ViewEngineAdapter adapter) {
- this.adapter = Objects.requireNonNull(adapter, "ViewEngineAdapter must not be null");
- }
-
- /**
- * Registers one request-scoped global value under {@code global.}.
- *
- * This is the only supported global registration API. Keep resolvers fast and side-effect free.
- */
- public ViewExtension addGlobal(String key, Function resolver) {
- String k = Objects.requireNonNull(key, "global key must not be null").trim();
- if (k.isEmpty()) {
- throw new IllegalArgumentException("global key must not be blank");
- }
- if (k.equals("global") || k.contains(".")) {
- throw new IllegalArgumentException("global key must be a simple key (no dots), received: " + key);
- }
- globals.add(new GlobalBinding(k, Objects.requireNonNull(resolver, "global resolver must not be null")));
- return this;
- }
-
- @Override
- public void provide(FlashContext ctx) {
- ViewRuntime runtime = new ViewRuntime(adapter, List.copyOf(globals));
- ctx.provide(ViewEngineAdapter.class, adapter);
- ctx.provide(ViewRuntime.class, runtime);
-
- // Processor kept for boot-time contract enforcement. Rendering itself stays in ViewHandler.
- ctx.addAnnotationProcessor(handlerClass -> {
- ViewTargetResolver.ResolvedView resolved = ViewTargetResolver.resolve(handlerClass, adapter.capabilities());
- if (resolved == null) return List.of();
-
- if (!ViewHandler.class.isAssignableFrom(handlerClass)) {
- throw new IllegalStateException("Handler " + handlerClass.getName()
- + " declares @Page/@Partial but does not extend ViewHandler");
- }
- return List.of();
- });
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewHandler.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewHandler.java
deleted file mode 100644
index cb412ac..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewHandler.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.models.Request;
-import dev.relism.models.RequestHandler;
-import dev.relism.models.Response;
-
-/**
- * Base class for class-based SSR handlers.
- *
- * Subclass contract:
- *
- * - Declare exactly one of {@link Page} or {@link Partial} on the class hierarchy.
- * - Cache dependencies in {@link #onViewInit()} (one-time, boot-time).
- * - Build per-request model in {@link #render(Request)} or {@link #render(Request, Response)}.
- *
- */
-public abstract class ViewHandler extends RequestHandler {
- private ViewRuntime runtime;
- private ViewTargetResolver.ResolvedView resolved;
-
- /**
- * Per-request model hook.
- *
- * Override this method for request-only rendering. If you need to mutate response
- * metadata while building the model, override {@link #render(Request, Response)}.
- */
- public ViewModel render(Request req) throws Exception {
- throw new UnsupportedOperationException("Override render(Request) or render(Request, Response)");
- }
-
- /**
- * Per-request model hook with response access.
- *
- *
Default implementation delegates to {@link #render(Request)}.
- */
- public ViewModel render(Request req, Response res) throws Exception {
- return render(req);
- }
-
- @Override
- protected final void onInit() {
- runtime = require(ViewRuntime.class);
- resolved = runtime.resolve(getClass());
- onViewInit();
- }
-
- /**
- * One-time initialization hook invoked after view metadata resolution.
- *
- *
Use this to cache services via {@link #require(Class)}. Do not perform request-bound
- * work here.
- */
- protected void onViewInit() {}
-
- @Override
- public final Object handle(Request request, Response response) throws Exception {
- return runtime.render(this, resolved, request, response);
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewKind.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewKind.java
deleted file mode 100644
index d2f18b3..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewKind.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package dev.relism.ext.view;
-
-/** Render mode for resolved handler view targets. */
-public enum ViewKind {
- PAGE,
- PARTIAL
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewRuntime.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewRuntime.java
deleted file mode 100644
index f557505..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewRuntime.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.models.Request;
-import dev.relism.models.Response;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-
-/**
- * Internal runtime for view target resolution and per-request rendering.
- *
- *
All expensive reflection is done once and cached per handler class.
- */
-final class ViewRuntime {
- private static final String GLOBAL_NAMESPACE = "global";
-
- private final ViewEngineAdapter adapter;
- private final List globals;
- private final ConcurrentHashMap, ViewTargetResolver.ResolvedView> resolvedCache = new ConcurrentHashMap<>();
-
- ViewRuntime(ViewEngineAdapter adapter, List globals) {
- this.adapter = adapter;
- this.globals = globals;
- }
-
- ViewTargetResolver.ResolvedView resolve(Class> handlerClass) {
- ViewTargetResolver.ResolvedView cached = resolvedCache.get(handlerClass);
- if (cached != null) return cached;
- ViewTargetResolver.ResolvedView resolved = ViewTargetResolver.resolve(handlerClass, adapter.capabilities());
- if (resolved == null) {
- throw new IllegalStateException("ViewHandler " + handlerClass.getName()
- + " must declare @Page or @Partial");
- }
- resolvedCache.put(handlerClass, resolved);
- return resolved;
- }
-
- Object render(ViewHandler handler, ViewTargetResolver.ResolvedView resolved, Request req, Response res) throws Exception {
- if (resolved == null) {
- throw new IllegalStateException("ViewHandler " + handler.getClass().getName()
- + " must declare @Page or @Partial");
- }
-
- RenderOutput out = render(resolved, req, res, handler.render(req, res));
- return out.body();
- }
-
- RenderOutput render(ViewTargetResolver.ResolvedView resolved, Request req, Response res, ViewModel local) throws Exception {
- ViewModel merged = merge(req, local);
- RenderOutput out = adapter.render(resolved.target(), merged.toMap(), req, res);
-
- if (out.contentType() != null) {
- res.type(out.contentType());
- } else {
- res.type(resolved.contentType());
- }
- return out;
- }
-
- ViewModel merge(Request req, ViewModel local) {
- // Single-pass merge: reserved global namespace + local model.
- // We avoid intermediate ViewModel allocations on the hot path.
- LinkedHashMap values = null;
-
- if (!globals.isEmpty()) {
- LinkedHashMap globalMap = new LinkedHashMap<>();
- for (GlobalBinding binding : globals) {
- Object resolved = binding.resolver().apply(req);
- globalMap.put(binding.key(), ViewModel.unwrapValue(resolved));
- }
- if (!globalMap.isEmpty()) {
- if (values == null) values = new LinkedHashMap<>();
- values.put(GLOBAL_NAMESPACE, Collections.unmodifiableMap(globalMap));
- }
- }
-
- if (local != null) {
- var localMap = local.toMap();
- if (localMap.containsKey(GLOBAL_NAMESPACE)) {
- throw new IllegalStateException("ViewModel key 'global' is reserved for framework globals");
- }
- if (values == null) return ViewModel.copyOf(local);
- values.putAll(localMap);
- }
-
- if (values == null || values.isEmpty()) return ViewModel.empty();
- return ViewModel.owned(values);
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTarget.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTarget.java
deleted file mode 100644
index a3a976d..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTarget.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package dev.relism.ext.view;
-
-/**
- * Resolved render target for one handler class.
- *
- * @param kind page or partial render
- * @param template template identifier/path
- * @param slot optional partial slot selector
- */
-public record ViewTarget(
- ViewKind kind,
- String template,
- String slot
-) {}
diff --git a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTargetResolver.java b/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTargetResolver.java
deleted file mode 100644
index e51d8e3..0000000
--- a/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTargetResolver.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.http.ContentType;
-import dev.relism.routing.Route;
-import dev.relism.routing.Routes;
-
-/** Boot-time resolver that maps handler annotations to concrete render targets. */
-final class ViewTargetResolver {
-
- private ViewTargetResolver() {}
-
- static ResolvedView resolve(Class> handlerClass, EngineCapabilities capabilities) {
- Page page = find(handlerClass, Page.class);
- Partial partial = find(handlerClass, Partial.class);
-
- int count = (page != null ? 1 : 0) + (partial != null ? 1 : 0);
- if (count == 0) return null;
-
- Route route = Routes.of(handlerClass);
- if (route == null) {
- throw new IllegalStateException("View handler " + handlerClass.getName()
- + " has view annotation but no route annotation (@Route/@GET/@POST/...)");
- }
-
- if (count > 1) {
- throw new IllegalStateException("Handler " + handlerClass.getName()
- + " route " + route.method() + " " + route.path()
- + " declares multiple view annotations. Use exactly one of @Page, @Partial");
- }
-
- if (page != null) {
- String template = page.value() == null ? "" : page.value().trim();
- if (template.isEmpty()) {
- throw new IllegalStateException("Handler " + handlerClass.getName()
- + " route " + route.method() + " " + route.path()
- + " uses @Page with an empty template name");
- }
- return new ResolvedView(new ViewTarget(ViewKind.PAGE, template, ""), ContentType.TEXT_HTML);
- }
-
- if (partial != null) {
- String template = partial.template() == null ? "" : partial.template().trim();
- if (template.isEmpty()) {
- throw new IllegalStateException("Handler " + handlerClass.getName()
- + " route " + route.method() + " " + route.path()
- + " uses @Partial with an empty template name");
- }
- String slot = partial.slot() == null ? "" : partial.slot().trim();
- if (!slot.isEmpty() && !capabilities.supportsPartialSlot()) {
- throw new IllegalStateException("Handler " + handlerClass.getName()
- + " route " + route.method() + " " + route.path()
- + " uses @Partial(slot=\"" + slot + "\") but current engine does not support partial slots");
- }
- return new ResolvedView(new ViewTarget(ViewKind.PARTIAL, template, slot), ContentType.TEXT_HTML);
- }
-
- return null;
- }
-
- private static A find(Class> cls, Class type) {
- while (cls != null && !cls.equals(Object.class)) {
- A a = cls.getAnnotation(type);
- if (a != null) return a;
- cls = cls.getSuperclass();
- }
- return null;
- }
-
- record ResolvedView(ViewTarget target, ContentType contentType) {}
-}
diff --git a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ThymeleafEngineTest.java b/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ThymeleafEngineTest.java
deleted file mode 100644
index cd89b9d..0000000
--- a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ThymeleafEngineTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.http.ContentType;
-import dev.relism.models.Response;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class ThymeleafEngineTest {
-
- @Test
- void render_page_resolvesTemplateAndLinks() {
- ThymeleafEngine engine = new ThymeleafEngine(false);
-
- RenderOutput out = engine.render(
- new ViewTarget(ViewKind.PAGE, "pages/home", ""),
- ViewModel.empty().with("title", "Home").with("id", 42).with("page", 2).toMap(),
- null,
- new Response(200, ContentType.JSON)
- );
-
- assertEquals(ContentType.TEXT_HTML, out.contentType());
- assertTrue(out.body().contains("Home"));
- assertTrue(out.body().contains("/users/42?page=2"));
- }
-
- @Test
- void render_partial_usesExplicitSlot() {
- ThymeleafEngine engine = new ThymeleafEngine(false);
-
- RenderOutput out = engine.render(
- new ViewTarget(ViewKind.PARTIAL, "pages/home", "rows"),
- ViewModel.empty().with("id", 42).toMap(),
- null,
- new Response(200, ContentType.JSON)
- );
-
- assertTrue(out.body().contains("row-42"));
- }
-
- @Test
- void render_partial_usesDefaultContentSlotWhenBlank() {
- ThymeleafEngine engine = new ThymeleafEngine(false);
-
- RenderOutput out = engine.render(
- new ViewTarget(ViewKind.PARTIAL, "pages/home", " "),
- ViewModel.empty().with("title", "ContentSlot").toMap(),
- null,
- new Response(200, ContentType.JSON)
- );
-
- assertTrue(out.body().contains("content-ContentSlot"));
- }
-
- @Test
- void capabilities_supportSlotSelection() {
- ThymeleafEngine engine = new ThymeleafEngine(true);
- assertTrue(engine.capabilities().supportsPartialSlot());
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewExtensionContractTest.java b/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewExtensionContractTest.java
deleted file mode 100644
index 01b34f3..0000000
--- a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewExtensionContractTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.extension.FlashContext;
-import dev.relism.extension.AnnotationProcessor;
-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 java.util.List;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-class ViewExtensionContractTest {
-
- @GET("/plain")
- @Page("pages/plain")
- static final class PlainPageHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @GET("/view")
- @Page("pages/view")
- static final class ViewPageHandler extends ViewHandler {
- @Override
- public ViewModel render(Request req) {
- return ViewModel.empty();
- }
- }
-
- @Test
- void processor_rejectsViewAnnotationOnNonViewHandler() {
- FlashContext ctx = new FlashContext();
- new ViewExtension(new NoopAdapter()).provide(ctx);
-
- assertThrows(IllegalStateException.class,
- () -> processors(ctx).forEach(p -> p.process(PlainPageHandler.class)));
- }
-
- @Test
- void processor_acceptsViewHandlerWithViewAnnotation() {
- FlashContext ctx = new FlashContext();
- new ViewExtension(new NoopAdapter()).provide(ctx);
-
- assertDoesNotThrow(() -> processors(ctx).forEach(p -> p.process(ViewPageHandler.class)));
- }
-
- @Test
- void constructor_withEngineType_buildsAndProvidesRuntime() {
- FlashContext ctx = new FlashContext();
- ViewExtension extension = new ViewExtension(ViewEngineType.THYMELEAF);
-
- extension.provide(ctx);
-
- assertNotNull(ctx.require(ViewRuntime.class));
- assertNotNull(ctx.require(ViewEngineAdapter.class));
- }
-
- @Test
- void addGlobal_nullResolver_throws() {
- ViewExtension extension = new ViewExtension(new NoopAdapter());
- assertThrows(NullPointerException.class, () -> extension.addGlobal("appName", null));
- }
-
- @Test
- void addGlobal_invalidKey_throws() {
- ViewExtension extension = new ViewExtension(new NoopAdapter());
- assertThrows(IllegalArgumentException.class, () -> extension.addGlobal("", req -> "x"));
- assertThrows(IllegalArgumentException.class, () -> extension.addGlobal("global", req -> "x"));
- assertThrows(IllegalArgumentException.class, () -> extension.addGlobal("a.b", req -> "x"));
- }
-
- @Test
- void constructor_withNullAdapter_throws() {
- assertThrows(NullPointerException.class, () -> new ViewExtension((ViewEngineAdapter) null));
- }
-
- @SuppressWarnings("unchecked")
- private static List processors(FlashContext ctx) {
- try {
- Method m = FlashContext.class.getDeclaredMethod("processors");
- m.setAccessible(true);
- return ((List) m.invoke(ctx)).stream()
- .filter(p -> p.getClass().getName().contains("ViewExtension"))
- .collect(Collectors.toList());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- private static final class NoopAdapter implements ViewEngineAdapter {
- @Override
- public EngineCapabilities capabilities() {
- return new EngineCapabilities(true);
- }
-
- @Override
- public RenderOutput render(ViewTarget target, java.util.Map model,
- Request req,
- Response res) {
- return RenderOutput.html("");
- }
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewHandlerLifecycleTest.java b/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewHandlerLifecycleTest.java
deleted file mode 100644
index c0ec348..0000000
--- a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewHandlerLifecycleTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package dev.relism.ext.view;
-
-import dev.relism.extension.FlashContext;
-import dev.relism.http.ContentType;
-import dev.relism.models.Request;
-import dev.relism.models.Response;
-import dev.relism.routing.GET;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class ViewHandlerLifecycleTest {
-
- @GET("/lifecycle")
- @Page("pages/home")
- static final class LifecycleHandler extends ViewHandler {
- boolean onViewInitCalled;
- DummyService service;
-
- @Override
- protected void onViewInit() {
- onViewInitCalled = true;
- service = require(DummyService.class);
- }
-
- @Override
- public ViewModel render(Request req) {
- return ViewModel.of("title", service.value());
- }
- }
-
- static final class DummyService {
- String value() { return "ok"; }
- }
-
- @Test
- void onInit_resolvesRuntime_and_onViewInit_runs_once() {
- FlashContext ctx = new FlashContext();
- ctx.provide(DummyService.class, new DummyService());
- ctx.provide(ViewRuntime.class, new ViewRuntime(new EchoAdapter(), List.of()));
-
- LifecycleHandler handler = new LifecycleHandler();
- handler.bind(ctx);
-
- assertTrue(handler.onViewInitCalled);
- assertEquals("ok", handler.service.value());
- }
-
- @Test
- void handle_rendersThroughRuntime() throws Exception {
- FlashContext ctx = new FlashContext();
- ctx.provide(DummyService.class, new DummyService());
- ctx.provide(ViewRuntime.class, new ViewRuntime(new EchoAdapter(), List.of()));
-
- LifecycleHandler handler = new LifecycleHandler();
- handler.bind(ctx);
- Response res = new Response(200, ContentType.JSON);
-
- Object out = handler.handle(null, res);
-
- assertEquals("ok", out);
- assertEquals(new String(ContentType.TEXT_HTML.getBytes()), new String(res.getContentType()));
- }
-
- private static final class EchoAdapter implements ViewEngineAdapter {
- @Override
- public EngineCapabilities capabilities() {
- return EngineCapabilities.NONE;
- }
-
- @Override
- public RenderOutput render(ViewTarget target, java.util.Map model,
- Request req,
- Response res) {
- return RenderOutput.html(String.valueOf(model.get("title")));
- }
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewRuntimeGlobalsTest.java b/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewRuntimeGlobalsTest.java
deleted file mode 100644
index 4c4aaee..0000000
--- a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewRuntimeGlobalsTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package dev.relism.ext.view;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class ViewRuntimeGlobalsTest {
-
- private static final GlobalBinding APP = new GlobalBinding("appName", req -> "flash");
- private static final GlobalBinding PATH = new GlobalBinding("requestPath", req -> "/x");
-
- @Test
- void merge_globalsAreNestedUnderReservedNamespace() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of(
- APP,
- PATH
- ));
-
- ViewModel merged = runtime.merge(null, ViewModel.of("title", "dashboard"));
-
- Map, ?> global = (Map, ?>) merged.toMap().get("global");
- assertEquals("flash", global.get("appName"));
- assertEquals("/x", global.get("requestPath"));
- assertEquals("dashboard", merged.toMap().get("title"));
- }
-
- @Test
- void merge_nullLocal_keepsGlobals() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of(
- new GlobalBinding("signedIn", req -> true)
- ));
-
- ViewModel merged = runtime.merge(null, null);
-
- Map, ?> global = (Map, ?>) merged.toMap().get("global");
- assertEquals(true, global.get("signedIn"));
- }
-
- @Test
- void merge_withoutGlobals_returnsCopyOfLocal() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of());
- ViewModel local = ViewModel.of("k", "v");
-
- ViewModel merged = runtime.merge(null, local);
-
- assertEquals("v", merged.toMap().get("k"));
- }
-
- @Test
- void merge_withNullGlobalsAndNullLocal_returnsEmpty() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of());
-
- ViewModel merged = runtime.merge(null, null);
-
- assertTrue(merged.toMap().isEmpty());
- }
-
- @Test
- void merge_localGlobalNamespace_throws() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of(APP));
-
- assertThrows(IllegalStateException.class,
- () -> runtime.merge(null, ViewModel.of("global", Map.of("x", 1))));
- }
-
- @Test
- void resolve_withoutViewAnnotation_failsFast() {
- ViewRuntime runtime = new ViewRuntime(new NoopAdapter(), List.of());
-
- assertThrows(IllegalStateException.class,
- () -> runtime.resolve(NoViewHandler.class));
- }
-
- @Test
- void render_prefersAdapterContentTypeWhenProvided() throws Exception {
- ViewRuntime runtime = new ViewRuntime(new PlainTextAdapter(), List.of());
- ViewTargetResolver.ResolvedView resolved = new ViewTargetResolver.ResolvedView(
- new ViewTarget(ViewKind.PAGE, "pages/home", ""),
- dev.relism.http.ContentType.TEXT_HTML
- );
- dev.relism.models.Response res = new dev.relism.models.Response(200, dev.relism.http.ContentType.JSON);
-
- runtime.render(resolved, null, res, ViewModel.of("a", 1));
-
- assertEquals(new String(dev.relism.http.ContentType.TEXT_PLAIN.getBytes()), new String(res.getContentType()));
- }
-
- @Test
- void render_usesResolvedDefaultContentTypeWhenAdapterOmitsIt() throws Exception {
- ViewRuntime runtime = new ViewRuntime(new NoTypeAdapter(), List.of());
- ViewTargetResolver.ResolvedView resolved = new ViewTargetResolver.ResolvedView(
- new ViewTarget(ViewKind.PAGE, "pages/home", ""),
- dev.relism.http.ContentType.TEXT_HTML
- );
- dev.relism.models.Response res = new dev.relism.models.Response(200, dev.relism.http.ContentType.JSON);
-
- runtime.render(resolved, null, res, ViewModel.of("a", 1));
-
- assertEquals(new String(dev.relism.http.ContentType.TEXT_HTML.getBytes()), new String(res.getContentType()));
- }
-
- static final class NoViewHandler extends dev.relism.models.RequestHandler {
- @Override
- public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
- return null;
- }
- }
-
- private static final class NoopAdapter implements ViewEngineAdapter {
- @Override
- public EngineCapabilities capabilities() {
- return EngineCapabilities.NONE;
- }
-
- @Override
- public RenderOutput render(ViewTarget target, java.util.Map model,
- dev.relism.models.Request req,
- dev.relism.models.Response res) {
- return RenderOutput.html("");
- }
- }
-
- private static final class NoTypeAdapter implements ViewEngineAdapter {
- @Override
- public EngineCapabilities capabilities() {
- return EngineCapabilities.NONE;
- }
-
- @Override
- public RenderOutput render(ViewTarget target, java.util.Map model,
- dev.relism.models.Request req,
- dev.relism.models.Response res) {
- return new RenderOutput("", null);
- }
- }
-
- private static final class PlainTextAdapter implements ViewEngineAdapter {
- @Override
- public EngineCapabilities capabilities() {
- return EngineCapabilities.NONE;
- }
-
- @Override
- public RenderOutput render(ViewTarget target, java.util.Map model,
- dev.relism.models.Request req,
- dev.relism.models.Response res) {
- return new RenderOutput("", dev.relism.http.ContentType.TEXT_PLAIN);
- }
- }
-}
diff --git a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewTargetResolverTest.java b/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewTargetResolverTest.java
deleted file mode 100644
index bb33cd0..0000000
--- a/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewTargetResolverTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package dev.relism.ext.view;
-
-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 ViewTargetResolverTest {
-
- @GET("/home")
- @Page("pages/home")
- static class PageHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @GET("/partial")
- @Partial(template = "fragments/row", slot = "row")
- static class SlotPartialHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @GET("/partial-default-slot")
- @Partial(template = "fragments/card")
- static class DefaultSlotPartialHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @GET("/bad")
- @Page("a")
- @Partial(template = "b")
- static class ConflictingHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @Page("pages/no-route")
- static class NoRouteHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @Test
- void resolve_page_returnsPageTarget() {
- ViewTargetResolver.ResolvedView resolved = ViewTargetResolver.resolve(PageHandler.class, new EngineCapabilities(true));
-
- assertNotNull(resolved);
- assertEquals(ViewKind.PAGE, resolved.target().kind());
- assertEquals("pages/home", resolved.target().template());
- }
-
- @Test
- void resolve_slotPartial_withoutCapability_failsFast() {
- assertThrows(IllegalStateException.class,
- () -> ViewTargetResolver.resolve(SlotPartialHandler.class, EngineCapabilities.NONE));
- }
-
- @Test
- void resolve_partial_withoutSlot_defaultsToEmptySlot() {
- ViewTargetResolver.ResolvedView resolved = ViewTargetResolver.resolve(DefaultSlotPartialHandler.class, new EngineCapabilities(true));
-
- assertNotNull(resolved);
- assertEquals(ViewKind.PARTIAL, resolved.target().kind());
- assertEquals("", resolved.target().slot());
- }
-
- @GET("/blank-page")
- @Page(" ")
- static class BlankPageHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @GET("/blank-partial")
- @Partial(template = " ")
- static class BlankPartialTemplateHandler extends RequestHandler {
- @Override
- public Object handle(Request request, Response response) {
- return null;
- }
- }
-
- @Test
- void resolve_multipleViewAnnotations_fails() {
- assertThrows(IllegalStateException.class,
- () -> ViewTargetResolver.resolve(ConflictingHandler.class, new EngineCapabilities(true)));
- }
-
- @Test
- void resolve_viewAnnotationWithoutRoute_fails() {
- assertThrows(IllegalStateException.class,
- () -> ViewTargetResolver.resolve(NoRouteHandler.class, new EngineCapabilities(true)));
- }
-
- @Test
- void resolve_blankPageTemplate_fails() {
- assertThrows(IllegalStateException.class,
- () -> ViewTargetResolver.resolve(BlankPageHandler.class, new EngineCapabilities(true)));
- }
-
- @Test
- void resolve_blankPartialTemplate_fails() {
- assertThrows(IllegalStateException.class,
- () -> ViewTargetResolver.resolve(BlankPartialTemplateHandler.class, new EngineCapabilities(true)));
- }
-}
diff --git a/flash-extensions/flash-web-bundler/pom.xml b/flash-extensions/flash-web-bundler/pom.xml
index 57fe221..e425d0d 100644
--- a/flash-extensions/flash-web-bundler/pom.xml
+++ b/flash-extensions/flash-web-bundler/pom.xml
@@ -7,7 +7,7 @@
dev.relism
flash-extensions
- 1.1-indev5
+ 1.1-indev6
flash-web-bundler
diff --git a/flash-extensions/pom.xml b/flash-extensions/pom.xml
index bb79a62..0cb6949 100644
--- a/flash-extensions/pom.xml
+++ b/flash-extensions/pom.xml
@@ -7,7 +7,7 @@
dev.relism
flash-parent
- 1.1-indev5
+ 1.1-indev6
flash-extensions
@@ -18,7 +18,9 @@
flash-ext-openapi
flash-ext-oidc
flash-ext-routeviewer
- flash-ext-view
+ flash-ext-view-core
+ flash-ext-view-jte
+ flash-ext-view-thymeleaf
flash-ext-limiter
flash-web-bundler
@@ -30,6 +32,11 @@
flash-ext-jackson
${project.version}