refactor(ext-oidc): replace auth modules with security extensions #17
+23
@@ -15,6 +15,23 @@ import java.util.function.Function;
|
|||||||
public abstract class BaseViewExtension<TTarget> implements FlashExtension {
|
public abstract class BaseViewExtension<TTarget> implements FlashExtension {
|
||||||
private final List<GlobalValue> globals = new ArrayList<>();
|
private final List<GlobalValue> globals = new ArrayList<>();
|
||||||
|
|
||||||
|
protected BaseViewExtension() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds this instance with globals carried over from a prior one. Subclasses whose
|
||||||
|
* fluent settings methods return a new instance (e.g. {@code JteExtension.templateRoot(...)})
|
||||||
|
* must route through this constructor — otherwise {@link #addGlobal} calls made before
|
||||||
|
* such a method silently vanish, since the new instance would start with an empty list.
|
||||||
|
*/
|
||||||
|
protected BaseViewExtension(List<GlobalValue> seedGlobals) {
|
||||||
|
globals.addAll(seedGlobals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshot of globals registered so far — for subclasses to carry over into a new instance. */
|
||||||
|
protected final List<GlobalValue> globals() {
|
||||||
|
return List.copyOf(globals);
|
||||||
|
}
|
||||||
|
|
||||||
public BaseViewExtension<TTarget> addGlobal(String key, Function<Request, Object> resolver) {
|
public BaseViewExtension<TTarget> addGlobal(String key, Function<Request, Object> resolver) {
|
||||||
String k = Objects.requireNonNull(key, "global key must not be null").trim();
|
String k = Objects.requireNonNull(key, "global key must not be null").trim();
|
||||||
if (k.isEmpty()) {
|
if (k.isEmpty()) {
|
||||||
@@ -34,7 +51,13 @@ public abstract class BaseViewExtension<TTarget> implements FlashExtension {
|
|||||||
@Override
|
@Override
|
||||||
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
|
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
|
||||||
ViewRuntimeBridge<TTarget> runtime = createRuntime(List.copyOf(globals));
|
ViewRuntimeBridge<TTarget> runtime = createRuntime(List.copyOf(globals));
|
||||||
|
try {
|
||||||
ctx.provide(ViewRuntimeBridge.class, runtime);
|
ctx.provide(ViewRuntimeBridge.class, runtime);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
throw new IllegalStateException("Another flash-ext-view implementation is already installed in "
|
||||||
|
+ "this FlashApp. Only one view engine (e.g. ThymeleafExtension or JteExtension, not both) "
|
||||||
|
+ "can be active per app — install a single implementation.", e);
|
||||||
|
}
|
||||||
ctx.addAnnotationProcessor(handlerClass -> {
|
ctx.addAnnotationProcessor(handlerClass -> {
|
||||||
validateHandlerClass(handlerClass);
|
validateHandlerClass(handlerClass);
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.relism.flash.ext.view.core;
|
||||||
|
|
||||||
|
import dev.relism.flash.extension.FlashContext;
|
||||||
|
import dev.relism.flash.models.RequestHandler;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class BaseViewExtensionTest {
|
||||||
|
|
||||||
|
private static final class FakeViewExtension extends BaseViewExtension<Object> {
|
||||||
|
@Override
|
||||||
|
protected ViewRuntimeBridge<Object> createRuntime(List<GlobalValue> globals) {
|
||||||
|
return new ViewRuntimeBridge<>() {
|
||||||
|
@Override public Object resolve(Class<?> handlerClass) { return null; }
|
||||||
|
@Override public RenderedView render(BaseViewHandler<Object> handler, Object target,
|
||||||
|
dev.relism.flash.models.Request req,
|
||||||
|
dev.relism.flash.models.Response res) { return null; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validateHandlerClass(Class<? extends RequestHandler> handlerClass) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void configure_twoViewEngines_failsWithClearMessage() {
|
||||||
|
FlashContext ctx = new FlashContext();
|
||||||
|
new FakeViewExtension().configure(null, ctx);
|
||||||
|
|
||||||
|
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||||
|
() -> new FakeViewExtension().configure(null, ctx));
|
||||||
|
|
||||||
|
assertTrue(ex.getMessage().contains("Only one view engine"),
|
||||||
|
"expected a clear cross-engine message, got: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ This module keeps jte semantics front and center:
|
|||||||
- `JteHandler`
|
- `JteHandler`
|
||||||
- `@Template`
|
- `@Template`
|
||||||
- `ViewModel` from `flash-ext-view-core`
|
- `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:
|
The extension mirrors `gg.jte.ContentType` into Flash HTTP content type:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
2. **Request-time**
|
2. **Request-time**
|
||||||
- Handler builds local `ViewModel`.
|
- 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`.
|
- jte renders template into `StringOutput`.
|
||||||
|
|
||||||
3. **Static assets (optional, enabled by default)**
|
3. **Static assets (optional, enabled by default)**
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ Register globals in extension setup:
|
|||||||
new JteExtension().addGlobal("appName", req -> "Flash")
|
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();
|
this.settings = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private JteExtension(JteSettings settings) {
|
private JteExtension(JteSettings settings, List<GlobalValue> seedGlobals) {
|
||||||
|
super(seedGlobals);
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JteExtension templateRoot(String templateRoot) {
|
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) {
|
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) {
|
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() {
|
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) {
|
public JteExtension staticCors(Consumer<JteSettings.Builder> corsConfig) {
|
||||||
JteSettings.Builder builder = settings.toBuilder();
|
JteSettings.Builder builder = settings.toBuilder();
|
||||||
builder.enableStaticCors(true);
|
builder.enableStaticCors(true);
|
||||||
corsConfig.accept(builder);
|
corsConfig.accept(builder);
|
||||||
return new JteExtension(builder.build());
|
return new JteExtension(builder.build(), globals());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+38
@@ -1,6 +1,11 @@
|
|||||||
package dev.relism.flash.ext.view.jte;
|
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.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.Request;
|
||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
@@ -89,6 +94,39 @@ class JteExtensionTest {
|
|||||||
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
|
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
|
@Test
|
||||||
void routes_register_static_wildcard_when_enabled() {
|
void routes_register_static_wildcard_when_enabled() {
|
||||||
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));
|
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));
|
||||||
|
|||||||
Reference in New Issue
Block a user