diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index de2383d88..8178f9917 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,13 +3,16 @@ updates:
- package-ecosystem: "maven"
directory: "/"
schedule:
- interval: "weekly"
+ interval: weekly
open-pull-requests-limit: 10
+ groups:
+ dependencies:
+ patterns:
+ - "*"
labels:
- "dependencies"
target-branch: "master"
-
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/.github/workflows/jdk-ea-stable.yml b/.github/workflows/jdk-ea-stable.yml
new file mode 100644
index 000000000..a6c44f00f
--- /dev/null
+++ b/.github/workflows/jdk-ea-stable.yml
@@ -0,0 +1,37 @@
+
+name: JDK EA Stable
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+ schedule:
+ - cron: '39 1 * * 1,3,5'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Java
+ uses: oracle-actions/setup-java@v1
+ with:
+ website: jdk.java.net
+ release: ea
+ version: stable
+ - name: Maven cache
+ uses: actions/cache@v4
+ env:
+ cache-name: maven-cache
+ with:
+ path:
+ ~/.m2
+ key: build-${{ env.cache-name }}
+ - name: Maven version
+ run: mvn --version
+ - name: Build with Maven
+ run: mvn clean verify package -Ptest
diff --git a/.github/workflows/jdk-ea.yml b/.github/workflows/jdk-ea.yml
index 6dec68f0c..7e3cdc1e3 100644
--- a/.github/workflows/jdk-ea.yml
+++ b/.github/workflows/jdk-ea.yml
@@ -2,6 +2,7 @@
name: avaje-http EA
on:
+ pull_request:
workflow_dispatch:
schedule:
- cron: '39 1 * * 2,5'
diff --git a/README.md b/README.md
index bfe8803b0..a47585153 100644
--- a/README.md
+++ b/README.md
@@ -47,17 +47,13 @@ Use source code generation to adapt annotated REST controllers `@Path, @Get, @Po
```
-### JDK 22+
+### JDK 23+
-In JDK 22+, annotation processors are disabled by default, you will need to add a flag to re-enable.
+In JDK 23+, annotation processors are disabled by default, you will need to add a flag to re-enable.
```xml
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- -proc:full
-
-
+
+ full
+
```
## Define a Controller (These APT processors work with both Java and Kotlin)
diff --git a/htmx-api/pom.xml b/htmx-api/pom.xml
new file mode 100644
index 000000000..8bdc810c5
--- /dev/null
+++ b/htmx-api/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+
+ io.avaje
+ avaje-http-parent
+ 2.8-RC1
+
+
+ avaje-htmx-api
+
+
+
+
+
+
+ io.avaje
+ avaje-lang
+ 1.1
+
+
+
+
diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java b/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java
new file mode 100644
index 000000000..56a8b2106
--- /dev/null
+++ b/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java
@@ -0,0 +1,16 @@
+package io.avaje.htmx.api;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Mark a controller method as using a content cache.
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface ContentCache {
+
+}
diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java
new file mode 100644
index 000000000..c213b6e13
--- /dev/null
+++ b/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java
@@ -0,0 +1,141 @@
+package io.avaje.htmx.api;
+
+import io.avaje.lang.Nullable;
+
+final class DHxRequest implements HtmxRequest {
+
+ private final boolean htmxRequest;
+
+ private final boolean boosted;
+ private final String currentUrl;
+ private final boolean historyRestoreRequest;
+ private final String promptResponse;
+ private final String target;
+ private final String triggerName;
+ private final String triggerId;
+
+ DHxRequest() {
+ this.htmxRequest = false;
+ this.boosted = false;
+ this.currentUrl = null;
+ this.historyRestoreRequest = false;
+ this.promptResponse = null;
+ this.target = null;
+ this.triggerName = null;
+ this.triggerId = null;
+ }
+
+ DHxRequest(boolean boosted, String currentUrl, boolean historyRestoreRequest, String promptResponse, String target, String triggerName, String triggerId) {
+ this.htmxRequest = true;
+ this.boosted = boosted;
+ this.currentUrl = currentUrl;
+ this.historyRestoreRequest = historyRestoreRequest;
+ this.promptResponse = promptResponse;
+ this.target = target;
+ this.triggerName = triggerName;
+ this.triggerId = triggerId;
+ }
+
+ @Override
+ public boolean isHtmxRequest() {
+ return htmxRequest;
+ }
+
+ @Override
+ public boolean isBoosted() {
+ return boosted;
+ }
+
+ @Nullable
+ @Override
+ public String currentUrl() {
+ return currentUrl;
+ }
+
+ @Override
+ public boolean isHistoryRestoreRequest() {
+ return historyRestoreRequest;
+ }
+
+ @Nullable
+ @Override
+ public String promptResponse() {
+ return promptResponse;
+ }
+
+ @Nullable
+ @Override
+ public String target() {
+ return target;
+ }
+
+ @Nullable
+ @Override
+ public String triggerName() {
+ return triggerName;
+ }
+
+ @Nullable
+ public String triggerId() {
+ return triggerId;
+ }
+
+ static final class DBuilder implements Builder {
+
+ private boolean boosted;
+ private String currentUrl;
+ private boolean historyRestoreRequest;
+ private String promptResponse;
+ private String target;
+ private String triggerName;
+ private String triggerId;
+
+ @Override
+ public DBuilder boosted(boolean boosted) {
+ this.boosted = boosted;
+ return this;
+ }
+
+ @Override
+ public DBuilder currentUrl(String currentUrl) {
+ this.currentUrl = currentUrl;
+ return this;
+ }
+
+ @Override
+ public DBuilder historyRestoreRequest(boolean historyRestoreRequest) {
+ this.historyRestoreRequest = historyRestoreRequest;
+ return this;
+ }
+
+ @Override
+ public DBuilder promptResponse(String promptResponse) {
+ this.promptResponse = promptResponse;
+ return this;
+ }
+
+ @Override
+ public DBuilder target(String target) {
+ this.target = target;
+ return this;
+ }
+
+ @Override
+ public DBuilder triggerName(String triggerName) {
+ this.triggerName = triggerName;
+ return this;
+ }
+
+ @Override
+ public DBuilder triggerId(String triggerId) {
+ this.triggerId = triggerId;
+ return this;
+ }
+
+ @Override
+ public HtmxRequest build() {
+ return new DHxRequest(boosted, currentUrl, historyRestoreRequest, promptResponse, target, triggerName, triggerId);
+ }
+ }
+
+}
diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/Html.java b/htmx-api/src/main/java/io/avaje/htmx/api/Html.java
new file mode 100644
index 000000000..bedae0ded
--- /dev/null
+++ b/htmx-api/src/main/java/io/avaje/htmx/api/Html.java
@@ -0,0 +1,18 @@
+package io.avaje.htmx.api;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Mark a controller as producing HTML by default and using "Templating"
+ * meaning that response objects are expected to a "Model View" passed to
+ * the "Templating" library.
+ */
+@Target(TYPE)
+@Retention(RUNTIME)
+public @interface Html {
+
+}
diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java
new file mode 100644
index 000000000..d8a47a22c
--- /dev/null
+++ b/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java
@@ -0,0 +1,106 @@
+package io.avaje.htmx.api;
+
+import io.avaje.lang.Nullable;
+
+/**
+ * This class can be used as a controller method argument to access
+ * the htmx Request Headers .
+ *
+ *
{@code
+ *
+ * @HxRequest
+ * @Get("/users")
+ * String users(HtmxRequest htmxRequest) {
+ * if (htmxRequest.isBoosted()) {
+ * ...
+ * }
+ * }
+ *
+ * }
+ *
+ * @see Request Headers Reference
+ */
+public interface HtmxRequest {
+
+ /**
+ * Represents a non-Htmx request.
+ */
+ HtmxRequest EMPTY = new DHxRequest();
+
+ /**
+ * Return a new builder for the HtmxRequest.
+ */
+ static Builder builder() {
+ return new DHxRequest.DBuilder();
+ }
+
+ /**
+ * Return true if this is an Htmx request.
+ */
+ boolean isHtmxRequest();
+
+ /**
+ * Indicates that the request is via an element using hx-boost.
+ *
+ * @return true if the request was made via HX-Boost, false otherwise
+ */
+ boolean isBoosted();
+
+ /**
+ * Return the current URL of the browser when the htmx request was made.
+ */
+ @Nullable
+ String currentUrl();
+
+ /**
+ * Indicates if the request is for history restoration after a miss in the local history cache
+ *
+ * @return true if this request is for history restoration, false otherwise
+ */
+ boolean isHistoryRestoreRequest();
+
+ /**
+ * Return the user response to an HX-Prompt.
+ */
+ @Nullable
+ String promptResponse();
+
+ /**
+ * Return the id of the target element if it exists.
+ */
+ @Nullable
+ String target();
+
+ /**
+ * Return the name of the triggered element if it exists.
+ */
+ @Nullable
+ String triggerName();
+
+ /**
+ * Return the id of the triggered element if it exists.
+ */
+ @Nullable
+ String triggerId();
+
+ /**
+ * Builder for {@link HtmxRequest}.
+ */
+ interface Builder {
+ Builder boosted(boolean boosted);
+
+ Builder currentUrl(String currentUrl);
+
+ Builder historyRestoreRequest(boolean historyRestoreRequest);
+
+ Builder promptResponse(String promptResponse);
+
+ Builder target(String target);
+
+ Builder triggerName(String triggerName);
+
+ Builder triggerId(String triggerId);
+
+ HtmxRequest build();
+ }
+}
diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java
new file mode 100644
index 000000000..e643f31e5
--- /dev/null
+++ b/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java
@@ -0,0 +1,54 @@
+package io.avaje.htmx.api;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Mark a controller method as handling Htmx requests and potentially restrict
+ * the handler to only be used for specific Htmx target or Htmx trigger.
+ *
+ * Controller methods with {@code @HxRequest} require the {@code HX-Request}
+ * HTTP Header to be set for the handler to process the request. Additionally,
+ * we can specify {@link #target()}, {@link #triggerId()}, or {@link #triggerName()}
+ * such that the handler is only invoked specifically for requests with those
+ * matching headers.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface HxRequest {
+
+ /**
+ * Restricts the mapping to the {@code id} of a specific target element.
+ *
+ * @see HX-Target
+ */
+ String target() default "";
+
+ /**
+ * Restricts the mapping to the {@code id} of a specific triggered element.
+ *
+ * @see HX-Trigger
+ */
+ String triggerId() default "";
+
+ /**
+ * Restricts the mapping to the {@code name} of a specific triggered element.
+ *
+ * @see HX-Trigger-Name
+ */
+ String triggerName() default "";
+
+ /**
+ * Restricts the mapping to the {@code id}, if any, or to the {@code name} of a specific triggered element.
+ *
+ * If you want to be explicit use {@link #triggerId()} or {@link #triggerName()}.
+ *
+ * @see HX-Trigger
+ * @see HX-Trigger-Name
+ */
+ String value() default "";
+}
diff --git a/htmx-api/src/main/java/module-info.java b/htmx-api/src/main/java/module-info.java
new file mode 100644
index 000000000..c018025a9
--- /dev/null
+++ b/htmx-api/src/main/java/module-info.java
@@ -0,0 +1,6 @@
+module io.avaje.htmx.api {
+
+ exports io.avaje.htmx.api;
+
+ requires static io.avaje.lang;
+}
diff --git a/htmx-nima-jstache/pom.xml b/htmx-nima-jstache/pom.xml
new file mode 100644
index 000000000..fa0592ddd
--- /dev/null
+++ b/htmx-nima-jstache/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ io.avaje
+ avaje-http-parent
+ 2.8-RC1
+
+
+ avaje-htmx-nima-jstache
+
+
+ 21
+ 21
+ 21
+ UTF-8
+ false
+ 1.3.6
+
+
+
+
+ io.jstach
+ jstachio
+ ${io.jstach.version}
+
+
+ io.avaje
+ avaje-htmx-api
+ ${project.version}
+
+
+ io.avaje
+ avaje-htmx-nima
+ ${project.version}
+
+
+ io.avaje
+ avaje-inject
+ 10.3
+ provided
+ true
+
+
+ io.avaje
+ avaje-spi-service
+ 2.6
+ provided
+ true
+
+
+
diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java
new file mode 100644
index 000000000..c74e0f617
--- /dev/null
+++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java
@@ -0,0 +1,25 @@
+package io.avaje.htmx.nima.jstache;
+
+import io.avaje.htmx.nima.TemplateContentCache;
+import io.avaje.htmx.nima.TemplateRender;
+import io.avaje.inject.BeanScopeBuilder;
+import io.avaje.inject.spi.InjectPlugin;
+import io.avaje.spi.ServiceProvider;
+
+/**
+ * Plugin for avaje inject that provides a default TemplateRender instance.
+ */
+@ServiceProvider
+public final class DefaultTemplateProvider implements InjectPlugin {
+
+ @Override
+ public Class>[] provides() {
+ return new Class>[]{TemplateRender.class, TemplateContentCache.class};
+ }
+
+ @Override
+ public void apply(BeanScopeBuilder builder) {
+ builder.provideDefault(null, TemplateRender.class, JStacheTemplateRender::new);
+ builder.provideDefault(null, TemplateContentCache.class, SimpleContentCache::new);
+ }
+}
diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java
new file mode 100644
index 000000000..7b5b70294
--- /dev/null
+++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java
@@ -0,0 +1,12 @@
+package io.avaje.htmx.nima.jstache;
+
+import io.avaje.htmx.nima.TemplateRender;
+import io.jstach.jstachio.JStachio;
+
+public final class JStacheTemplateRender implements TemplateRender {
+
+ @Override
+ public String render(Object viewModel) {
+ return JStachio.render(viewModel);
+ }
+}
diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java
new file mode 100644
index 000000000..aaa64b6a5
--- /dev/null
+++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java
@@ -0,0 +1,31 @@
+package io.avaje.htmx.nima.jstache;
+
+import io.avaje.htmx.nima.TemplateContentCache;
+import io.helidon.webserver.http.ServerRequest;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public class SimpleContentCache implements TemplateContentCache {
+
+ private final ConcurrentHashMap localCache = new ConcurrentHashMap<>();
+
+ @Override
+ public String key(ServerRequest req) {
+ return req.requestedUri().path().rawPath();
+ }
+
+ @Override
+ public String key(ServerRequest req, Object formParams) {
+ return req.requestedUri().path().rawPath() + formParams;
+ }
+
+ @Override
+ public String content(String key) {
+ return localCache.get(key);
+ }
+
+ @Override
+ public void contentPut(String key, String content) {
+ localCache.put(key, content);
+ }
+}
diff --git a/htmx-nima-jstache/src/main/java/module-info.java b/htmx-nima-jstache/src/main/java/module-info.java
new file mode 100644
index 000000000..88d587079
--- /dev/null
+++ b/htmx-nima-jstache/src/main/java/module-info.java
@@ -0,0 +1,12 @@
+module io.avaje.htmx.nima.jstache {
+
+ exports io.avaje.htmx.nima.jstache;
+
+ requires transitive io.avaje.htmx.nima;
+ requires transitive io.helidon.webserver;
+ requires transitive io.jstach.jstachio;
+ requires transitive io.avaje.inject;
+ requires static io.avaje.spi;
+
+ provides io.avaje.inject.spi.InjectExtension with io.avaje.htmx.nima.jstache.DefaultTemplateProvider;
+}
diff --git a/htmx-nima/pom.xml b/htmx-nima/pom.xml
new file mode 100644
index 000000000..0598cec23
--- /dev/null
+++ b/htmx-nima/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ io.avaje
+ avaje-http-parent
+ 2.8-RC1
+
+
+ avaje-htmx-nima
+
+
+ 21
+ false
+ UTF-8
+
+
+
+
+ io.avaje
+ avaje-htmx-api
+ 2.8-RC1
+
+
+ io.helidon.webserver
+ helidon-webserver
+ 4.1.0
+
+
+
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java
new file mode 100644
index 000000000..6b44530a1
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java
@@ -0,0 +1,50 @@
+package io.avaje.htmx.nima;
+
+import io.helidon.http.Header;
+import io.helidon.http.ServerRequestHeaders;
+import io.helidon.webserver.http.Handler;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+import static io.avaje.htmx.nima.HxHeaders.*;
+
+final class DHxHandler implements Handler {
+
+
+ private final Handler delegate;
+ private final String target;
+ private final String trigger;
+ private final String triggerName;
+
+ DHxHandler(Handler delegate, String target, String trigger, String triggerName) {
+ this.delegate = delegate;
+ this.target = target;
+ this.trigger = trigger;
+ this.triggerName = triggerName;
+ }
+
+ @Override
+ public void handle(ServerRequest req, ServerResponse res) throws Exception {
+ final var headers = req.headers();
+ if (headers.contains(HX_REQUEST) && matched(headers)) {
+ delegate.handle(req, res);
+ } else {
+ res.next();
+ }
+ }
+
+ private boolean matched(ServerRequestHeaders headers) {
+ if (target != null && notMatched(headers.get(HX_TARGET), target)) {
+ return false;
+ }
+ if (trigger != null && notMatched(headers.get(HX_TRIGGER), trigger)) {
+ return false;
+ }
+ return triggerName == null || !notMatched(headers.get(HX_TRIGGER_NAME), triggerName);
+ }
+
+ private boolean notMatched(Header header, String matchValue) {
+ return header == null || !matchValue.equals(header.get());
+ }
+
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java
new file mode 100644
index 000000000..b9370074c
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java
@@ -0,0 +1,38 @@
+package io.avaje.htmx.nima;
+
+import io.helidon.webserver.http.Handler;
+
+final class DHxHandlerBuilder implements HxHandler.Builder {
+
+ private final Handler delegate;
+ private String target;
+ private String trigger;
+ private String triggerName;
+
+ DHxHandlerBuilder(Handler delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public DHxHandlerBuilder target(String target) {
+ this.target = target;
+ return this;
+ }
+
+ @Override
+ public DHxHandlerBuilder trigger(String trigger) {
+ this.trigger = trigger;
+ return this;
+ }
+
+ @Override
+ public DHxHandlerBuilder triggerName(String triggerName) {
+ this.triggerName = triggerName;
+ return this;
+ }
+
+ @Override
+ public Handler build() {
+ return new DHxHandler(delegate, target, trigger, triggerName);
+ }
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java
new file mode 100644
index 000000000..0c28dbbd5
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java
@@ -0,0 +1,46 @@
+package io.avaje.htmx.nima;
+
+import io.helidon.webserver.http.Handler;
+
+/**
+ * Wrap a Handler with filtering for Htmx specific headers.
+ *
+ * The underlying Handler will not be invoked unless the request
+ * is a Htmx request and matches the required attributes.
+ */
+public interface HxHandler {
+
+ /**
+ * Create a builder that wraps the underlying handler with Htmx
+ * specific attribute matching.
+ */
+ static Builder builder(Handler delegate) {
+ return new DHxHandlerBuilder(delegate);
+ }
+
+ /**
+ * Build the Htmx request handler.
+ */
+ interface Builder {
+
+ /**
+ * Match on the given target.
+ */
+ Builder target(String target);
+
+ /**
+ * Match on the given trigger.
+ */
+ Builder trigger(String trigger);
+
+ /**
+ * Match on the given trigger name.
+ */
+ Builder triggerName(String triggerName);
+
+ /**
+ * Build and return the Handler.
+ */
+ Handler build();
+ }
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java
new file mode 100644
index 000000000..156798d29
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java
@@ -0,0 +1,65 @@
+package io.avaje.htmx.nima;
+
+import io.helidon.http.HeaderName;
+import io.helidon.http.HeaderNames;
+
+/**
+ * HTMX request headers.
+ *
+ * @see Request Headers Reference
+ */
+public interface HxHeaders {
+
+ /**
+ * Indicates that the request comes from an element that uses hx-boost.
+ *
+ * @see HX-Boosted
+ */
+ HeaderName HX_BOOSTED = HeaderNames.create("HX-Boosted");
+
+ /**
+ * The current URL of the browser
+ *
+ * @see HX-Current-URL
+ */
+ HeaderName HX_CURRENT_URL = HeaderNames.create("HX-Current-URL");
+
+ /**
+ * Indicates if the request is for history restoration after a miss in the local history cache.
+ *
+ * @see HX-History-Restore-Request
+ */
+ HeaderName HX_HISTORY_RESTORE_REQUEST = HeaderNames.create("HX-History-Restore-Request");
+
+ /**
+ * Contains the user response to a hx-prompt .
+ *
+ * @see HX-Prompt
+ */
+ HeaderName HX_PROMPT = HeaderNames.create("HX-Prompt");
+ /**
+ * Only present and {@code true} if the request is issued by htmx.
+ *
+ * @see HX-Request
+ */
+ HeaderName HX_REQUEST = HeaderNames.create("HX-Request");
+ /**
+ * The {@code id} of the target element if it exists.
+ *
+ * @see HX-Target
+ */
+ HeaderName HX_TARGET = HeaderNames.create("HX-Target");
+ /**
+ * The {@code name} of the triggered element if it exists
+ *
+ * @see HX-Trigger-Name
+ */
+ HeaderName HX_TRIGGER_NAME = HeaderNames.create("HX-Trigger-Name");
+ /**
+ * The {@code id} of the triggered element if it exists.
+ *
+ * @see HX-Trigger
+ */
+ HeaderName HX_TRIGGER = HeaderNames.create("HX-Trigger");
+
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java
new file mode 100644
index 000000000..2dda4c675
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java
@@ -0,0 +1,49 @@
+package io.avaje.htmx.nima;
+
+import io.avaje.htmx.api.HtmxRequest;
+import io.helidon.webserver.http.ServerRequest;
+
+/**
+ * Obtain the HtmxRequest for the given Helidon ServerRequest.
+ */
+public class HxReq {
+
+ /**
+ * Create given the server request.
+ */
+ public static HtmxRequest of(ServerRequest request) {
+ final var headers = request.headers();
+ if (!headers.contains(HxHeaders.HX_REQUEST)) {
+ return HtmxRequest.EMPTY;
+ }
+
+ var builder = HtmxRequest.builder();
+ if (headers.contains(HxHeaders.HX_BOOSTED)) {
+ builder.boosted(true);
+ }
+ if (headers.contains(HxHeaders.HX_HISTORY_RESTORE_REQUEST)) {
+ builder.historyRestoreRequest(true);
+ }
+ var currentUrl = headers.get(HxHeaders.HX_CURRENT_URL);
+ if (currentUrl != null) {
+ builder.currentUrl(currentUrl.get());
+ }
+ var prompt = headers.get(HxHeaders.HX_PROMPT);
+ if (prompt != null) {
+ builder.promptResponse(prompt.get());
+ }
+ var target = headers.get(HxHeaders.HX_TARGET);
+ if (target != null) {
+ builder.target(target.get());
+ }
+ var triggerName = headers.get(HxHeaders.HX_TRIGGER_NAME);
+ if (triggerName != null) {
+ builder.triggerName(triggerName.get());
+ }
+ var trigger = headers.get(HxHeaders.HX_TRIGGER);
+ if (trigger != null) {
+ builder.triggerId(trigger.get());
+ }
+ return builder.build();
+ }
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateContentCache.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateContentCache.java
new file mode 100644
index 000000000..83a8a60e7
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateContentCache.java
@@ -0,0 +1,30 @@
+package io.avaje.htmx.nima;
+
+import io.helidon.webserver.http.ServerRequest;
+
+/**
+ * Defines caching of template content.
+ */
+public interface TemplateContentCache {
+
+ /**
+ * Return the key given the request.
+ */
+ String key(ServerRequest req);
+
+ /**
+ * Return the key given the request with form parameters.
+ */
+ String key(ServerRequest req, Object formParams);
+
+ /**
+ * Return the content given the key.
+ */
+ String content(String key);
+
+ /**
+ * Put the content into the cache.
+ */
+ void contentPut(String key, String content);
+
+}
diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java
new file mode 100644
index 000000000..fa69f00f6
--- /dev/null
+++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java
@@ -0,0 +1,12 @@
+package io.avaje.htmx.nima;
+
+/**
+ * Template render API for Helidon.
+ */
+public interface TemplateRender {
+
+ /**
+ * Render the given template view model to the server response.
+ */
+ String render(Object viewModel);
+}
diff --git a/htmx-nima/src/main/java/module-info.java b/htmx-nima/src/main/java/module-info.java
new file mode 100644
index 000000000..8adfb5b45
--- /dev/null
+++ b/htmx-nima/src/main/java/module-info.java
@@ -0,0 +1,7 @@
+module io.avaje.htmx.nima {
+
+ requires io.avaje.htmx.api;
+ requires io.helidon.webserver;
+
+ exports io.avaje.htmx.nima;
+}
diff --git a/http-api-javalin/pom.xml b/http-api-javalin/pom.xml
index f38b8839f..9f3b18bd3 100644
--- a/http-api-javalin/pom.xml
+++ b/http-api-javalin/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
..
diff --git a/http-api/pom.xml b/http-api/pom.xml
index 704fb8d12..80efe5d32 100644
--- a/http-api/pom.xml
+++ b/http-api/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
..
@@ -24,7 +24,7 @@
io.javalin
javalin
- 6.1.6
+ 6.3.0
provided
true
diff --git a/http-client-gson-adapter/pom.xml b/http-client-gson-adapter/pom.xml
index a2c4ff1d2..099a2b053 100644
--- a/http-client-gson-adapter/pom.xml
+++ b/http-client-gson-adapter/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-client-gson
@@ -20,7 +20,7 @@
io.avaje
avaje-http-client
- 2.6-RC1
+ 2.8-RC1
provided
diff --git a/http-client-moshi-adapter/pom.xml b/http-client-moshi-adapter/pom.xml
index 93173772b..f5cd2899c 100644
--- a/http-client-moshi-adapter/pom.xml
+++ b/http-client-moshi-adapter/pom.xml
@@ -3,7 +3,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-client-moshi
@@ -19,7 +19,7 @@
io.avaje
avaje-http-client
- 2.6-RC1
+ 2.8-RC1
provided
diff --git a/http-client/pom.xml b/http-client/pom.xml
index 7144e47f2..c7562cc9c 100644
--- a/http-client/pom.xml
+++ b/http-client/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-client
@@ -29,21 +29,21 @@
com.fasterxml.jackson.core
jackson-databind
- 2.17.1
+ 2.17.2
true
io.avaje
avaje-jsonb
- 1.11
+ 2.1
true
io.avaje
avaje-inject
- 9.12
+ 10.3
true
@@ -59,14 +59,14 @@
io.javalin
javalin
- 6.1.6
+ 6.3.0
test
io.avaje
avaje-http-api
- 2.6-RC1
+ 2.8-RC1
test
@@ -99,7 +99,7 @@
io.avaje
avaje-inject-generator
- 9.12
+ 10.3
diff --git a/http-generator-client/pom.xml b/http-generator-client/pom.xml
index 305f4aa6e..a662b44a7 100644
--- a/http-generator-client/pom.xml
+++ b/http-generator-client/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-client-generator
diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java
index bfa894148..2ba487244 100644
--- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java
+++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java
@@ -17,6 +17,7 @@
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
+import io.avaje.http.generator.core.APContext;
import io.avaje.http.generator.core.ClientPrism;
import io.avaje.http.generator.core.ControllerReader;
import io.avaje.http.generator.core.ImportPrism;
@@ -44,6 +45,7 @@ public SourceVersion getSupportedSourceVersion() {
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnv = processingEnv;
+ APContext.init(processingEnv);
ProcessingContext.init(processingEnv, new ClientPlatformAdapter(), false);
this.componentWriter = new SimpleComponentWriter(metaData);
useJsonB = ProcessingContext.useJsonb();
@@ -51,7 +53,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment round) {
- ProcessingContext.findModule(annotations, round);
+ APContext.setProjectModuleElement(annotations, round);
final var platform = platform();
if (!(platform instanceof ClientPlatformAdapter)) {
setPlatform(new ClientPlatformAdapter());
@@ -107,7 +109,8 @@ protected String writeClientAdapter(ControllerReader reader) throws IOException
private void initialiseComponent() {
metaData.initialiseFullName();
if (!metaData.all().isEmpty()) {
- ProcessingContext.validateModule(metaData.fullName());
+ ProcessingContext.addClientComponent(metaData.fullName());
+ ProcessingContext.validateModule();
}
try {
componentWriter.init();
diff --git a/http-generator-core/pom.xml b/http-generator-core/pom.xml
index 31194e098..0b730af17 100644
--- a/http-generator-core/pom.xml
+++ b/http-generator-core/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-generator-core
@@ -26,6 +26,14 @@
provided
+
+ io.avaje
+ avaje-htmx-api
+ ${project.version}
+ true
+ provided
+
+
io.swagger.core.v3
swagger-annotations
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java
index 595014b7a..485835fa3 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java
@@ -22,6 +22,11 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
+import io.avaje.prism.GenerateAPContext;
+import io.avaje.prism.GenerateModuleInfoReader;
+
+@GenerateAPContext
+@GenerateModuleInfoReader
@SupportedOptions({"useJavax", "useSingleton", "instrumentRequests","disableDirectWrites"})
public abstract class BaseProcessor extends AbstractProcessor {
@@ -42,6 +47,7 @@ public Set getSupportedAnnotationTypes() {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
+ APContext.init(processingEnv);
ProcessingContext.init(processingEnv, providePlatformAdapter());
}
@@ -51,7 +57,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment round) {
var pathElements = round.getElementsAnnotatedWith(typeElement(PathPrism.PRISM_TYPE));
-
+ APContext.setProjectModuleElement(annotations, round);
if (contextPathString == null) {
contextPathString =
ElementFilter.modulesIn(pathElements).stream()
@@ -79,6 +85,7 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
if (round.processingOver()) {
writeOpenAPI();
+ ProcessingContext.validateModule();
}
return false;
}
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java
index 3914821cc..2e5cd9dcd 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java
@@ -47,6 +47,10 @@ public final class ControllerReader {
private final String producesPrism;
private final boolean hasValid;
+ /** Set true via {@code @Html} to indicate use of Templating */
+ private final boolean html;
+ /** Set true via {@code @ContentCache} to indicate use of Templating content cache */
+ private boolean hasContentCache;
private boolean methodHasValid;
/**
@@ -70,7 +74,8 @@ public ControllerReader(TypeElement beanType, String contextPath) {
docHidden = initDocHidden();
}
this.hasValid = initHasValid();
- this.producesPrism = initProduces();
+ this.html = initHtml();
+ this.producesPrism = initProduces(html);
this.apiResponses = buildApiResponses();
hasInstrument =
instrumentAllWebMethods()
@@ -172,8 +177,13 @@ private boolean matchMethod(ExecutableElement interfaceMethod, ExecutableElement
return interfaceMethod.toString().equals(element.toString());
}
- private String initProduces() {
- return findAnnotation(ProducesPrism::getOptionalOn).map(ProducesPrism::value).orElse(null);
+ private boolean initHtml() {
+ return findAnnotation(HtmlPrism::getOptionalOn).isPresent();
+ }
+
+ private String initProduces(boolean html) {
+ String defaultProduces = html ? "text/html;charset=UTF8" : null;
+ return findAnnotation(ProducesPrism::getOptionalOn).map(ProducesPrism::value).orElse(defaultProduces);
}
private boolean initDocHidden() {
@@ -188,6 +198,14 @@ String produces() {
return producesPrism;
}
+ public boolean html() {
+ return html;
+ }
+
+ public boolean hasContentCache() {
+ return hasContentCache;
+ }
+
public TypeElement beanType() {
return beanType;
}
@@ -235,10 +253,11 @@ public void read(boolean withSingleton) {
}
private void deriveIncludeValidation() {
- methodHasValid = methodHasValid();
+ methodHasValid = anyMethodHasValid();
+ hasContentCache = anyMethodHasContentCache();
}
- private boolean methodHasValid() {
+ private boolean anyMethodHasValid() {
for (final MethodReader method : methods) {
if (method.hasValid()) {
return true;
@@ -247,6 +266,15 @@ private boolean methodHasValid() {
return false;
}
+ private boolean anyMethodHasContentCache() {
+ for (final MethodReader method : methods) {
+ if (method.hasContentCache()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void readField(Element element) {
if (!requestScope) {
final String rawType = element.asType().toString();
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java
index a47923feb..212600c96 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java
@@ -5,9 +5,14 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
-public class JsonBUtil {
+public final class JsonBUtil {
+
private JsonBUtil() {}
+ public static boolean isJsonMimeType(String producesMimeType) {
+ return producesMimeType == null || producesMimeType.toLowerCase().contains("application/json");
+ }
+
public static Map jsonTypes(ControllerReader reader) {
final Map jsonTypes = new LinkedHashMap<>();
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java
index 2ffa86e20..d427b57b9 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java
@@ -41,8 +41,10 @@ public class MethodReader {
private final List extends TypeMirror> actualParams;
private final PathSegments pathSegments;
private final boolean hasValid;
+ private final Optional contentCache;
private final List superMethods;
private final Optional timeout;
+ private final HxRequestPrism hxRequest;
private WebMethod webMethod;
private int statusCode;
@@ -77,6 +79,7 @@ public class MethodReader {
this.securityRequirements = readSecurityRequirements();
this.apiResponses = buildApiResponses();
this.javadoc = buildJavadoc(element);
+ this.hxRequest = HxRequestPrism.getInstanceOn(element);
this.timeout = RequestTimeoutPrism.getOptionalOn(element);
timeout.ifPresent(
p -> {
@@ -85,10 +88,12 @@ public class MethodReader {
});
if (isWebMethod()) {
this.hasValid = initValid();
+ this.contentCache = initContentCache();
this.instrumentContext = initResolver();
this.pathSegments = PathSegments.parse(Util.combinePath(bean.path(), webMethodPath));
} else {
this.hasValid = false;
+ this.contentCache = Optional.empty();
this.pathSegments = null;
this.instrumentContext = false;
}
@@ -122,7 +127,11 @@ private boolean initValid() {
private boolean superMethodHasValid() {
return superMethods.stream()
- .anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent());
+ .anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent());
+ }
+
+ private Optional initContentCache() {
+ return findAnnotation(ContentCachePrism::getOptionalOn);
}
@Override
@@ -190,6 +199,13 @@ private void initSetWebMethod(WebMethod webMethod, ExceptionHandlerPrism excepti
bean.addImportType(exType);
}
+ /**
+ * Return the Htmx request annotation for this method.
+ */
+ public HxRequestPrism hxRequest() {
+ return hxRequest;
+ }
+
public Javadoc javadoc() {
return javadoc;
}
@@ -404,6 +420,10 @@ boolean hasValid() {
return hasValid;
}
+ public boolean hasContentCache() {
+ return contentCache.isPresent();
+ }
+
public String simpleName() {
return element.getSimpleName().toString();
}
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java
index f99a34b16..f15a9ebc0 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java
@@ -1,11 +1,8 @@
package io.avaje.http.generator.core;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.net.URI;
import java.nio.file.Paths;
-import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -16,7 +13,6 @@
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
-import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
@@ -53,8 +49,7 @@ private static final class Ctx {
private final boolean instrumentAllMethods;
private final boolean disableDirectWrites;
private final boolean javalin6;
- private ModuleElement module;
- private boolean validated;
+ private String clientFQN;
Ctx(ProcessingEnvironment env, PlatformAdapter adapter, boolean generateOpenAPI) {
readAdapter = adapter;
@@ -146,7 +141,8 @@ public static JavaFileObject createWriter(String cls, Element origin) throws IOE
/** Create a file writer for the META-INF services file. */
public static FileObject createMetaInfWriter(String target) throws IOException {
- return CTX.get().filer.createResource(StandardLocation.CLASS_OUTPUT, "", target);
+
+ return filer().createResource(StandardLocation.CLASS_OUTPUT, "", target);
}
public static JavaFileObject createWriter(String cls) throws IOException {
@@ -175,7 +171,7 @@ public static List superMethods(Element element, String metho
.filter(type -> !type.toString().contains("java.lang.Object"))
.map(superType -> {
final var superClass = (TypeElement) types.asElement(superType);
- for (final ExecutableElement method : ElementFilter.methodsIn(CTX.get().elementUtils.getAllMembers(superClass))) {
+ for (final var method : ElementFilter.methodsIn(CTX.get().elementUtils.getAllMembers(superClass))) {
if (method.getSimpleName().contentEquals(methodName)) {
return method;
}
@@ -231,48 +227,26 @@ public static boolean isAssignable2Interface(String type, String superType) {
static Stream superTypes(Element element) {
final Types types = CTX.get().typeUtils;
return types.directSupertypes(element.asType()).stream()
- .filter(type -> !type.toString().contains("java.lang.Object"))
- .map(superType -> (TypeElement) types.asElement(superType))
- .flatMap(e -> Stream.concat(superTypes(e), Stream.of(e)))
- .map(Object::toString);
- }
-
- public static void findModule(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- if (CTX.get().module == null) {
- CTX.get().module =
- annotations.stream()
- .map(roundEnv::getElementsAnnotatedWith)
- .flatMap(Collection::stream)
- .findAny()
- .map(ProcessingContext::getModuleElement)
- .orElse(null);
- }
- }
+ .filter(type -> !type.toString().contains("java.lang.Object"))
+ .map(superType -> (TypeElement) types.asElement(superType))
+ .flatMap(e -> Stream.concat(superTypes(e), Stream.of(e)))
+ .map(Object::toString);
+ }
+
+ public static void validateModule() {
+ var module = APContext.getProjectModuleElement();
+ if (module != null && !module.isUnnamed()) {
+ try (var bufferedReader = APContext.getModuleInfoReader()) {
+ var reader = new ModuleInfoReader(module, bufferedReader);
+ reader.requires().forEach(r -> {
+ if (!r.isStatic() && r.getDependency().getQualifiedName().contentEquals("io.avaje.http.api.javalin")) {
+ logWarn(module, "io.avaje.http.api.javalin only contains SOURCE retention annotations. It should added as `requires static`");
+ }
+ });
+ var fqn = CTX.get().clientFQN;
+
+ reader.validateServices("io.avaje.http.client.HttpClient.GeneratedComponent", Set.of(fqn));
- public static void validateModule(String fqn) {
- var module = CTX.get().module;
- if (module != null && !CTX.get().validated && !module.isUnnamed()) {
-
- CTX.get().validated = true;
- try (var inputStream =
- CTX.get()
- .filer
- .getResource(StandardLocation.SOURCE_PATH, "", "module-info.java")
- .toUri()
- .toURL()
- .openStream();
- var reader = new BufferedReader(new InputStreamReader(inputStream))) {
-
- var noProvides = reader.lines().map(s -> {
- if (s.contains("io.avaje.http.api.javalin") && !s.contains("static")) {
- logWarn("io.avaje.http.api.javalin only contains SOURCE retention annotations. It should added as `requires static`");
- }
- return s;
- })
- .noneMatch(s -> s.contains(fqn));
- if (noProvides && !buildPluginAvailable()) {
- logError(module, "Missing `provides io.avaje.http.client.HttpClient.GeneratedComponent with %s;`", fqn);
- }
} catch (Exception e) {
// can't read module
}
@@ -309,4 +283,8 @@ private static boolean resourceExists(String relativeName) {
return false;
}
}
+
+ public static void addClientComponent(String clientFQN) {
+ CTX.get().clientFQN = clientFQN;
+ }
}
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java
index 1594766f2..037b69507 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java
@@ -316,12 +316,12 @@ static class EnumHandler extends ObjectHandler {
@Override
public String toMethod() {
- return "(" + type.shortType() + ") toEnum(" + type.shortType() + ".class, ";
+ return "(" + type.shortTypeNested() + ") toEnum(" + type.shortTypeNested() + ".class, ";
}
@Override
public String asMethod() {
- return "(" + type.shortType() + ") asEnum(" + type.shortType() + ".class, ";
+ return "(" + type.shortTypeNested() + ") asEnum(" + type.shortTypeNested() + ".class, ";
}
}
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java
index f22658b2b..24738fdb3 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java
@@ -31,6 +31,13 @@ static UType parse(String type) {
*/
String shortType();
+ /**
+ * Return the short type taking nested type into account.
+ */
+ default String shortTypeNested() {
+ return shortType();
+ }
+
/**
* Return the short name.
*/
@@ -152,6 +159,11 @@ public String shortType() {
return Util.shortName(rawType);
}
+ @Override
+ public String shortTypeNested() {
+ return Util.shortName(rawType, true);
+ }
+
@Override
public String shortName() {
return Util.initLower(shortType()).replace(".", "$");
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java
index 331488cfa..e4af9c899 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java
@@ -4,6 +4,7 @@ public enum MediaType {
APPLICATION_JSON("application/json"),
TEXT_PLAIN("text/plain"),
TEXT_HTML("text/html"),
+ HTML_UTF8("text/html;charset=UTF8"),
UNKNOWN("");
private final String value;
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java
index 129ae8cc7..5300dda1b 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java
@@ -34,6 +34,9 @@
@GeneratePrism(value = io.swagger.v3.oas.annotations.Hidden.class, publicAccess = true)
@GeneratePrism(value = io.avaje.http.api.Client.Import.class, publicAccess = true)
@GeneratePrism(value = io.avaje.http.api.RequestTimeout.class, publicAccess = true)
+@GeneratePrism(value = io.avaje.htmx.api.HxRequest.class, publicAccess = true)
+@GeneratePrism(value = io.avaje.htmx.api.Html.class, publicAccess = true)
+@GeneratePrism(value = io.avaje.htmx.api.ContentCache.class, publicAccess = true)
package io.avaje.http.generator.core;
import io.avaje.prism.GeneratePrism;
diff --git a/http-generator-core/src/main/java/module-info.java b/http-generator-core/src/main/java/module-info.java
index 0d5787e3f..f5142424b 100644
--- a/http-generator-core/src/main/java/module-info.java
+++ b/http-generator-core/src/main/java/module-info.java
@@ -10,6 +10,7 @@
// SHADED: All content after this line will be removed at package time
requires static io.avaje.prism;
requires static io.avaje.http.api;
+ requires static io.avaje.htmx.api;
requires static io.swagger.v3.oas.models;
requires static io.swagger.v3.oas.annotations;
requires static java.validation;
diff --git a/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java b/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java
index 12dea5d6d..51e79c70b 100644
--- a/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java
+++ b/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java
@@ -2,6 +2,7 @@
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class UTypeTest {
@@ -17,4 +18,12 @@ void isJavaLangPackage_expect_false() {
assertFalse(UType.isJavaLangPackage("java.lang.other.Foo"));
assertFalse(UType.isJavaLangPackage("not.lang.Foo"));
}
+
+ @Test
+ void parseNestedEnum() {
+ UType uType = UType.parse("my.pack.Foo.NestedEnum");
+ assertThat(uType.mainType()).isEqualTo("my.pack.Foo.NestedEnum");
+ assertThat(uType.shortType()).isEqualTo("Foo.NestedEnum");
+ assertThat(uType.shortTypeNested()).isEqualTo("NestedEnum");
+ }
}
diff --git a/http-generator-helidon/pom.xml b/http-generator-helidon/pom.xml
index 0494b1daf..8fb09b39f 100644
--- a/http-generator-helidon/pom.xml
+++ b/http-generator-helidon/pom.xml
@@ -4,7 +4,7 @@
avaje-http-parent
io.avaje
- 2.6-RC1
+ 2.8-RC1
avaje-http-helidon-generator
diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java
index 8a31e3a51..5afa98747 100644
--- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java
+++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java
@@ -7,14 +7,7 @@
import java.util.Map;
import java.util.Optional;
-import io.avaje.http.generator.core.Append;
-import io.avaje.http.generator.core.CoreWebMethod;
-import io.avaje.http.generator.core.MethodParam;
-import io.avaje.http.generator.core.MethodReader;
-import io.avaje.http.generator.core.ParamType;
-import io.avaje.http.generator.core.PathSegments;
-import io.avaje.http.generator.core.UType;
-import io.avaje.http.generator.core.WebMethod;
+import io.avaje.http.generator.core.*;
import io.avaje.http.generator.core.openapi.MediaType;
import javax.lang.model.type.TypeMirror;
@@ -67,8 +60,10 @@ final class ControllerMethodWriter {
private final boolean useJsonB;
private final boolean instrumentContext;
private final boolean isFilter;
+ private final ControllerReader reader;
- ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB) {
+ ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB, ControllerReader reader) {
+ this.reader = reader;
this.method = method;
this.writer = writer;
this.webMethod = method.webMethod();
@@ -92,10 +87,35 @@ void writeRule() {
} else if (isFilter) {
writer.append(" routing.addFilter(this::_%s);", method.simpleName()).eol();
} else {
- writer.append(" routing.%s(\"%s\", this::_%s);", webMethod.name().toLowerCase(), method.fullPath().replace("\\", "\\\\"), method.simpleName()).eol();
+ writer.append(" routing.%s(\"%s\", ", webMethod.name().toLowerCase(), method.fullPath().replace("\\", "\\\\"));
+ var hxRequest = method.hxRequest();
+ if (hxRequest != null) {
+ writer.append("HxHandler.builder(this::_%s)", method.simpleName());
+ if (hasValue(hxRequest.target())) {
+ writer.append(".target(\"%s\")", hxRequest.target());
+ }
+ if (hasValue(hxRequest.triggerId())) {
+ writer.append(".trigger(\"%s\")", hxRequest.triggerId());
+ } else if (hasValue(hxRequest.value())) {
+ writer.append(".trigger(\"%s\")", hxRequest.value());
+ }
+ if (hasValue(hxRequest.triggerName())) {
+ writer.append(".triggerName(\"%s\")", hxRequest.triggerName());
+ } else if (hasValue(hxRequest.value())) {
+ writer.append(".triggerName(\"%s\")", hxRequest.value());
+ }
+ writer.append(".build());").eol();
+
+ } else {
+ writer.append("this::_%s);", method.simpleName()).eol();
+ }
}
}
+ private static boolean hasValue(String value) {
+ return value != null && !value.isBlank();
+ }
+
void writeHandler(boolean requestScoped) {
if (method.isErrorMethod()) {
writer.append(" private void _%s(ServerRequest req, ServerResponse res, %s ex) {", method.simpleName(), method.exceptionShortName()).eol();
@@ -111,7 +131,7 @@ void writeHandler(boolean requestScoped) {
writer.append(" res.status(%s);", lookupStatusCode(statusCode)).eol();
}
}
-
+ boolean withFormParams = false;
final var bodyType = method.bodyType();
if (bodyType != null && !method.isErrorMethod() && !isFilter) {
if ("InputStream".equals(bodyType)) {
@@ -124,26 +144,41 @@ void writeHandler(boolean requestScoped) {
} else {
defaultHelidonBodyContent();
}
- } else if (usesFormParams()) {
- writer.append(" var formParams = req.content().as(Parameters.class);").eol();
+ } else {
+ withFormParams = usesFormParams();
+ if (withFormParams) {
+ writer.append(" var formParams = req.content().as(Parameters.class);").eol();
+ }
+ }
+ final ResponseMode responseMode = responseMode();
+ final boolean withContentCache = responseMode == ResponseMode.Templating && useContentCache();
+ if (withContentCache) {
+ writer.append(" var key = contentCache.key(req");
+ if (withFormParams) {
+ writer.append(", formParams");
+ }
+ writer.append(");").eol();
+ writer.append(" var cacheContent = contentCache.content(key);").eol();
+ writer.append(" if (cacheContent != null) {").eol();
+ writeContextReturn(" ");
+ writer.append(" res.send(cacheContent);").eol();
+ writer.append(" return;").eol();
+ writer.append(" }").eol();
}
final var segments = method.pathSegments();
if (segments.fullPath().contains("{")) {
writer.append(" var pathParams = req.path().pathParameters();").eol();
}
-
for (final PathSegments.Segment matrixSegment : segments.matrixSegments()) {
matrixSegment.writeCreateSegment(writer, platform());
}
-
final var params = method.params();
for (final MethodParam param : params) {
if (!isExceptionOrFilterChain(param)) {
param.writeCtxGet(writer, segments);
}
}
-
if (method.includeValidate()) {
for (final MethodParam param : params) {
param.writeValidate(writer);
@@ -185,7 +220,7 @@ void writeHandler(boolean requestScoped) {
}
writer.append(");").eol();
- if (!method.isVoid() && !isFilter) {
+ if (responseMode != ResponseMode.Void) {
TypeMirror typeMirror = method.returnType();
boolean includeNoContent = !typeMirror.getKind().isPrimitive();
if (includeNoContent) {
@@ -194,19 +229,29 @@ void writeHandler(boolean requestScoped) {
writer.append(" } else {").eol();
}
String indent = includeNoContent ? " " : " ";
- writeContextReturn(indent);
- if (isInputStream(method.returnType())) {
- final var uType = UType.parse(method.returnType());
- writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
- } else if (producesJson()) {
- if (returnTypeString()) {
- writer.append(indent).append("res.send(result); // send raw JSON").eol();
- } else {
- final var uType = UType.parse(method.returnType());
- writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
+ if (responseMode == ResponseMode.Templating) {
+ writer.append(indent).append("var content = renderer.render(result);").eol();
+ if (withContentCache) {
+ writer.append(indent).append("contentCache.contentPut(key, content);").eol();
}
+ writeContextReturn(indent);
+ writer.append(indent).append("res.send(content);").eol();
+
} else {
- writer.append(indent).append("res.send(result);").eol();
+ writeContextReturn(indent);
+ if (responseMode == ResponseMode.InputStream) {
+ final var uType = UType.parse(method.returnType());
+ writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
+ } else if (responseMode == ResponseMode.Json) {
+ if (returnTypeString()) {
+ writer.append(indent).append("res.send(result); // send raw JSON").eol();
+ } else {
+ final var uType = UType.parse(method.returnType());
+ writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
+ }
+ } else {
+ writer.append(indent).append("res.send(result);").eol();
+ }
}
if (includeNoContent) {
writer.append(" }").eol();
@@ -215,6 +260,40 @@ void writeHandler(boolean requestScoped) {
writer.append(" }").eol().eol();
}
+ enum ResponseMode {
+ Void,
+ Json,
+ Templating,
+ InputStream,
+ Other
+ }
+
+ ResponseMode responseMode() {
+ if (method.isVoid() || isFilter) {
+ return ResponseMode.Void;
+ }
+ if (isInputStream(method.returnType())) {
+ return ResponseMode.InputStream;
+ }
+ if (producesJson()) {
+ return ResponseMode.Json;
+ }
+ if (useTemplating()) {
+ return ResponseMode.Templating;
+ }
+ return ResponseMode.Other;
+ }
+
+ private boolean useContentCache() {
+ return method.hasContentCache();
+ }
+
+ private boolean useTemplating() {
+ return reader.html()
+ && !"byte[]".equals(method.returnType().toString())
+ && (method.produces() == null || method.produces().toLowerCase().contains("html"));
+ }
+
private static boolean isExceptionOrFilterChain(MethodParam param) {
return isAssignable2Interface(param.utype().mainType(), "java.lang.Exception")
|| "FilterChain".equals(param.shortType());
@@ -280,6 +359,7 @@ private void writeContextReturn(String indent) {
final var contentTypeString = "res.headers().contentType(MediaTypes.";
writer.append(indent);
switch (produces) {
+ case HTML_UTF8 -> writer.append("res.headers().contentType(HTML_UTF8);").eol();
case APPLICATION_JSON -> writer.append(contentTypeString).append("APPLICATION_JSON);").eol();
case TEXT_HTML -> writer.append(contentTypeString).append("TEXT_HTML);").eol();
case TEXT_PLAIN -> writer.append(contentTypeString).append("TEXT_PLAIN);").eol();
diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java
index e192f9d05..c2a39d2e8 100644
--- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java
+++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java
@@ -6,6 +6,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import io.avaje.http.generator.core.BaseControllerWriter;
import io.avaje.http.generator.core.Constants;
@@ -59,6 +60,17 @@ class ControllerWriter extends BaseControllerWriter {
reader.addImportType("io.helidon.webserver.http.RoutingRequest");
reader.addImportType("io.helidon.webserver.http.RoutingResponse");
}
+ if (reader.methods().stream()
+ .map(MethodReader::hxRequest)
+ .anyMatch(Objects::nonNull)) {
+ reader.addImportType("io.avaje.htmx.nima.HxHandler");
+ }
+ if (reader.html()) {
+ reader.addImportType("io.avaje.htmx.nima.TemplateRender");
+ if (reader.hasContentCache()) {
+ reader.addImportType("io.avaje.htmx.nima.TemplateContentCache");
+ }
+ }
}
void write() {
@@ -80,7 +92,7 @@ protected void writeImports() {
private List writerMethods() {
return reader.methods().stream()
.filter(MethodReader::isWebMethod)
- .map(it -> new ControllerMethodWriter(it, writer, useJsonB))
+ .map(it -> new ControllerMethodWriter(it, writer, useJsonB, reader))
.toList();
}
@@ -120,16 +132,24 @@ private void writeClassStart() {
if (reader.isIncludeValidator()) {
writer.append(" private static final HeaderName HEADER_ACCEPT_LANGUAGE = HeaderNames.create(\"Accept-Language\");").eol();
}
+ if (reader.html()) {
+ writer.append(" private static final io.helidon.common.media.type.MediaType HTML_UTF8 = MediaTypes.create(\"text/html;charset=UTF8\");").eol();
+ }
writer.append(" private final %s %s;", controllerType, controllerName).eol();
if (reader.isIncludeValidator()) {
writer.append(" private final Validator validator;").eol();
}
-
if (instrumentContext) {
writer.append(" private final RequestContextResolver resolver;").eol();
}
+ if (reader.html()) {
+ writer.append(" private final TemplateRender renderer;").eol();
+ if (reader.hasContentCache()) {
+ writer.append(" private final TemplateContentCache contentCache;").eol();
+ }
+ }
for (final UType type : jsonTypes.values()) {
if (!isInputStream(type.full())) {
@@ -146,6 +166,12 @@ private void writeClassStart() {
if (useJsonB) {
writer.append(", Jsonb jsonb");
}
+ if (reader.html()) {
+ writer.append(", TemplateRender renderer");
+ if (reader.hasContentCache()) {
+ writer.append(", TemplateContentCache contentCache");
+ }
+ }
if (instrumentContext) {
writer.append(", RequestContextResolver resolver");
}
@@ -155,6 +181,12 @@ private void writeClassStart() {
if (reader.isIncludeValidator()) {
writer.append(" this.validator = validator;").eol();
}
+ if (reader.html()) {
+ writer.append(" this.renderer = renderer;").eol();
+ if (reader.hasContentCache()) {
+ writer.append(" this.contentCache = contentCache;").eol();
+ }
+ }
if (instrumentContext) {
writer.append(" this.resolver = resolver;").eol();
}
@@ -176,6 +208,6 @@ private void writeClassStart() {
}
private boolean isInputStream(String type) {
- return isAssignable2Interface(type.toString(), "java.io.InputStream");
+ return isAssignable2Interface(type, "java.io.InputStream");
}
}
diff --git a/http-generator-javalin/pom.xml b/http-generator-javalin/pom.xml
index ea9a6ae73..1985a057c 100644
--- a/http-generator-javalin/pom.xml
+++ b/http-generator-javalin/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-javalin-generator
diff --git a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java
index 3cda06122..799d67763 100644
--- a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java
+++ b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java
@@ -4,14 +4,7 @@
import java.util.List;
-import io.avaje.http.generator.core.Append;
-import io.avaje.http.generator.core.CoreWebMethod;
-import io.avaje.http.generator.core.MethodParam;
-import io.avaje.http.generator.core.MethodReader;
-import io.avaje.http.generator.core.PathSegments;
-import io.avaje.http.generator.core.UType;
-import io.avaje.http.generator.core.Util;
-import io.avaje.http.generator.core.WebMethod;
+import io.avaje.http.generator.core.*;
import io.avaje.http.generator.core.openapi.MediaType;
/** Write code to register Web route for a given controller method. */
@@ -158,8 +151,9 @@ private void writeContextReturn() {
}
private void writeContextReturn(final String resultVariableName) {
- final var produces = method.produces();
- if (produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces)) {
+ var produces = method.produces();
+ boolean applicationJson = produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces);
+ if (applicationJson || JsonBUtil.isJsonMimeType(produces)) {
if (useJsonB) {
var uType = UType.parse(method.returnType());
final boolean isfuture = "java.util.concurrent.CompletableFuture".equals(uType.mainType());
@@ -169,12 +163,19 @@ private void writeContextReturn(final String resultVariableName) {
}
writer.append(" try {");
}
- writer.append(" %sJsonType.toJson(%s, ctx.contentType(\"application/json\").res().getOutputStream());", uType.shortName(), resultVariableName);
+ if (produces == null) {
+ produces = MediaType.APPLICATION_JSON.getValue();
+ }
+ writer.append(" %sJsonType.toJson(%s, ctx.contentType(\"%s\").res().getOutputStream());", uType.shortName(), resultVariableName, produces);
if (isfuture || method.isErrorMethod()) {
writer.append(" } catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }");
}
} else {
- writer.append(" ctx.json(%s);", resultVariableName);
+ if (applicationJson) {
+ writer.append(" ctx.json(%s);", resultVariableName);
+ } else {
+ writer.append(" ctx.contentType(\"%s\").json(%s);", produces, resultVariableName);
+ }
}
} else if (MediaType.TEXT_HTML.getValue().equalsIgnoreCase(produces)) {
writer.append(" ctx.html(%s);", resultVariableName);
diff --git a/http-generator-jex/pom.xml b/http-generator-jex/pom.xml
index 787b49aca..f77bac53a 100644
--- a/http-generator-jex/pom.xml
+++ b/http-generator-jex/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
avaje-http-jex-generator
diff --git a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java
index dc663e297..da6d99a86 100644
--- a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java
+++ b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java
@@ -3,12 +3,7 @@
import static io.avaje.http.generator.core.ProcessingContext.platform;
import java.util.List;
-import io.avaje.http.generator.core.Append;
-import io.avaje.http.generator.core.MethodParam;
-import io.avaje.http.generator.core.MethodReader;
-import io.avaje.http.generator.core.PathSegments;
-import io.avaje.http.generator.core.Util;
-import io.avaje.http.generator.core.WebMethod;
+import io.avaje.http.generator.core.*;
import io.avaje.http.generator.core.openapi.MediaType;
/**
@@ -102,6 +97,8 @@ private void writeContextReturn() {
writer.append("ctx.html(");
} else if (produces.equalsIgnoreCase(MediaType.TEXT_PLAIN.getValue())) {
writer.append("ctx.text(");
+ } else if (JsonBUtil.isJsonMimeType(produces)) {
+ writer.append("ctx.contentType(\"%s\").json(", produces);
} else {
writer.append("ctx.contentType(\"%s\").write(", produces);
}
diff --git a/http-hibernate-validator/pom.xml b/http-hibernate-validator/pom.xml
index 2990065c8..323d53949 100644
--- a/http-hibernate-validator/pom.xml
+++ b/http-hibernate-validator/pom.xml
@@ -5,12 +5,12 @@
io.avaje
avaje-http-hibernate-validator
- 3.5-RC3
+ 3.5
org.avaje
java11-oss
- 3.10
+ 4.3
@@ -36,14 +36,14 @@
io.avaje
avaje-http-api
- 2.0-RC1
+ 2.6
provided
io.avaje
avaje-inject
- 9.3
+ 10.1
provided
diff --git a/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java b/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java
index ce2d3356a..cb8759224 100644
--- a/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java
+++ b/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java
@@ -6,7 +6,7 @@
/**
* Plugin for avaje inject that provides a default BeanValidator instance.
*/
-public final class ValidatorProvider implements io.avaje.inject.spi.Plugin {
+public final class ValidatorProvider implements io.avaje.inject.spi.InjectPlugin {
@Override
public Class>[] provides() {
diff --git a/http-hibernate-validator/src/main/java/module-info.java b/http-hibernate-validator/src/main/java/module-info.java
index dbd4a1f54..2d1c0f6f9 100644
--- a/http-hibernate-validator/src/main/java/module-info.java
+++ b/http-hibernate-validator/src/main/java/module-info.java
@@ -8,5 +8,5 @@
requires io.avaje.inject;
requires jakarta.validation;
- provides io.avaje.inject.spi.Plugin with ValidatorProvider;
+ provides io.avaje.inject.spi.InjectExtension with ValidatorProvider;
}
diff --git a/http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin b/http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.InjectExtension
similarity index 100%
rename from http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin
rename to http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.InjectExtension
diff --git a/http-inject-plugin/pom.xml b/http-inject-plugin/pom.xml
index fc8de7fac..91e9da057 100644
--- a/http-inject-plugin/pom.xml
+++ b/http-inject-plugin/pom.xml
@@ -4,7 +4,7 @@
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
..
@@ -19,17 +19,24 @@
io.avaje
avaje-inject
- 9.12
+ 10.3
provided
true
io.avaje
avaje-http-api
- 1.37
+ 2.7
true
provided
+
+ io.avaje
+ avaje-spi-service
+ 2.6
+ provided
+ true
+
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
index 56b5ad2f2..b22eb7714 100644
--- a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
@@ -3,10 +3,12 @@
import io.avaje.http.api.context.RequestContextResolver;
import io.avaje.http.api.context.ThreadLocalRequestContextResolver;
import io.avaje.inject.BeanScopeBuilder;
-import io.avaje.inject.spi.Plugin;
+import io.avaje.inject.spi.InjectPlugin;
+import io.avaje.spi.ServiceProvider;
/** Plugin for avaje inject that provides a default RequestContextResolver instance. */
-public final class DefaultResolverProvider implements Plugin {
+@ServiceProvider
+public final class DefaultResolverProvider implements InjectPlugin {
@Override
public Class>[] provides() {
diff --git a/http-inject-plugin/src/main/java/module-info.java b/http-inject-plugin/src/main/java/module-info.java
index c8149f2fe..e13c5e735 100644
--- a/http-inject-plugin/src/main/java/module-info.java
+++ b/http-inject-plugin/src/main/java/module-info.java
@@ -1,8 +1,8 @@
module io.avaje.http.plugin {
- requires io.avaje.http.api;
- requires io.avaje.inject;
-
- provides io.avaje.inject.spi.Plugin with io.avaje.http.inject.DefaultResolverProvider;
+ requires io.avaje.http.api;
+ requires io.avaje.inject;
+ requires static io.avaje.spi;
+ provides io.avaje.inject.spi.InjectExtension with io.avaje.http.inject.DefaultResolverProvider;
}
diff --git a/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin b/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin
deleted file mode 100644
index d996e4e0c..000000000
--- a/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin
+++ /dev/null
@@ -1 +0,0 @@
-io.avaje.http.inject.DefaultResolverProvider
diff --git a/pom.xml b/pom.xml
index a5f16b9ec..e569bebd2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,12 +4,12 @@
org.avaje
java11-oss
- 4.1
+ 4.4
io.avaje
avaje-http-parent
- 2.6-RC1
+ 2.8-RC1
pom
@@ -19,9 +19,9 @@
true
- 2.2.22
+ 2.2.23
2.14.2
- 1.21
+ 1.31
${project.build.directory}${file.separator}module-info.shade
@@ -35,6 +35,7 @@
+ htmx-api
http-api
http-api-javalin
http-client
@@ -60,12 +61,23 @@
jdk21plus
- [21,22]
+ [21,)
+ htmx-nima
+ htmx-nima-jstache
http-generator-helidon
+
+ test21
+
+ htmx-nima
+ htmx-nima-jstache
+ http-generator-helidon
+ tests
+
+
module-info.shade
diff --git a/tests/pom.xml b/tests/pom.xml
index 61d79f2b6..cdec44f34 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -4,7 +4,7 @@
avaje-http-parent
io.avaje
- 2.6-RC1
+ 2.8-RC1
tests
@@ -12,13 +12,13 @@
true
- 5.10.2
- 3.26.0
- 2.17.1
+ 5.11.0
+ 3.26.3
+ 2.17.2
2.5
- 9.12
- 4.0.9
- 6.1.6
+ 10.3
+ 4.1.0
+ 6.3.0
@@ -33,11 +33,12 @@
jdk21plus
- [21,22]
+ [21,)
test-nima
test-nima-jsonb
+ test-nima-htmx
diff --git a/tests/test-client-generation/pom.xml b/tests/test-client-generation/pom.xml
index 049ff4b3c..a1d6ba4da 100644
--- a/tests/test-client-generation/pom.xml
+++ b/tests/test-client-generation/pom.xml
@@ -4,7 +4,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-client-generation
diff --git a/tests/test-client/pom.xml b/tests/test-client/pom.xml
index d2d60d9da..70285d69d 100644
--- a/tests/test-client/pom.xml
+++ b/tests/test-client/pom.xml
@@ -6,7 +6,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-client
diff --git a/tests/test-javalin-jsonb/pom.xml b/tests/test-javalin-jsonb/pom.xml
index f45031ffb..d910cbb86 100644
--- a/tests/test-javalin-jsonb/pom.xml
+++ b/tests/test-javalin-jsonb/pom.xml
@@ -5,7 +5,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-javalin-jsonb
@@ -13,7 +13,7 @@
true
org.example.myapp.Main
- 2.2.22
+ 2.2.23
1.3.71
@@ -80,13 +80,13 @@
io.avaje
avaje-jsonb
- 1.11
+ 2.1
io.avaje
avaje-jsonb-generator
- 1.11
+ 2.1
provided
@@ -101,7 +101,7 @@
io.rest-assured
rest-assured
- 5.4.0
+ 5.5.0
test
diff --git a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java
index a918215ed..fb6d8ecf5 100644
--- a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java
+++ b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java
@@ -25,6 +25,7 @@
import io.javalin.http.Context;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.inject.Inject;
+import org.example.myapp.web.other.Foo;
/**
* Hello resource manager.
@@ -82,6 +83,7 @@ List findByName(String name, @QueryParam("my-param") @Default("one") S
/**
* Simple example post with JSON body response.
*/
+ @Produces(MediaType.APPLICATION_JSON_PATCH_JSON)
@Post
HelloDto post(HelloDto dto) {
dto.name = "posted";
@@ -182,4 +184,10 @@ String controlStatusCode(Context ctx) {
ctx.status(201);
return "controlStatusCode";
}
+
+ @Produces(value = "text/plain")
+ @Get("takesNestedEnum")
+ String takesNestedEnum(Foo.NestedEnum myEnum) {
+ return "takesNestedEnum-" + myEnum;
+ }
}
diff --git a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java
new file mode 100644
index 000000000..f3ed0507a
--- /dev/null
+++ b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java
@@ -0,0 +1,7 @@
+package org.example.myapp.web.other;
+
+public class Foo {
+ public enum NestedEnum {
+ A, B, C
+ }
+}
diff --git a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
index 087e3dd87..549828b2d 100644
--- a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
+++ b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
@@ -1,7 +1,7 @@
{
"openapi" : "3.0.1",
"info" : {
- "title" : "Example service showing off the Path extension method of controller",
+ "title" : "Example service",
"description" : "Example Javalin controllers with Java and Maven",
"version" : ""
},
@@ -14,11 +14,11 @@
"tags" : [
{
"name" : "tag1",
- "description" : "this is added to openapi tags"
+ "description" : "it's somethin"
},
{
"name" : "tag1",
- "description" : "it's somethin"
+ "description" : "this is added to openapi tags"
}
],
"paths" : {
@@ -320,7 +320,7 @@
"201" : {
"description" : "",
"content" : {
- "application/json" : {
+ "application/json-patch+json" : {
"schema" : {
"$ref" : "#/components/schemas/HelloDto"
}
@@ -646,6 +646,41 @@
}
}
},
+ "/hello/takesNestedEnum" : {
+ "get" : {
+ "tags" : [
+
+ ],
+ "summary" : "",
+ "description" : "",
+ "parameters" : [
+ {
+ "name" : "myEnum",
+ "in" : "query",
+ "schema" : {
+ "type" : "string",
+ "enum" : [
+ "A",
+ "B",
+ "C"
+ ]
+ }
+ }
+ ],
+ "responses" : {
+ "200" : {
+ "description" : "",
+ "content" : {
+ "text/plain" : {
+ "schema" : {
+ "type" : "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/hello/withMatrix/{year_segment}/{other}" : {
"get" : {
"tags" : [
diff --git a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
index cc5441088..b2262c874 100644
--- a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
+++ b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
@@ -25,7 +25,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.http.generator.javalin.JavalinProcessor;
-import io.avaje.jsonb.generator.Processor;
+import io.avaje.jsonb.generator.JsonbProcessor;
class JavalinProcessorTest {
@@ -76,7 +76,7 @@ public void runAnnotationProcessorJsonB() throws Exception {
final var task =
compiler.getTask(
new PrintWriter(System.out), null, null, List.of("--release=11"), null, files);
- task.setProcessors(List.of(new JavalinProcessor(), new Processor()));
+ task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor()));
assertThat(task.call()).isTrue();
assert Files.readString(
@@ -104,7 +104,7 @@ public void runAnnotationProcessorJavax() throws Exception {
"-AdisableDirectWrites=true"),
null,
files);
- task.setProcessors(List.of(new JavalinProcessor(), new Processor()));
+ task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor()));
// we don't have javax on the cp
assertThat(task.call()).isFalse();
@@ -133,7 +133,7 @@ public void runAnnotationProcessorJakarta() throws Exception {
"-AdisableDirectWrites=true"),
null,
files);
- task.setProcessors(List.of(new JavalinProcessor(), new Processor()));
+ task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor()));
assertThat(task.call()).isTrue();
@@ -163,7 +163,7 @@ public void testOpenAPIGeneration() throws Exception {
List.of("--release=11", "-AdisableDirectWrites=true"),
null,
openAPIController);
- task.setProcessors(List.of(new JavalinProcessor(), new Processor()));
+ task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor()));
assertThat(task.call()).isTrue();
@@ -197,7 +197,7 @@ public void testInheritableOpenAPIGeneration() throws Exception {
List.of("--release=11", "-AdisableDirectWrites=true"),
null,
openAPIController);
- task.setProcessors(List.of(new JavalinProcessor(), new Processor()));
+ task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor()));
assertThat(task.call()).isTrue();
diff --git a/tests/test-javalin/pom.xml b/tests/test-javalin/pom.xml
index 6307622f1..925384baf 100644
--- a/tests/test-javalin/pom.xml
+++ b/tests/test-javalin/pom.xml
@@ -4,7 +4,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-javalin
@@ -12,7 +12,7 @@
true
org.example.myapp.Main
- 2.2.22
+ 2.2.23
1.3.71
@@ -94,7 +94,7 @@
io.rest-assured
rest-assured
- 5.4.0
+ 5.5.0
test
diff --git a/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java b/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java
index 4ddd454ba..bfad1c72a 100644
--- a/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java
+++ b/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java
@@ -82,6 +82,7 @@ List findByName(String name, @QueryParam("my-param") @Default("one") S
/**
* Simple example post with JSON body response.
*/
+ @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@Post
HelloDto post(HelloDto dto) {
dto.name = "posted";
diff --git a/tests/test-javalin/src/main/resources/public/openapi.json b/tests/test-javalin/src/main/resources/public/openapi.json
index ff158eb08..76a5ad5b4 100644
--- a/tests/test-javalin/src/main/resources/public/openapi.json
+++ b/tests/test-javalin/src/main/resources/public/openapi.json
@@ -310,7 +310,7 @@
"201" : {
"description" : "",
"content" : {
- "application/json" : {
+ "application/json;charset=UTF-8" : {
"schema" : {
"$ref" : "#/components/schemas/HelloDto"
}
diff --git a/tests/test-jex/pom.xml b/tests/test-jex/pom.xml
index 70714c850..dc6834c5f 100644
--- a/tests/test-jex/pom.xml
+++ b/tests/test-jex/pom.xml
@@ -4,7 +4,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-jex
@@ -13,7 +13,7 @@
true
org.example.myapp.Main
2.5
- 2.2.22
+ 2.2.23
diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml
new file mode 100644
index 000000000..30dc21e50
--- /dev/null
+++ b/tests/test-nima-htmx/pom.xml
@@ -0,0 +1,100 @@
+
+
+ 4.0.0
+
+ io.avaje
+ tests
+ 2.8-RC1
+
+
+ test-nima-htmx
+
+
+ 21
+ UTF-8
+ false
+ 1.3.6
+
+
+
+
+ io.avaje
+ avaje-inject
+ ${avaje-inject.version}
+
+
+ io.avaje
+ avaje-htmx-nima-jstache
+ ${project.version}
+
+
+ io.jstach
+ jstachio
+ ${io.jstach.version}
+
+
+ io.avaje
+ avaje-htmx-api
+ ${project.version}
+
+
+ io.avaje
+ avaje-htmx-nima
+ ${project.version}
+
+
+ io.avaje
+ avaje-nima
+ 1.0
+
+
+
+
+ io.avaje
+ avaje-nima-test
+ 1.0
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 21
+
+
+ io.avaje
+ avaje-http-helidon-generator
+ ${project.version}
+
+
+ io.avaje
+ avaje-inject-generator
+ ${avaje-inject.version}
+
+
+ io.avaje
+ avaje-jsonb-generator
+ 2.1
+
+
+ io.jstach
+ jstachio-apt
+ ${io.jstach.version}
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java
new file mode 100644
index 000000000..1b6d205ad
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java
@@ -0,0 +1,15 @@
+package org.example.htmx;
+
+import io.avaje.inject.InjectModule;
+import io.avaje.nima.Nima;
+
+@InjectModule(name = "hxTest")
+public class Main {
+
+ public static void main(String[] args) {
+ Nima.builder()
+ .port(8090)
+ .build()
+ .start();
+ }
+}
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java
new file mode 100644
index 000000000..482ffe033
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java
@@ -0,0 +1,32 @@
+package org.example.htmx;
+
+import io.avaje.htmx.api.Html;
+import io.avaje.htmx.api.ContentCache;
+import io.avaje.htmx.api.HxRequest;
+import io.avaje.http.api.Controller;
+import io.avaje.http.api.Get;
+import io.avaje.http.api.Path;
+
+import java.time.Instant;
+import java.util.List;
+
+@Html
+@Controller
+@Path("/")
+public class UIController {
+
+ @ContentCache
+ @Get
+ ViewHome index() {
+ return new ViewHome("Robin3");
+ }
+
+ @ContentCache
+ @HxRequest(target = "name")
+ @Get("name")
+ ViewName name() {
+ var mlist = List.of("one","two","three", "four");
+ return new ViewName("JimBolin", Instant.now(), "MoreMeMore", mlist);
+ }
+
+}
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java
new file mode 100644
index 000000000..607dc690b
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java
@@ -0,0 +1,7 @@
+package org.example.htmx;
+
+import io.jstach.jstache.JStache;
+
+@JStache(path = "home")
+public record ViewHome(String name) {
+}
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java
new file mode 100644
index 000000000..2cda3b0cf
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java
@@ -0,0 +1,13 @@
+package org.example.htmx;
+
+import io.jstach.jstache.JStache;
+
+import java.time.Instant;
+import java.util.List;
+
+@JStache(path = "name")
+public record ViewName(String name, Instant foo, String more, List mlist) {
+ public String when() {
+ return foo.toString();
+ }
+}
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java
new file mode 100644
index 000000000..a0e8d10d8
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java
@@ -0,0 +1,4 @@
+@JStachePath(prefix = "ui/", suffix = ".mustache")
+package org.example.htmx;
+
+import io.jstach.jstache.JStachePath;
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java
new file mode 100644
index 000000000..ec94bf95e
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java
@@ -0,0 +1,14 @@
+package org.example.htmx.template;
+
+import io.avaje.htmx.nima.TemplateRender;
+import io.avaje.inject.Component;
+import io.jstach.jstachio.JStachio;
+
+@Component
+public class JstacheTemplateRender implements TemplateRender {
+
+ @Override
+ public String render(Object viewModel) {
+ return JStachio.render(viewModel);
+ }
+}
diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java
new file mode 100644
index 000000000..bf1757496
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java
@@ -0,0 +1,33 @@
+package org.example.htmx.template;
+
+import io.avaje.inject.Component;
+import io.avaje.htmx.nima.TemplateContentCache;
+import io.helidon.webserver.http.ServerRequest;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class SimpleContentCache implements TemplateContentCache {
+
+ private final ConcurrentHashMap localCache = new ConcurrentHashMap<>();
+
+ @Override
+ public String key(ServerRequest req) {
+ return req.requestedUri().path().rawPath();
+ }
+
+ @Override
+ public String key(ServerRequest req, Object formParams) {
+ return req.requestedUri().path().rawPath() + formParams;
+ }
+
+ @Override
+ public String content(String key) {
+ return localCache.get(key);
+ }
+
+ @Override
+ public void contentPut(String key, String content) {
+ localCache.put(key, content);
+ }
+}
diff --git a/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache b/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache
new file mode 100644
index 000000000..26a8c9e85
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache
@@ -0,0 +1,11 @@
+
+
+
+ Hi
+
+
+
+Heading
+{{$body}}Empty body{{/body}}
+
+
diff --git a/tests/test-nima-htmx/src/main/resources/ui/home.mustache b/tests/test-nima-htmx/src/main/resources/ui/home.mustache
new file mode 100644
index 000000000..af4686584
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/resources/ui/home.mustache
@@ -0,0 +1,8 @@
+{{
+ Hi there {{ name }}
+
+ One
+{{/body}}
+{{/fragments/layout}}
diff --git a/tests/test-nima-htmx/src/main/resources/ui/name.mustache b/tests/test-nima-htmx/src/main/resources/ui/name.mustache
new file mode 100644
index 000000000..4c2f0f851
--- /dev/null
+++ b/tests/test-nima-htmx/src/main/resources/ui/name.mustache
@@ -0,0 +1,6 @@
+
+Yond {{ name }} its {{ when }} !! and {{ more }} sad
+ {{#mlist}}
+ in {{.}} ot
+ {{/mlist}}
+
diff --git a/tests/test-nima-jsonb/pom.xml b/tests/test-nima-jsonb/pom.xml
index 6b9f309a0..5d0fe3206 100644
--- a/tests/test-nima-jsonb/pom.xml
+++ b/tests/test-nima-jsonb/pom.xml
@@ -6,7 +6,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-nima-jsonb
@@ -31,7 +31,7 @@
io.avaje
avaje-jsonb
- 1.11
+ 2.1
io.avaje
@@ -90,7 +90,7 @@
io.avaje
avaje-jsonb-generator
- 1.11
+ 2.1
@@ -109,7 +109,7 @@
io.avaje
avaje-inject-maven-plugin
- 1.2
+ 10.3
process-sources
diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java b/tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java
similarity index 92%
rename from tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java
rename to tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java
index 47fb7479c..dcc7aa3ec 100644
--- a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java
+++ b/tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java
@@ -1,4 +1,4 @@
-package org.example.path.nest;
+package org.example.htmx;
import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java b/tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java
similarity index 61%
rename from tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java
rename to tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java
index 73f306ff6..d9695fcaa 100644
--- a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java
+++ b/tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java
@@ -1,5 +1,5 @@
@Path("nested")
-package org.example.path.nest;
+package org.example.htmx;
import io.avaje.http.api.Path;
diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java b/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java
index 5d1a3f970..2421bd26e 100644
--- a/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java
+++ b/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java
@@ -1,6 +1,6 @@
package org.example.path;
-import org.example.path.nest.PathNestController.NestedTypeResponse;
+import org.example.htmx.PathNestController.NestedTypeResponse;
import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
diff --git a/tests/test-nima/pom.xml b/tests/test-nima/pom.xml
index 3a909037e..ba385f431 100644
--- a/tests/test-nima/pom.xml
+++ b/tests/test-nima/pom.xml
@@ -6,7 +6,7 @@
io.avaje
tests
- 2.6-RC1
+ 2.8-RC1
test-nima