Skip to content

Commit 09519ab

Browse files
committed
- make sure htmx always run first
- setup engines at startup time
1 parent 9ee1966 commit 09519ab

6 files changed

Lines changed: 66 additions & 16 deletions

File tree

jooby/src/main/java/io/jooby/TemplateEngine.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* @author edgar
1919
*/
2020
public interface TemplateEngine extends MessageEncoder {
21+
/** Just a template engine that is on top of the stack (run before all other engines). */
22+
interface OnTop extends TemplateEngine {}
2123

2224
/** Name of application property that defines the template path. */
2325
String TEMPLATE_PATH = "templates.path";

jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ public class HttpMessageEncoder implements MessageEncoder {
3030
public HttpMessageEncoder add(MediaType type, MessageEncoder encoder) {
3131
if (encoder instanceof TemplateEngine engine) {
3232
// Media type is ignored for template engines. They have a custom object type
33-
templateEngineList.add(engine);
33+
if (engine instanceof TemplateEngine.OnTop) {
34+
// need to go first
35+
templateEngineList.addFirst(engine);
36+
} else {
37+
templateEngineList.add(engine);
38+
}
3439
} else {
3540
if (encoders == null) {
3641
encoders = new LinkedHashMap<>();

modules/jooby-htmx/src/main/java/io/jooby/htmx/HtmxModule.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ public void install(Jooby app) throws Exception {
5858
if (errorHandler != null) {
5959
app.error(errorHandler.toErrorHandler());
6060
}
61-
62-
app.encoder(new HtmxTemplateEngine());
61+
var htmxEngine = new HtmxTemplateEngine();
62+
app.encoder(htmxEngine);
63+
// validate and setup engines:
64+
app.onStarting(() -> htmxEngine.init(app));
6365
}
6466
}

modules/jooby-htmx/src/main/java/io/jooby/htmx/HtmxTemplateEngine.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*/
66
package io.jooby.htmx;
77

8+
import java.util.ArrayList;
9+
import java.util.List;
10+
811
import org.jspecify.annotations.Nullable;
912

1013
import io.jooby.*;
@@ -23,12 +26,22 @@
2326
* @author edgar
2427
* @since 4.5.0
2528
*/
26-
public class HtmxTemplateEngine implements TemplateEngine {
29+
public class HtmxTemplateEngine implements TemplateEngine.OnTop {
30+
31+
private List<TemplateEngine> engines;
32+
33+
void init(Jooby app) {
34+
engines = new ArrayList<>(app.getRouter().getTemplateEngines());
35+
engines.remove(this);
36+
if (engines.isEmpty()) {
37+
throw new IllegalStateException("No template engines registered");
38+
}
39+
}
2740

2841
@Override
2942
public Output render(Context ctx, ModelAndView<?> modelAndView) throws Exception {
3043
if (modelAndView instanceof HtmxModelAndView<?> htmxView) {
31-
var engineEncoder = resolveTemplateEngine(ctx, htmxView);
44+
var engineEncoder = resolveTemplateEngine(htmxView);
3245
if (engineEncoder == null) {
3346
throw new IllegalStateException(
3447
"No template engine registered to handle: " + htmxView.getView());
@@ -47,17 +60,16 @@ public Output render(Context ctx, ModelAndView<?> modelAndView) throws Exception
4760
* ModelAndView}. Iterates through the available template engines in the context, returning the
4861
* first one that supports the provided model and view.
4962
*
50-
* @param ctx The web context containing the registered resources and state information.
5163
* @param mv The {@link ModelAndView} to be rendered. The method determines its compatibility with
5264
* the available template engines.
5365
* @return The {@link TemplateEngine} capable of rendering the provided {@link ModelAndView}, or
5466
* {@code null} if no suitable engine is found.
5567
*/
56-
private @Nullable TemplateEngine resolveTemplateEngine(Context ctx, ModelAndView mv) {
68+
private @Nullable TemplateEngine resolveTemplateEngine(ModelAndView mv) {
5769
// Find the encoder that handles standard ModelAndView
58-
for (var templateEngine : ctx.getRouter().getTemplateEngines()) {
59-
if (templateEngine != this && templateEngine.supports(mv)) {
60-
return templateEngine;
70+
for (var engine : engines) {
71+
if (engine.supports(mv)) {
72+
return engine;
6173
}
6274
}
6375
return null;

modules/jooby-htmx/src/test/java/io/jooby/htmx/HtmxModuleTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ void shouldInstallWithoutErrorHandler() throws Exception {
3636

3737
// Verify the template engine WAS registered
3838
verify(app).encoder(any(HtmxTemplateEngine.class));
39+
40+
// Verify the init lifecycle hook was registered
41+
verify(app).onStarting(any());
3942
}
4043

4144
@Test
@@ -54,5 +57,8 @@ void shouldInstallWithErrorHandler() throws Exception {
5457

5558
// 4. Verify the template engine WAS registered
5659
verify(app).encoder(any(HtmxTemplateEngine.class));
60+
61+
// 5. Verify the init lifecycle hook was registered
62+
verify(app).onStarting(any());
5763
}
5864
}

modules/jooby-htmx/src/test/java/io/jooby/htmx/HtmxTemplateEngineTest.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.junit.jupiter.api.Test;
2424

2525
import io.jooby.Context;
26+
import io.jooby.Jooby;
2627
import io.jooby.ModelAndView;
2728
import io.jooby.Router;
2829
import io.jooby.TemplateEngine;
@@ -35,13 +36,29 @@ class HtmxTemplateEngineTest {
3536
private HtmxTemplateEngine engine;
3637
private Context ctx;
3738
private Router router;
39+
private Jooby app;
3840

3941
@BeforeEach
4042
void setUp() {
4143
engine = new HtmxTemplateEngine();
4244
ctx = mock(Context.class);
4345
router = mock(Router.class);
46+
app = mock(Jooby.class);
47+
4448
when(ctx.getRouter()).thenReturn(router);
49+
when(app.getRouter()).thenReturn(router);
50+
}
51+
52+
// --- Lifecycle / Init Tests ---
53+
54+
@Test
55+
void shouldThrowIllegalStateExceptionWhenNoOtherTemplateEnginesRegistered() {
56+
// Router only has the HtmxTemplateEngine registered, no underlying engines like Handlebars
57+
when(router.getTemplateEngines()).thenReturn(List.of(engine));
58+
59+
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> engine.init(app));
60+
61+
assertEquals("No template engines registered", ex.getMessage());
4562
}
4663

4764
// --- Supports Tests ---
@@ -68,12 +85,17 @@ void shouldReturnNullForStandardModelAndView() throws Exception {
6885

6986
@Test
7087
void shouldThrowIllegalStateExceptionWhenNoTemplateEngineFound() {
88+
// 1. Setup incompatible engine and initialize the HtmxTemplateEngine
89+
TemplateEngine incompatibleEngine = mock(TemplateEngine.class);
90+
when(router.getTemplateEngines()).thenReturn(Arrays.asList(engine, incompatibleEngine));
91+
engine.init(app); // Cache the engines
92+
93+
// 2. Setup the HTMX view
7194
HtmxModelAndView<?> htmxView = mock(HtmxModelAndView.class);
7295
when(htmxView.getView()).thenReturn("missing.hbs");
96+
when(incompatibleEngine.supports(htmxView)).thenReturn(false);
7397

74-
// Router has no other engines registered
75-
when(router.getTemplateEngines()).thenReturn(List.of(engine));
76-
98+
// 3. Execute and verify
7799
IllegalStateException ex =
78100
assertThrows(IllegalStateException.class, () -> engine.render(ctx, htmxView));
79101

@@ -90,17 +112,18 @@ void shouldRenderMultipleTemplatesIntoCompositeOutput() throws Exception {
90112

91113
when(htmxView.iterator()).thenReturn(views.iterator());
92114

93-
// 2. Mock a delegate Template Engine (e.g., Handlebars)
115+
// 2. Mock Delegate Engines
94116
TemplateEngine delegateEngine = mock(TemplateEngine.class);
95117
when(delegateEngine.supports(htmxView)).thenReturn(true);
96118

97-
// Mock an incompatible engine to cover the "continue" branch inside resolveTemplateEngine
98119
TemplateEngine incompatibleEngine = mock(TemplateEngine.class);
99120
when(incompatibleEngine.supports(htmxView)).thenReturn(false);
100121

101-
// Register engines. We include `engine` (this) to ensure the `!= this` branch is hit.
122+
// Register and initialize engines (HtmxTemplateEngine should remove itself from the cached
123+
// list)
102124
when(router.getTemplateEngines())
103125
.thenReturn(Arrays.asList(engine, incompatibleEngine, delegateEngine));
126+
engine.init(app);
104127

105128
// 3. Mock the Output Pipeline
106129
OutputFactory outputFactory = mock(OutputFactory.class);

0 commit comments

Comments
 (0)