package dev.relism.extension; import dev.relism.http.HttpMethod; import dev.relism.models.RequestHandler; import dev.relism.models.SimpleHandler; import dev.relism.routing.AbstractRouter; import dev.relism.routing.PathUtils; import dev.relism.routing.Route; import dev.relism.routing.RouteHandle; import dev.relism.routing.Middleware; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Stream; /** * Scoped registration context for a mounted sub-router namespace. * *

Obtained via {@link FlashApp#mount(String, java.util.function.Consumer)}. * A scope has its own child {@link ExtensionContext} that inherits all services and * annotation processors from the parent app, so extensions like {@code @Authenticated} * and {@code @RolesAllowed} work identically inside a scope. * *

Extensions installed on a scope are scoped to that namespace and not visible * in the parent or sibling scopes. * *

{@code
 * app.mount("/api", scope -> {
 *     scope.install(new RateLimitExtension());
 *     scope.register(new UserHandler());   // @Authenticated auto-injected
 *     scope.get("/health", (req, res) -> "ok");
 *     scope.scan("dev.example.api.handlers");
 * });
 * }
*/ public final class FlashScope implements FlashRegistrar { private final AbstractRouter router; private final String namespace; private final ExtensionContext ctx; /** Pending RouteHandle awaiting .with() — auto-flushed before each new registration. */ private RouteHandle pending; /** * Package-private — only {@link FlashApp} creates scopes. * * @param router the sub-router that will receive routes registered on this scope * @param namespace the namespace prefix (e.g. {@code "/api"}) * @param parentCtx the parent app's ExtensionContext — a child is created from it */ FlashScope(AbstractRouter router, String namespace, ExtensionContext parentCtx) { this.router = router; this.namespace = namespace; this.ctx = parentCtx.child(); } // ── Pending flush ───────────────────────────────────────────────────────── private void flushPending() { if (pending != null) { pending.ensureRegistered(); pending = null; } } private

RouteHandle

track(RouteHandle

handle) { flushPending(); pending = handle; return handle; } // ── FlashRegistrar — extension installation ─────────────────────────────── /** * Installs an extension scoped to this namespace. * The extension registers routes and services on this scope only. */ @Override public FlashScope install(FlashExtension ext) { flushPending(); ext.install(this, ctx); return this; } // ── FlashRegistrar — route registration ─────────────────────────────────── @Override public RouteHandle get (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.GET, path, h); } @Override public RouteHandle post (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.POST, path, h); } @Override public RouteHandle put (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PUT, path, h); } @Override public RouteHandle delete (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.DELETE, path, h); } @Override public RouteHandle patch (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PATCH, path, h); } @Override public RouteHandle options(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.OPTIONS, path, h); } @Override public RouteHandle head (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.HEAD, path, h); } @Override public RouteHandle trace (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.TRACE, path, h); } @Override public RouteHandle connect(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.CONNECT, path, h); } @Override public RouteHandle purge (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PURGE, path, h); } private RouteHandle routeLambda(HttpMethod method, String path, SimpleHandler.FunctionalHandler h) { return track(new RouteHandle<>(this, m -> { String full = ns(path); emit(method, full, null, List.of(), m); router.doRegister(method, full, h, m); })); } /** * Begins registration of a class-based handler. Annotation processors from the * parent app and any installed on this scope are applied. The {@link Route @Route} * path is prepended with this scope's namespace automatically. * *

Calling {@link RouteHandle#with} is optional — the route is registered * automatically before the next operation or when the scope consumer returns. */ @Override public RouteHandle register(RequestHandler handler) { return track(new RouteHandle<>(this, explicit -> { List 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); Route annotation = handler.getClass().getAnnotation(Route.class); if (annotation != null) { String full = ns(annotation.path()); emit(annotation.method(), full, handler.getClass(), injected, explicit); router.doRegister(annotation.method(), full, (RequestHandler) handler, all); } })); } /** * Scans {@code packageName} for {@link RequestHandler} subclasses annotated with * {@link Route @Route}. Each is instantiated via its no-arg constructor and registered * with this scope's namespace prefix and annotation processors applied. */ @Override public FlashScope scan(String packageName) { flushPending(); PackageScanner.findHandlers(packageName).forEach(cls -> register(instantiate(cls)).ensureRegistered()); return this; } @Override public FlashScope onException(AbstractRouter.ExceptionHandler h) { flushPending(); router.onException(h); return this; } @Override public FlashScope onNotFound(SimpleHandler.FunctionalHandler h) { flushPending(); router.onNotFound(h); return this; } @Override public ExtensionContext ctx() { return ctx; } // ── Internals ───────────────────────────────────────────────────────────── /** Ensures any pending route is registered when the scope consumer returns. */ void flush() { flushPending(); } /** Returns the sub-router for GlobalRouter to mount. */ AbstractRouter router() { return router; } /** Prepends this scope's namespace to the given path. */ private String ns(String path) { return namespace + PathUtils.sanitize(path); } @SuppressWarnings("unchecked") private static RequestHandler instantiate(Class cls) { try { return (RequestHandler) cls.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to instantiate handler: " + cls.getName() + " — ensure it has a public no-arg constructor", e); } } /** * Emits a {@link RouteEvent} to all {@link RouteListener}s visible from this scope's context. * Parent-level listeners (registered on the app) are included via context inheritance. * No-op if no listener has been registered. Never called on the request hot-path. */ @SuppressWarnings("unchecked") private void emit(HttpMethod method, String path, Class handlerClass, List injected, Middleware[] explicit) { List listeners = ctx.routeListeners(); if (listeners.isEmpty()) return; Middleware[] routerMws = router.routerMiddlewares(); List> chain = new ArrayList<>(routerMws.length + injected.size() + explicit.length); for (Middleware m : routerMws) chain.add((Class) m.getClass()); for (Middleware m : injected) chain.add((Class) m.getClass()); for (Middleware m : explicit) chain.add((Class) m.getClass()); RouteEvent event = new RouteEvent(method, path, namespace, router.getClass().getSimpleName(), handlerClass, List.copyOf(chain)); listeners.forEach(l -> l.onRoute(event)); } }