diff --git a/flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/Check.java b/flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/Check.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/Check.java rename to flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/Check.java diff --git a/flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/ValidationException.java b/flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/ValidationException.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/ValidationException.java rename to flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/ValidationException.java diff --git a/flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/Validator.java b/flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/Validator.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/main/java/dev/relism/flash/ext/validation/Validator.java rename to flash-extensions/flash-ext-jackson-core/src/main/java/dev/relism/flash/ext/jackson/Validator.java diff --git a/flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidatorTest.java b/flash-extensions/flash-ext-jackson-core/src/test/java/dev/relism/flash/ext/jackson/ValidatorTest.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidatorTest.java rename to flash-extensions/flash-ext-jackson-core/src/test/java/dev/relism/flash/ext/jackson/ValidatorTest.java diff --git a/flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidationOpenApiInteropTest.java b/flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/ConstraintsInTheDocumentTest.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidationOpenApiInteropTest.java rename to flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/ConstraintsInTheDocumentTest.java diff --git a/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonExtensionTest.java b/flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/JsonExtensionTest.java similarity index 100% rename from flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonExtensionTest.java rename to flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/JsonExtensionTest.java diff --git a/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JsonTest.java b/flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/JsonTest.java similarity index 100% rename from flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JsonTest.java rename to flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/JsonTest.java diff --git a/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonMiddlewareTest.java b/flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/MarshallingTest.java similarity index 100% rename from flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonMiddlewareTest.java rename to flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/MarshallingTest.java diff --git a/flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidationRoutesTest.java b/flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/ValidatedBodyTest.java similarity index 100% rename from flash-extensions/flash-ext-validation/src/test/java/dev/relism/flash/ext/validation/ValidationRoutesTest.java rename to flash-extensions/flash-ext-jackson-json/src/test/java/dev/relism/flash/ext/jackson/json/ValidatedBodyTest.java diff --git a/flash/src/main/java/dev/relism/flash/extension/Inject.java b/flash/src/main/java/dev/relism/flash/extension/Inject.java new file mode 100644 index 0000000..d1ce903 --- /dev/null +++ b/flash/src/main/java/dev/relism/flash/extension/Inject.java @@ -0,0 +1,36 @@ +package dev.relism.flash.extension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A service this handler needs, filled in once when the handler is bound. + * + *
{@code
+ * public final class ListUsers extends RequestHandler {
+ * @Inject private UserService users;
+ *
+ * @Override public Object handle(Request req, Response res) { return users.findAll(); }
+ * }
+ * }
+ *
+ * This is how a handler takes a service. {@code onInit} stays for what has to be computed at + * boot, or for a service that may not be there ({@code find}). + * + *
The type is the class's own type argument, so it is written once, in the signature: + * + *
{@code
+ * @POST("/users")
+ * public final class CreateUser extends JsonHandler {
+ * @Override protected Object handle(Request req, Response res, NewUser body) {
+ * return users.create(body);
+ * }
+ * }
+ * }
+ *
+ * Reading the body is the format's job: a subclass such as {@code JsonHandler} implements + * {@link #body} and declares its media type with + * {@link dev.relism.flash.routing.Consumes @Consumes}. Documentation tools read both off the + * class, which is what lets a request body be described without saying its type a second time. + * + * @param the body type + */ +public abstract class BodyHandler extends RequestHandler { + + /** + * The body type a handler class declares, or {@code null} when it leaves it open. + * + *
Resolved through the whole chain, so an intermediate base class that passes its own type
+ * argument along answers with the type its subclass fixed.
+ */
+ public static Class> bodyTypeOf(Class> handlerClass) {
+ Map Declared once on a handler base class — a JSON one, an XML one — and inherited by every
+ * handler written against it, so nothing has to repeat it. Tooling reads it; the router does not.
+ */
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Consumes {
+ ContentType value();
+}
diff --git a/flash/src/test/java/dev/relism/flash/models/BodyHandlerTest.java b/flash/src/test/java/dev/relism/flash/models/BodyHandlerTest.java
new file mode 100644
index 0000000..02bb11c
--- /dev/null
+++ b/flash/src/test/java/dev/relism/flash/models/BodyHandlerTest.java
@@ -0,0 +1,109 @@
+package dev.relism.flash.models;
+
+import dev.relism.flash.http.ContentType;
+import dev.relism.flash.http.HttpMethod;
+import dev.relism.flash.routing.routers.fastpathrouter.FastPathViews;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class BodyHandlerTest {
+
+ record Payload(String text) {}
+
+ static abstract class TextHandler extends BodyHandler {
+ @Override protected B body(Request request) {
+ return read(new String(request.body().bytes(), StandardCharsets.UTF_8));
+ }
+ protected abstract B read(String text);
+ }
+
+ static final class Echo extends TextHandlerService access
- * Override {@link #onInit()} to cache services from the {@link FlashContext}
- * into private fields. This keeps the hot-path ({@code handle}) free of map lookups.
+ * Annotate a field with {@link Inject} and it is filled at boot, or override {@link #onInit()}
+ * to cache services from the {@link FlashContext} by hand. Either way the hot path
+ * ({@code handle}) reads a field and never the context.
*
* {@code
* @GET("/users")
@@ -53,9 +57,41 @@ public abstract class RequestHandler {
*/
public final void bind(FlashContext ctx) {
this.ctx = ctx;
+ inject();
onInit();
}
+ /** Fills every {@link Inject} field, this class's and its bases', before {@link #onInit}. */
+ private void inject() {
+ for (Class> type = getClass(); type != null && type != RequestHandler.class; type = type.getSuperclass()) {
+ for (Field field : type.getDeclaredFields()) {
+ if (!field.isAnnotationPresent(Inject.class)) continue;
+ String where = type.getSimpleName() + "." + field.getName();
+
+ // A static field would be shared by every handler, and a final one may already have
+ // been folded into the code that reads it. Both are refused rather than surprising.
+ if (Modifier.isStatic(field.getModifiers()))
+ throw new IllegalStateException(where + " is static: an injected field belongs to the handler");
+ if (Modifier.isFinal(field.getModifiers()))
+ throw new IllegalStateException(where + " is final: an injected field is written after construction");
+
+ Object service;
+ try {
+ service = ctx.require(field.getType());
+ } catch (RuntimeException missing) {
+ throw new IllegalStateException(where + " asks for " + field.getType().getSimpleName()
+ + ", which nothing provides", missing);
+ }
+ try {
+ field.setAccessible(true);
+ field.set(this, service);
+ } catch (ReflectiveOperationException | RuntimeException unreachable) {
+ throw new IllegalStateException("Could not write " + where, unreachable);
+ }
+ }
+ }
+ }
+
/**
* Override to cache services at boot time. Called once after {@link #bind},
* before any request reaches this handler.
diff --git a/flash/src/main/java/dev/relism/flash/routing/Consumes.java b/flash/src/main/java/dev/relism/flash/routing/Consumes.java
new file mode 100644
index 0000000..a281b41
--- /dev/null
+++ b/flash/src/main/java/dev/relism/flash/routing/Consumes.java
@@ -0,0 +1,22 @@
+package dev.relism.flash.routing;
+
+import dev.relism.flash.http.ContentType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The media type a handler reads its request body as.
+ *
+ *