pre-major refactoring + ext api.

This commit is contained in:
Relism
2026-03-26 14:10:53 +01:00
parent f8c86e315f
commit 7b996b552b
57 changed files with 3823 additions and 117 deletions
+30 -11
View File
@@ -125,18 +125,37 @@ public class HttpServer {
return this;
}
public HttpServer register(RequestHandler handler, Middleware... m) { globalRouter.register(handler, m); return this; }
// ── Direct registration (called by FlashApp via RouteHandle.with()) ──────
public HttpServer get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.get (path, h, m); return this; }
public HttpServer post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.post (path, h, m); return this; }
public HttpServer put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.put (path, h, m); return this; }
public HttpServer delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.delete (path, h, m); return this; }
public HttpServer patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.patch (path, h, m); return this; }
public HttpServer options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.options(path, h, m); return this; }
public HttpServer head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.head (path, h, m); return this; }
public HttpServer trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.trace (path, h, m); return this; }
public HttpServer connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.connect(path, h, m); return this; }
public HttpServer purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.purge (path, h, m); return this; }
public HttpServer doRegister(dev.relism.http.HttpMethod method, String path,
SimpleHandler.FunctionalHandler handler,
dev.relism.routing.Middleware[] middlewares) {
globalRouter.doRegister(method, path, handler, middlewares);
return this;
}
public HttpServer doRegister(RequestHandler handler,
dev.relism.routing.Middleware[] middlewares) {
globalRouter.doRegister(handler, middlewares);
return this;
}
// ── Fluent registration (delegates to globalRouter) ───────────────────────
public dev.relism.routing.RouteHandle<HttpServer> register(RequestHandler handler) {
return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(handler, m));
}
public dev.relism.routing.RouteHandle<HttpServer> get (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.GET, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> post (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.POST, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> put (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.PUT, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> delete (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.DELETE, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> patch (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.PATCH, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> options(String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.OPTIONS, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> head (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.HEAD, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> trace (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.TRACE, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> connect(String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.CONNECT, path, h, m)); }
public dev.relism.routing.RouteHandle<HttpServer> purge (String path, SimpleHandler.FunctionalHandler h) { return new dev.relism.routing.RouteHandle<>(this, m -> globalRouter.doRegister(dev.relism.http.HttpMethod.PURGE, path, h, m)); }
private void process(Socket socket) {
@@ -0,0 +1,53 @@
package dev.relism.exceptions;
/**
* Runtime exception carrying an HTTP status code. Extensions map this to a
* structured JSON error response via the global exception handler installed by
* {@code flash-ext-jackson}.
*
* <pre>{@code
* throw HttpException.notFound("Blog");
* throw new HttpException(422, "Validation failed: title is required");
* }</pre>
*/
public class HttpException extends RuntimeException {
private final int status;
public HttpException(int status, String message) {
super(message);
this.status = status;
}
public int status() { return status; }
// ── Factory methods ───────────────────────────────────────────────────────
public static HttpException badRequest(String message) {
return new HttpException(400, message);
}
public static HttpException unauthorized() {
return new HttpException(401, "Unauthorized");
}
public static HttpException forbidden() {
return new HttpException(403, "Forbidden");
}
public static HttpException notFound(String what) {
return new HttpException(404, what + " not found");
}
public static HttpException conflict(String message) {
return new HttpException(409, message);
}
public static HttpException unprocessable(String message) {
return new HttpException(422, message);
}
public static HttpException internal(String message) {
return new HttpException(500, message);
}
}
@@ -0,0 +1,22 @@
package dev.relism.extension;
import dev.relism.models.RequestHandler;
import dev.relism.routing.Middleware;
import java.util.List;
/**
* Inspects a handler class at registration time and returns zero or more
* {@link Middleware middlewares} to inject automatically.
*
* <p>Processors are called once per {@link FlashApp#register} call, before
* the handler is compiled into the router. Returning an empty list is always
* valid — processors may also use the call purely for side effects
* (e.g. collecting OpenAPI metadata).
*
* <p>Register processors via {@link ExtensionContext#addAnnotationProcessor}.
*/
@FunctionalInterface
public interface AnnotationProcessor {
List<Middleware> process(Class<? extends RequestHandler> handlerClass);
}
@@ -0,0 +1,62 @@
package dev.relism.extension;
import java.util.*;
/**
* Shared registry passed to every extension during {@link FlashExtension#install}.
* Extensions use it in two ways:
* <ol>
* <li><b>Service sharing</b> — provide/require typed objects (e.g. {@code ObjectMapper},
* {@code OpenApiBuilder}) so extensions can build on each other.</li>
* <li><b>Annotation processing</b> — register {@link AnnotationProcessor}s that
* {@link FlashApp} calls for every handler, injecting middleware derived from
* annotations ({@code @RolesAllowed}, {@code @Authenticated}, etc.).</li>
* </ol>
*/
public class ExtensionContext {
private final Map<Class<?>, Object> registry = new LinkedHashMap<>();
private final List<AnnotationProcessor> processors = new ArrayList<>();
// ── Service registry ─────────────────────────────────────────────────────
/** Stores {@code instance} under {@code type} for retrieval by other extensions. */
public <T> void provide(Class<T> type, T instance) {
registry.put(type, instance);
}
/**
* Retrieves the service registered under {@code type}.
* Throws {@link IllegalStateException} if not present — install order matters.
*/
@SuppressWarnings("unchecked")
public <T> T require(Class<T> type) {
T val = (T) registry.get(type);
if (val == null)
throw new IllegalStateException(
"Extension dependency not found: " + type.getSimpleName() +
" — install the required extension first");
return val;
}
/** Returns the service under {@code type}, or empty if not installed. */
@SuppressWarnings("unchecked")
public <T> Optional<T> find(Class<T> type) {
return Optional.ofNullable((T) registry.get(type));
}
// ── Annotation processors ────────────────────────────────────────────────
/**
* Registers an {@link AnnotationProcessor}. Called by extensions during
* {@link FlashExtension#install}. Processors are invoked in registration order.
*/
public void addAnnotationProcessor(AnnotationProcessor processor) {
processors.add(processor);
}
/** Returns an unmodifiable view of all registered processors. */
List<AnnotationProcessor> processors() {
return Collections.unmodifiableList(processors);
}
}
@@ -0,0 +1,182 @@
package dev.relism.extension;
import dev.relism.HttpServer;
import dev.relism.http.HttpMethod;
import dev.relism.models.RequestHandler;
import dev.relism.models.SimpleHandler;
import dev.relism.routing.AbstractRouter;
import dev.relism.routing.Middleware;
import dev.relism.routing.RouteHandle;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
/**
* Primary entry point for working with Flash. Wraps {@link HttpServer} and
* wires the extension layer.
*
* <p>The key addition over raw {@link HttpServer} is the annotation-aware
* {@link #register} override: before delegating to Flash it runs all registered
* {@link AnnotationProcessor}s and prepends any injected middlewares
* (e.g. from {@code @RolesAllowed}) to the explicit ones.
*
* <p>Route registration is lazy: calling {@code get()}, {@code post()}, or
* {@code register()} returns a {@link RouteHandle} that is not yet registered.
* The route is registered automatically before the next operation or at
* {@link #start()}. Call {@link RouteHandle#with} explicitly to add middleware:
*
* <pre>{@code
* FlashApp.of(new HttpServer(config))
* .install(new JacksonExtension())
* .install(new OidcExtension(OidcConfig.fromEnv()))
* .install(new OpenApiExtension("/openapi"))
* .start();
*
* // No middleware — .with() is optional
* app.get("/ping", (req, res) -> "pong");
* app.get("/hello", (req, res) -> "Hello");
*
* // With middleware
* app.get("/me", (req, res) -> ClaimsHolder.user().email()).with(oidc.protect());
* app.get("/admin", (req, res) -> "secret").with(oidc.requireRole("admin"));
*
* // Class-based — @Authenticated / @RolesAllowed auto-injected, .with() optional
* app.register(new HomePage());
* app.register(new MePage()); // @Authenticated handled by annotation processor
* app.register(new AdminPage()); // @RolesAllowed("admin") handled automatically
*
* app.start();
* }</pre>
*/
public class FlashApp {
private final HttpServer server;
private final ExtensionContext ctx = new ExtensionContext();
/** The last returned RouteHandle that has not yet been registered. */
private RouteHandle<?> pending;
private FlashApp(HttpServer server) {
this.server = server;
}
public static FlashApp of(HttpServer server) {
return new FlashApp(server);
}
// ── Pending flush ─────────────────────────────────────────────────────────
/**
* Registers any pending route (returned by the previous {@code get/post/register}
* call) with no middleware, if it has not already been registered via
* {@link RouteHandle#with}.
*/
private void flushPending() {
if (pending != null) {
pending.ensureRegistered();
pending = null;
}
}
private <P> RouteHandle<P> pending(RouteHandle<P> handle) {
flushPending();
pending = handle;
return handle;
}
// ── Extension installation ────────────────────────────────────────────────
public FlashApp install(FlashExtension ext) {
flushPending();
ext.install(this, ctx);
return this;
}
/** Exposes the context so callers can retrieve services installed by extensions. */
public ExtensionContext ctx() {
return ctx;
}
// ── Handler registration (annotation-aware) ───────────────────────────────
/**
* Begins registration of a class-based handler. Before registering, all
* {@link AnnotationProcessor}s are called (e.g. to inject OIDC middleware from
* {@code @Authenticated} / {@code @RolesAllowed}). Their middlewares are prepended
* outermost to any extra middlewares supplied via {@link RouteHandle#with}.
*
* <p>Calling {@link RouteHandle#with} is optional when no extra middleware is needed —
* the route is registered automatically before the next operation or at {@link #start()}.
*
* <p>Middleware execution order: injected (from annotations) → explicit (.with()) → handler.
*/
public RouteHandle<FlashApp> register(RequestHandler handler) {
return pending(new RouteHandle<>(this, explicit -> {
List<Middleware> injected = ctx.processors().stream()
.flatMap(p -> p.process(handler.getClass()).stream())
.toList();
Middleware[] all = injected.isEmpty()
? explicit
: Stream.concat(injected.stream(), Arrays.stream(explicit))
.toArray(Middleware[]::new);
server.doRegister(handler, all);
}));
}
// ── Lambda route registration ─────────────────────────────────────────────
/**
* Begins registration of a lambda route. Calling {@link RouteHandle#with} is
* optional when no middleware is needed — the route is registered automatically
* before the next operation or at {@link #start()}.
*
* <pre>{@code
* app.get("/ping", (req, res) -> "pong"); // no middleware
* app.get("/me", (req, res) -> ClaimsHolder.user().email())
* .with(oidc.protect()); // with middleware
* }</pre>
*/
public RouteHandle<FlashApp> get (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.GET, path, h, m))); }
public RouteHandle<FlashApp> post (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.POST, path, h, m))); }
public RouteHandle<FlashApp> put (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.PUT, path, h, m))); }
public RouteHandle<FlashApp> delete (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.DELETE, path, h, m))); }
public RouteHandle<FlashApp> patch (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.PATCH, path, h, m))); }
public RouteHandle<FlashApp> options(String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.OPTIONS, path, h, m))); }
public RouteHandle<FlashApp> head (String path, SimpleHandler.FunctionalHandler h) { return pending(new RouteHandle<>(this, m -> server.doRegister(HttpMethod.HEAD, path, h, m))); }
public FlashApp mount(String namespace, AbstractRouter router) {
flushPending();
server.mount(namespace, router);
return this;
}
public FlashApp onException(AbstractRouter.ExceptionHandler handler) {
flushPending();
server.onException(handler);
return this;
}
public FlashApp onNotFound(SimpleHandler.FunctionalHandler handler) {
flushPending();
server.onNotFound(handler);
return this;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
public CompletableFuture<Void> start() {
flushPending();
return server.start();
}
public CompletableFuture<Void> stop() {
return server.stop();
}
/** Direct access to the underlying server for advanced use cases. */
public HttpServer server() {
return server;
}
}
@@ -0,0 +1,25 @@
package dev.relism.extension;
/**
* Contract for all Flash extensions. An extension receives the full {@link FlashApp}
* so it can both register handlers on the server and expose services through
* {@link ExtensionContext} for other extensions.
*
* <pre>{@code
* public class OpenApiExtension implements FlashExtension {
* public void install(FlashApp app, ExtensionContext ctx) {
* ObjectMapper mapper = ctx.require(ObjectMapper.class);
* app.get("/openapi.json", (req, res) -> mapper.writeValueAsString(spec));
* }
* }
*
* FlashApp.of(new HttpServer(config))
* .install(new JacksonExtension())
* .install(new OpenApiExtension("/openapi.json"))
* .install(new OidcExtension(OidcConfig.fromEnv()));
* }</pre>
*/
@FunctionalInterface
public interface FlashExtension {
void install(FlashApp app, ExtensionContext ctx);
}
@@ -0,0 +1,106 @@
package dev.relism.models;
import dev.relism.exceptions.HttpException;
/**
* Typed accessors for request parameters. All methods throw
* {@link HttpException} (400) on missing or malformed input, so handlers
* do not need to write per-field validation boilerplate.
*
* <pre>{@code
* int page = RequestHelper.queryInt(req, "page", 1, 100);
* long id = RequestHelper.paramLong(req, "id");
* String q = RequestHelper.queryRequired(req, "q");
* }</pre>
*/
public final class RequestHelper {
private RequestHelper() {}
// ── Query parameters ──────────────────────────────────────────────────────
/**
* Returns the query param as an int, or {@code defaultValue} if absent.
* Caps the result at {@code max}. Throws 400 on non-integer input.
*/
public static int queryInt(Request req, String name, int defaultValue, int max) {
String v = req.query(name);
if (v == null || v.isEmpty()) return defaultValue;
try {
return Math.min(Integer.parseInt(v), max);
} catch (NumberFormatException e) {
throw HttpException.badRequest("Invalid integer for query param '" + name + "': " + v);
}
}
/**
* Returns the query param value. Throws 400 if absent or blank.
*/
public static String queryRequired(Request req, String name) {
String v = req.query(name);
if (v == null || v.isBlank())
throw HttpException.badRequest("Missing required query param: " + name);
return v;
}
/**
* Returns the query param, or {@code null} if absent.
*/
public static String queryOptional(Request req, String name) {
return req.query(name);
}
/**
* Returns the query param as a long, or {@code defaultValue} if absent.
* Throws 400 on non-long input.
*/
public static long queryLong(Request req, String name, long defaultValue) {
String v = req.query(name);
if (v == null || v.isEmpty()) return defaultValue;
try {
return Long.parseLong(v);
} catch (NumberFormatException e) {
throw HttpException.badRequest("Invalid long for query param '" + name + "': " + v);
}
}
// ── Path parameters ───────────────────────────────────────────────────────
/**
* Returns the path parameter as a long. Throws 400 if absent or non-long.
*/
public static long paramLong(Request req, String name) {
String v = req.param(name);
if (v == null)
throw HttpException.badRequest("Missing path param: " + name);
try {
return Long.parseLong(v);
} catch (NumberFormatException e) {
throw HttpException.badRequest("Invalid long for path param '" + name + "': " + v);
}
}
/**
* Returns the path parameter as an int. Throws 400 if absent or non-int.
*/
public static int paramInt(Request req, String name) {
String v = req.param(name);
if (v == null)
throw HttpException.badRequest("Missing path param: " + name);
try {
return Integer.parseInt(v);
} catch (NumberFormatException e) {
throw HttpException.badRequest("Invalid int for path param '" + name + "': " + v);
}
}
/**
* Returns the path parameter as a String. Throws 400 if absent.
*/
public static String paramRequired(Request req, String name) {
String v = req.param(name);
if (v == null)
throw HttpException.badRequest("Missing path param: " + name);
return v;
}
}
@@ -90,8 +90,6 @@ public abstract class AbstractRouter {
return compiled;
}
private static final Middleware[] NO_MIDDLEWARES = new Middleware[0];
// ── Error handler configuration ───────────────────────────────────────────
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
@@ -104,38 +102,23 @@ public abstract class AbstractRouter {
return this;
}
// ── Lambda handler registration ───────────────────────────────────────────
// ── Internal registration (package-private) ───────────────────────────────
/**
* Registers a lambda handler with optional handler-level middlewares.
* Router-level middlewares are applied automatically on top.
* Registers a lambda handler immediately with a pre-built middleware array.
* Called by {@link RouteHandle#with} and by {@link dev.relism.extension.FlashApp}.
*/
public AbstractRouter register(HttpMethod method, String path,
SimpleHandler.FunctionalHandler handler,
Middleware... middlewares) {
public AbstractRouter doRegister(HttpMethod method, String path,
SimpleHandler.FunctionalHandler handler, Middleware[] middlewares) {
return addRoute(method, PathUtils.sanitize(path),
compile(new SimpleHandler(handler), middlewares));
}
public AbstractRouter get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.GET, path, h, m); }
public AbstractRouter post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.POST, path, h, m); }
public AbstractRouter put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PUT, path, h, m); }
public AbstractRouter delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.DELETE, path, h, m); }
public AbstractRouter patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PATCH, path, h, m); }
public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.OPTIONS, path, h, m); }
public AbstractRouter head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.HEAD, path, h, m); }
public AbstractRouter trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.TRACE, path, h, m); }
public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.CONNECT, path, h, m); }
public AbstractRouter purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PURGE, path, h, m); }
// ── Class-based handler registration ─────────────────────────────────────
/**
* Registers a class-based handler with optional handler-level middlewares.
* The class must carry a {@link Route @Route} annotation declaring method and path.
* Router-level middlewares are applied automatically on top.
* Registers a class-based handler immediately with a pre-built middleware array.
* Called by {@link RouteHandle#with} and by {@link dev.relism.extension.FlashApp}.
*/
public AbstractRouter register(RequestHandler handler, Middleware... middlewares) {
public AbstractRouter doRegister(RequestHandler handler, Middleware[] middlewares) {
Route annotation = handler.getClass().getAnnotation(Route.class);
if (annotation != null)
addRoute(annotation.method(), annotation.path(),
@@ -143,6 +126,44 @@ public abstract class AbstractRouter {
return this;
}
// ── Lambda handler registration ───────────────────────────────────────────
/**
* Begins registration of a lambda handler. Call {@link RouteHandle#with} on the
* returned handle to supply middlewares (if any) and complete the registration.
*
* <pre>{@code
* router.get("/ping", (req, res) -> "pong").with();
* router.get("/admin", adminHandler).with(auth, logging);
* }</pre>
*/
public RouteHandle<AbstractRouter> get (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.GET, path, h, m)); }
public RouteHandle<AbstractRouter> post (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.POST, path, h, m)); }
public RouteHandle<AbstractRouter> put (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.PUT, path, h, m)); }
public RouteHandle<AbstractRouter> delete (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.DELETE, path, h, m)); }
public RouteHandle<AbstractRouter> patch (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.PATCH, path, h, m)); }
public RouteHandle<AbstractRouter> options(String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.OPTIONS, path, h, m)); }
public RouteHandle<AbstractRouter> head (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.HEAD, path, h, m)); }
public RouteHandle<AbstractRouter> trace (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.TRACE, path, h, m)); }
public RouteHandle<AbstractRouter> connect(String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.CONNECT, path, h, m)); }
public RouteHandle<AbstractRouter> purge (String path, SimpleHandler.FunctionalHandler h) { return new RouteHandle<>(this, m -> doRegister(HttpMethod.PURGE, path, h, m)); }
// ── Class-based handler registration ─────────────────────────────────────
/**
* Begins registration of a class-based handler. The class must carry a
* {@link Route @Route} annotation. Call {@link RouteHandle#with} to supply
* optional extra middlewares and complete the registration.
*
* <pre>{@code
* router.register(new BlogHandler()).with();
* router.register(new AdminHandler()).with(logging);
* }</pre>
*/
public RouteHandle<AbstractRouter> register(RequestHandler handler) {
return new RouteHandle<>(this, m -> doRegister(handler, m));
}
// ── Routing ───────────────────────────────────────────────────────────────
public abstract RequestHandler route(Request request);
@@ -0,0 +1,70 @@
package dev.relism.routing;
import java.util.function.Consumer;
/**
* Deferred route registration handle returned by {@code router.get()},
* {@code router.post()}, {@code app.get()}, {@code app.register()}, etc.
*
* <p>The route is <em>not</em> registered until {@link #with} is called.
* This separates the handler declaration from the middleware declaration,
* keeping the registration methods clean.
*
* <pre>{@code
* // No middleware
* app.get("/ping", (req, res) -> "pong").with();
*
* // With middleware
* app.get("/me", (req, res) -> ClaimsHolder.user().email()).with(oidc.protect());
* app.get("/admin", (req, res) -> "admin").with(oidc.requireRole("admin"));
*
* // Class-based handler — annotations (@Authenticated, @RolesAllowed) are
* // processed automatically; .with() can add extra middleware on top.
* app.register(new MyHandler()).with();
* app.register(new SecuredHandler()).with(logging);
* }</pre>
*
* @param <P> the parent type returned by {@link #with} for further chaining
* ({@link dev.relism.extension.FlashApp} or {@link AbstractRouter})
*/
public final class RouteHandle<P> {
private final P parent;
private final Consumer<Middleware[]> registerFn;
private boolean registered = false;
public RouteHandle(P parent, Consumer<Middleware[]> registerFn) {
this.parent = parent;
this.registerFn = registerFn;
}
/**
* Registers the route with the given middlewares and returns the parent
* for further chaining.
*
* <p>Calling without arguments is equivalent to registering with no middleware.
* When using {@link dev.relism.extension.FlashApp}, calling {@code .with()} is
* optional for routes that need no middleware — the route is registered
* automatically before the next operation or at {@code start()}.
*
* @param middlewares zero or more middlewares; first element executes outermost
* @return the parent ({@link dev.relism.extension.FlashApp} or
* {@link AbstractRouter}) for further method chaining
*/
public P with(Middleware... middlewares) {
if (!registered) {
registerFn.accept(middlewares);
registered = true;
}
return parent;
}
/**
* Registers the route with no middleware if it has not been registered yet.
* Called automatically by {@link dev.relism.extension.FlashApp} before each
* new route registration and at {@code start()}.
*/
public void ensureRegistered() {
with();
}
}