fix: enhance global key validation and update template parameters

This commit is contained in:
Relism
2026-04-29 10:31:14 +02:00
parent 0e2dad23e5
commit f310646868
6 changed files with 26 additions and 30 deletions
@@ -18,9 +18,13 @@ public abstract class BaseViewExtension<TTarget> implements FlashExtension {
if (k.isEmpty()) { if (k.isEmpty()) {
throw new IllegalArgumentException("global key must not be blank"); throw new IllegalArgumentException("global key must not be blank");
} }
if (k.equals("global") || k.contains(".")) { if (k.contains(".")) {
throw new IllegalArgumentException("global key must be a simple key (no dots), received: " + key); throw new IllegalArgumentException("global key must be a simple key (no dots), received: " + key);
} }
boolean duplicate = globals.stream().anyMatch(g -> g.key().equals(k));
if (duplicate) {
throw new IllegalArgumentException("global key '" + k + "' is already registered");
}
globals.add(new GlobalValue(k, Objects.requireNonNull(resolver, "global resolver must not be null"))); globals.add(new GlobalValue(k, Objects.requireNonNull(resolver, "global resolver must not be null")));
return this; return this;
} }
@@ -1,11 +1,10 @@
package gg.jte.generated.ondemand.pages; package gg.jte.generated.ondemand.pages;
import dev.relism.flash.ext.view.jte.model.HomePage; import dev.relism.flash.ext.view.jte.model.HomePage;
import java.util.Map;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public final class JtehomeGenerated { public final class JtehomeGenerated {
public static final String JTE_NAME = "pages/home.jte"; public static final String JTE_NAME = "pages/home.jte";
public static final int[] JTE_LINE_INFO = {0,0,1,2,2,2,2,6,6,6,6,7,7,7,8,8,8,9,9,9,10,10,10,2,3,4,4,4,4}; public static final int[] JTE_LINE_INFO = {0,0,1,1,1,1,5,5,5,5,6,6,6,7,7,7,8,8,8,8,8,8,1,2,3,3,3,3};
public static void render(gg.jte.html.HtmlTemplateOutput jteOutput, gg.jte.html.HtmlInterceptor jteHtmlInterceptor, HomePage page, String build, Map<String, Object> global) { public static void render(gg.jte.html.HtmlTemplateOutput jteOutput, gg.jte.html.HtmlInterceptor jteHtmlInterceptor, HomePage page, String build, String appName) {
jteOutput.writeContent("\n<h1>"); jteOutput.writeContent("\n<h1>");
jteOutput.setContext("h1", null); jteOutput.setContext("h1", null);
jteOutput.writeUserContent(page.title()); jteOutput.writeUserContent(page.title());
@@ -17,13 +16,13 @@ public final class JtehomeGenerated {
jteOutput.writeUserContent(build); jteOutput.writeUserContent(build);
jteOutput.writeContent("</small>\n<small>"); jteOutput.writeContent("</small>\n<small>");
jteOutput.setContext("small", null); jteOutput.setContext("small", null);
jteOutput.writeUserContent((String) global.get("appName")); jteOutput.writeUserContent(appName);
jteOutput.writeContent("</small>\n"); jteOutput.writeContent("</small>");
} }
public static void renderMap(gg.jte.html.HtmlTemplateOutput jteOutput, gg.jte.html.HtmlInterceptor jteHtmlInterceptor, java.util.Map<String, Object> params) { public static void renderMap(gg.jte.html.HtmlTemplateOutput jteOutput, gg.jte.html.HtmlInterceptor jteHtmlInterceptor, java.util.Map<String, Object> params) {
HomePage page = (HomePage)params.get("page"); HomePage page = (HomePage)params.get("page");
String build = (String)params.get("build"); String build = (String)params.get("build");
Map<String, Object> global = (Map<String, Object>)params.get("global"); String appName = (String)params.get("appName");
render(jteOutput, jteHtmlInterceptor, page, build, global); render(jteOutput, jteHtmlInterceptor, page, build, appName);
} }
} }
@@ -22,7 +22,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
final class JteRuntime implements ViewRuntimeBridge<JteTarget> { final class JteRuntime implements ViewRuntimeBridge<JteTarget> {
private static final String GLOBAL_NAMESPACE = "global";
private final JteSettings settings; private final JteSettings settings;
private final List<GlobalValue> globals; private final List<GlobalValue> globals;
@@ -60,30 +59,25 @@ final class JteRuntime implements ViewRuntimeBridge<JteTarget> {
} }
private ViewModel merge(Request req, ViewModel local) { private ViewModel merge(Request req, ViewModel local) {
LinkedHashMap<String, Object> values = null; LinkedHashMap<String, Object> values = new LinkedHashMap<>();
if (!globals.isEmpty()) {
LinkedHashMap<String, Object> globalMap = new LinkedHashMap<>();
for (GlobalValue binding : globals) { for (GlobalValue binding : globals) {
Object resolved = binding.resolver().apply(req); Object resolved = binding.resolver().apply(req);
globalMap.put(binding.key(), ViewModel.unwrapValue(resolved)); values.put(binding.key(), ViewModel.unwrapValue(resolved));
}
if (!globalMap.isEmpty()) {
values = new LinkedHashMap<>();
values.put(GLOBAL_NAMESPACE, Collections.unmodifiableMap(globalMap));
}
} }
if (local != null) { if (local != null) {
Map<String, Object> localMap = local.toMap(); Map<String, Object> localMap = local.toMap();
if (localMap.containsKey(GLOBAL_NAMESPACE)) { for (String key : localMap.keySet()) {
throw new IllegalStateException("ViewModel key 'global' is reserved for framework globals"); if (values.containsKey(key)) {
throw new IllegalStateException(
"ViewModel key '" + key + "' conflicts with a registered global");
}
} }
if (values == null) return ViewModel.copyOf(local);
values.putAll(localMap); values.putAll(localMap);
} }
if (values == null || values.isEmpty()) return ViewModel.empty(); if (values.isEmpty()) return ViewModel.empty();
return ViewModel.owned(values); return ViewModel.owned(values);
} }
@@ -38,7 +38,7 @@ class JteRuntimeTest {
} }
@Test @Test
void render_rejectsReservedGlobalKeyInLocalModel() { void render_rejectsConflictingLocalKey() {
JteRuntime runtime = new JteRuntime( JteRuntime runtime = new JteRuntime(
JteSettings.builder().templateRoot("/templates").developmentMode(true).build(), JteSettings.builder().templateRoot("/templates").developmentMode(true).build(),
List.of(new GlobalValue("appName", req -> "Flash")) List.of(new GlobalValue("appName", req -> "Flash"))
@@ -48,12 +48,12 @@ class JteRuntimeTest {
runtime.render(new JteHandler() { runtime.render(new JteHandler() {
@Override @Override
public ViewModel render(Request req) { public ViewModel render(Request req) {
return ViewModel.of("global", "bad"); return ViewModel.of("appName", "override"); // collide con global
} }
}, new JteTarget("pages/home.jte", ContentType.TEXT_HTML), null, new Response(200, ContentType.JSON)) }, new JteTarget("pages/home.jte", ContentType.TEXT_HTML), null, new Response(200, ContentType.JSON))
); );
assertTrue(ex.getMessage().contains("reserved")); assertTrue(ex.getMessage().contains("appName"));
} }
@Test @Test
@@ -1,10 +1,9 @@
@import dev.relism.flash.ext.view.jte.model.HomePage @import dev.relism.flash.ext.view.jte.model.HomePage
@import java.util.Map
@param HomePage page @param HomePage page
@param String build @param String build
@param Map<String, Object> global @param String appName
<h1>${page.title()}</h1> <h1>${page.title()}</h1>
<p>${page.author()}</p> <p>${page.author()}</p>
<small>${build}</small> <small>${build}</small>
<small>${(String) global.get("appName")}</small> <small>${appName}</small>