fix(ext-view): clear cross-engine error, propagate globals across JteExtension builder chain
- BaseViewExtension: wrap the ViewRuntimeBridge provider collision with a message naming the real constraint (one view engine per FlashApp) instead of the generic "duplicate provider" error. - BaseViewExtension/JteExtension: carry registered globals across JteExtension's immutable settings builders (templateRoot/serveStatics/staticPrefix/withStaticCors/ staticCors). addGlobal() called before any of those used to be silently dropped, since each builder method returned a fresh instance with an empty globals list. - Correct flash-ext-view-jte docs (README, architecture.md, model-and-globals.md): globals merge flat with one typed @param per key, not under a global.* namespace like Thymeleaf — the docs previously claimed the same reserved namespace for both engines, which doesn't match JteRuntime's actual merge behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9de2800a83
commit
4feabc45d5
@@ -8,7 +8,7 @@ This module keeps jte semantics front and center:
|
||||
- `JteHandler`
|
||||
- `@Template`
|
||||
- `ViewModel` from `flash-ext-view-core`
|
||||
- `global.*` reserved namespace
|
||||
- globals merged flat into the model, one typed `@param` per global
|
||||
|
||||
The extension mirrors `gg.jte.ContentType` into Flash HTTP content type:
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
2. **Request-time**
|
||||
- Handler builds local `ViewModel`.
|
||||
- Runtime injects globals under reserved `global` namespace and merges local model.
|
||||
- Runtime merges globals and local model into one flat parameter map (fails fast on key collision).
|
||||
- jte renders template into `StringOutput`.
|
||||
|
||||
3. **Static assets (optional, enabled by default)**
|
||||
|
||||
@@ -28,6 +28,21 @@ Register globals in extension setup:
|
||||
new JteExtension().addGlobal("appName", req -> "Flash")
|
||||
```
|
||||
|
||||
Globals are available under `global` namespace in templates.
|
||||
Globals are merged **flat** into the model — same level as local `ViewModel` keys, not
|
||||
under a `global.*` namespace. jte templates are statically typed, so each template declares
|
||||
one `@param` per global it actually uses, named exactly like the global key:
|
||||
|
||||
`global` is reserved and cannot be used as local model key.
|
||||
```jte
|
||||
@param String appName
|
||||
|
||||
<span>${appName}</span>
|
||||
```
|
||||
|
||||
A template that doesn't declare `appName` simply never sees it — no need to declare every
|
||||
global on every page, only the ones a given template actually uses.
|
||||
|
||||
A local `ViewModel` key that collides with a registered global name fails fast with
|
||||
`IllegalStateException` at render time, so a typo can't silently shadow a global.
|
||||
|
||||
Every registered global is still resolved on **every** render regardless of whether the
|
||||
target template declares it, so keep resolvers cheap (no blocking I/O, no heavy allocation).
|
||||
|
||||
+7
-6
@@ -29,31 +29,32 @@ public final class JteExtension extends BaseViewExtension<JteTarget> {
|
||||
this.settings = builder.build();
|
||||
}
|
||||
|
||||
private JteExtension(JteSettings settings) {
|
||||
private JteExtension(JteSettings settings, List<GlobalValue> seedGlobals) {
|
||||
super(seedGlobals);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public JteExtension templateRoot(String templateRoot) {
|
||||
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build());
|
||||
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build(), globals());
|
||||
}
|
||||
|
||||
public JteExtension serveStatics(boolean serveStatics) {
|
||||
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build());
|
||||
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build(), globals());
|
||||
}
|
||||
|
||||
public JteExtension staticPrefix(String staticPrefix) {
|
||||
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build());
|
||||
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build(), globals());
|
||||
}
|
||||
|
||||
public JteExtension withStaticCors() {
|
||||
return new JteExtension(settings.toBuilder().enableStaticCors(true).build());
|
||||
return new JteExtension(settings.toBuilder().enableStaticCors(true).build(), globals());
|
||||
}
|
||||
|
||||
public JteExtension staticCors(Consumer<JteSettings.Builder> corsConfig) {
|
||||
JteSettings.Builder builder = settings.toBuilder();
|
||||
builder.enableStaticCors(true);
|
||||
corsConfig.accept(builder);
|
||||
return new JteExtension(builder.build());
|
||||
return new JteExtension(builder.build(), globals());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+38
@@ -1,6 +1,11 @@
|
||||
package dev.relism.flash.ext.view.jte;
|
||||
|
||||
import dev.relism.flash.ext.view.core.RenderedView;
|
||||
import dev.relism.flash.ext.view.core.ViewModel;
|
||||
import dev.relism.flash.ext.view.core.ViewRuntimeBridge;
|
||||
import dev.relism.flash.ext.view.jte.model.HomePage;
|
||||
import dev.relism.flash.extension.FlashContext;
|
||||
import dev.relism.flash.http.ContentType;
|
||||
import dev.relism.flash.models.Request;
|
||||
import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.models.Response;
|
||||
@@ -89,6 +94,39 @@ class JteExtensionTest {
|
||||
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addGlobal_survives_subsequent_settings_builder_calls() throws Exception {
|
||||
// addGlobal() before a chained settings method (templateRoot/serveStatics/...) used to
|
||||
// be silently dropped: those methods return a *new* JteExtension instance, and the
|
||||
// globals list lived on the instance, not the settings being carried forward.
|
||||
JteExtension ext = new JteExtension(cfg -> cfg.developmentMode(true))
|
||||
.addGlobal("appName", req -> "FlashLab")
|
||||
.templateRoot("templates")
|
||||
.serveStatics(false);
|
||||
|
||||
TestRegistrar app = new TestRegistrar();
|
||||
FlashContext ctx = new FlashContext();
|
||||
ext.configure(app, ctx);
|
||||
ctx.complete();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ViewRuntimeBridge<JteTarget> runtime = (ViewRuntimeBridge<JteTarget>) ctx.require(ViewRuntimeBridge.class);
|
||||
|
||||
JteHandler handler = new JteHandler() {
|
||||
@Override
|
||||
public ViewModel render(Request req) {
|
||||
return ViewModel.empty()
|
||||
.with("page", new HomePage("Flash + jte", "elorc"))
|
||||
.with("build", "dev");
|
||||
}
|
||||
};
|
||||
JteTarget target = new JteTarget("pages/home.jte", ContentType.TEXT_HTML);
|
||||
RenderedView out = runtime.render(handler, target, request("/", null, null, null),
|
||||
new Response(200, ContentType.JSON));
|
||||
|
||||
assertTrue(out.body().contains("FlashLab"), "global registered before templateRoot() must survive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void routes_register_static_wildcard_when_enabled() {
|
||||
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));
|
||||
|
||||
Reference in New Issue
Block a user