diff --git a/src/main/java/com/retailsvc/http/AfterResponseHook.java b/src/main/java/com/retailsvc/http/AfterResponseHook.java
index e08b35b..fd4b790 100644
--- a/src/main/java/com/retailsvc/http/AfterResponseHook.java
+++ b/src/main/java/com/retailsvc/http/AfterResponseHook.java
@@ -18,5 +18,18 @@
@FunctionalInterface
public interface AfterResponseHook {
+ /**
+ * Invoked after the response has been written to the client.
+ *
+ *
Any exception thrown by this method is logged at DEBUG and swallowed; it does not affect the
+ * response (already sent) and does not prevent subsequent hooks from running.
+ *
+ * @param request the resolved {@link Request} that was handled; routing and parameter/body
+ * validation have already passed by the time this is called
+ * @param response the {@link Response} that was written to the client. {@link Response#status()}
+ * and {@link Response#headers()} reflect what was sent on the wire; {@link Response#body()}
+ * may be {@code null} on streaming responses (and is always {@code null} on the error path,
+ * since the body bytes have already been emitted)
+ */
void after(Request request, Response response);
}
diff --git a/src/main/java/com/retailsvc/http/BadRequestException.java b/src/main/java/com/retailsvc/http/BadRequestException.java
index 4abaa69..2cbd8fd 100644
--- a/src/main/java/com/retailsvc/http/BadRequestException.java
+++ b/src/main/java/com/retailsvc/http/BadRequestException.java
@@ -16,18 +16,58 @@ public final class BadRequestException extends RuntimeException {
private static final int DEFAULT_STATUS = 400;
+ /** HTTP 4xx status code carried by this exception. */
private final int status;
+
+ /** JSON Pointer (RFC 6901) to the offending property; may be {@code null}. */
private final String pointer;
+
+ /** Validation keyword that failed (for example {@code "required"}); may be {@code null}. */
private final String keyword;
+ /**
+ * Creates a new exception with the default HTTP status {@code 400 Bad Request}.
+ *
+ *
Equivalent to {@link #BadRequestException(int, String, String, String)} invoked with {@code
+ * status = 400} and no pointer or keyword.
+ *
+ * @param detail human-readable explanation of the error; surfaced as the RFC 7807 {@code detail}
+ * member
+ */
public BadRequestException(String detail) {
this(DEFAULT_STATUS, detail, null, null);
}
+ /**
+ * Creates a new exception with an explicit 4xx HTTP status.
+ *
+ *
Equivalent to {@link #BadRequestException(int, String, String, String)} with no pointer or
+ * keyword.
+ *
+ * @param status the HTTP status code; must be in the range {@code 400}-{@code 499} or an {@link
+ * IllegalArgumentException} is thrown
+ * @param detail human-readable explanation of the error; surfaced as the RFC 7807 {@code detail}
+ * member
+ */
public BadRequestException(int status, String detail) {
this(status, detail, null, null);
}
+ /**
+ * Creates a new exception with the full set of RFC 7807 problem fields. Intended for
+ * validation-style errors where the offending property can be pinpointed with a JSON Pointer and
+ * the failing rule identified by a JSON Schema keyword.
+ *
+ * @param status the HTTP status code; must be in the range {@code 400}-{@code 499}
+ * @param detail human-readable explanation of the error; surfaced as the RFC 7807 {@code detail}
+ * member
+ * @param pointer JSON Pointer (RFC 6901) to the offending property in the request payload; may be
+ * {@code null} when not applicable
+ * @param keyword JSON Schema / validation keyword that failed (for example {@code "required"},
+ * {@code "pattern"}, {@code "maxLength"}); may be {@code null} when not applicable
+ * @throws NullPointerException if {@code detail} is {@code null}
+ * @throws IllegalArgumentException if {@code status} is not in the range {@code 400}-{@code 499}
+ */
public BadRequestException(int status, String detail, String pointer, String keyword) {
super(Objects.requireNonNull(detail, "detail must not be null"));
if (status < 400 || status > 499) {
@@ -38,14 +78,32 @@ public BadRequestException(int status, String detail, String pointer, String key
this.keyword = keyword;
}
+ /**
+ * Returns the HTTP status code carried by this exception.
+ *
+ * @return the HTTP status code; always a 4xx value in the range {@code 400}-{@code 499}
+ */
public int status() {
return status;
}
+ /**
+ * Returns the JSON Pointer locating the offending property in the request payload.
+ *
+ * @return an {@link Optional} containing the JSON Pointer (RFC 6901) to the offending property,
+ * or {@link Optional#empty()} when no pointer was supplied
+ */
public Optional pointer() {
return Optional.ofNullable(pointer);
}
+ /**
+ * Returns the JSON Schema / validation keyword that failed.
+ *
+ * @return an {@link Optional} containing the failing JSON Schema or validation keyword (for
+ * example {@code "required"}, {@code "pattern"}), or {@link Optional#empty()} when no keyword
+ * was supplied
+ */
public Optional keyword() {
return Optional.ofNullable(keyword);
}
diff --git a/src/main/java/com/retailsvc/http/Credential.java b/src/main/java/com/retailsvc/http/Credential.java
index c3b1134..5c5ca99 100644
--- a/src/main/java/com/retailsvc/http/Credential.java
+++ b/src/main/java/com/retailsvc/http/Credential.java
@@ -7,9 +7,25 @@
public sealed interface Credential
permits Credential.ApiKeyCredential, Credential.BearerCredential, Credential.BasicCredential {
+ /**
+ * Credential extracted from an OpenAPI {@code apiKey} security scheme (header, query, or cookie).
+ *
+ * @param value raw key value as presented by the client.
+ */
record ApiKeyCredential(String value) implements Credential {}
+ /**
+ * Credential extracted from an {@code http} security scheme with {@code bearer} style.
+ *
+ * @param token raw bearer token (no {@code Bearer } prefix).
+ */
record BearerCredential(String token) implements Credential {}
+ /**
+ * Credential extracted from an {@code http} security scheme with {@code basic} style.
+ *
+ * @param username decoded username portion.
+ * @param password decoded password portion.
+ */
record BasicCredential(String username, String password) implements Credential {}
}
diff --git a/src/main/java/com/retailsvc/http/Dependency.java b/src/main/java/com/retailsvc/http/Dependency.java
index 54458f3..9d82f80 100644
--- a/src/main/java/com/retailsvc/http/Dependency.java
+++ b/src/main/java/com/retailsvc/http/Dependency.java
@@ -12,6 +12,11 @@
* @param up whether the dependency is healthy
*/
public record Dependency(String id, boolean up) {
+ /**
+ * Canonical constructor that validates {@code id} is non-null.
+ *
+ * @throws NullPointerException if {@code id} is {@code null}
+ */
public Dependency {
Objects.requireNonNull(id, "id");
}
diff --git a/src/main/java/com/retailsvc/http/ExceptionHandler.java b/src/main/java/com/retailsvc/http/ExceptionHandler.java
index 02f42dc..f911f2d 100644
--- a/src/main/java/com/retailsvc/http/ExceptionHandler.java
+++ b/src/main/java/com/retailsvc/http/ExceptionHandler.java
@@ -10,5 +10,13 @@
@FunctionalInterface
public interface ExceptionHandler {
+ /**
+ * Maps the throwable to the {@link Response} to render.
+ *
+ * @param t the exception thrown anywhere in the request pipeline (never null)
+ * @return the response to write; library default maps {@code BadRequestException} to 4xx
+ * problem+json and anything else to 500 problem+json with the exception message redacted by
+ * default
+ */
Response handle(Throwable t);
}
diff --git a/src/main/java/com/retailsvc/http/GsonTypeMapper.java b/src/main/java/com/retailsvc/http/GsonTypeMapper.java
index fff3bac..36023bd 100644
--- a/src/main/java/com/retailsvc/http/GsonTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/GsonTypeMapper.java
@@ -37,6 +37,7 @@ public GsonTypeMapper() {
/**
* Creates a mapper backed by the supplied {@link Gson}.
*
+ * @param gson the {@link Gson} instance to delegate to
* @throws NullPointerException if {@code gson} is null
*/
public GsonTypeMapper(Gson gson) {
@@ -47,6 +48,8 @@ public GsonTypeMapper(Gson gson) {
* Returns a {@link GsonBuilder} pre-populated with the wrapped {@link Gson}'s configuration, so
* callers can derive a customized {@link Gson} from the library default (or from their own
* starting point).
+ *
+ * @return a new {@link GsonBuilder} seeded with the wrapped {@link Gson}'s configuration
*/
public GsonBuilder gsonBuilder() {
return delegate.gson().newBuilder();
diff --git a/src/main/java/com/retailsvc/http/Handlers.java b/src/main/java/com/retailsvc/http/Handlers.java
index 8ca9abd..7334130 100644
--- a/src/main/java/com/retailsvc/http/Handlers.java
+++ b/src/main/java/com/retailsvc/http/Handlers.java
@@ -21,12 +21,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/** Built-in {@link RequestHandler} and {@link ExceptionHandler} factories. */
public final class Handlers {
private static final Logger LOG = LoggerFactory.getLogger(Handlers.class);
private Handlers() {}
+ /**
+ * Default {@link ExceptionHandler} that maps common library exceptions to RFC 7807 problem
+ * responses and falls back to a 500 for anything unrecognised.
+ *
+ * @param jsonMapper mapper used to serialise problem detail bodies
+ * @return the exception handler
+ */
public static ExceptionHandler defaultExceptionHandler(TypeMapper jsonMapper) {
Objects.requireNonNull(jsonMapper, "jsonMapper must not be null");
return t ->
@@ -54,7 +62,12 @@ public static ExceptionHandler defaultExceptionHandler(TypeMapper jsonMapper) {
};
}
- /** Returns 204 No Content on GET/HEAD; 405 with {@code Allow: GET, HEAD} otherwise. */
+ /**
+ * Liveness handler that returns 204 No Content on GET/HEAD; 405 with {@code Allow: GET, HEAD}
+ * otherwise.
+ *
+ * @return the liveness handler
+ */
public static RequestHandler aliveHandler() {
return req ->
switch (req.method()) {
@@ -79,6 +92,7 @@ public static RequestHandler aliveHandler() {
* The body is rendered by a built-in writer; no JSON library on the classpath is required.
*
* @param probe supplier of the current {@link HealthOutcome}
+ * @return the health handler
*/
public static RequestHandler healthHandler(Supplier probe) {
Objects.requireNonNull(probe, "probe");
@@ -110,6 +124,7 @@ public static RequestHandler healthHandler(Supplier probe) {
* request — the handler owns the stream lifecycle.
*
* @param classpathResource absolute classpath path, e.g. {@code /schemas/v1/openapi.yaml}
+ * @return the spec handler
*/
public static RequestHandler resourceHandler(String classpathResource) {
return resourceHandler(ResourceSource.ofClasspath(classpathResource));
@@ -119,6 +134,9 @@ public static RequestHandler resourceHandler(String classpathResource) {
* Serves a filesystem file as a streaming response. Content-Type is inferred from the file
* extension. Existence and length are resolved at construction; a missing file fails immediately
* with {@link IllegalArgumentException}. The file is opened and closed per request.
+ *
+ * @param file path to the file to serve
+ * @return a handler that streams {@code file} on {@code GET}
*/
public static RequestHandler resourceHandler(Path file) {
return resourceHandler(ResourceSource.ofFile(file));
diff --git a/src/main/java/com/retailsvc/http/HealthOutcome.java b/src/main/java/com/retailsvc/http/HealthOutcome.java
index 9db0c05..951af7f 100644
--- a/src/main/java/com/retailsvc/http/HealthOutcome.java
+++ b/src/main/java/com/retailsvc/http/HealthOutcome.java
@@ -14,11 +14,16 @@
*/
public record HealthOutcome(List dependencies) {
+ /** Canonical constructor; normalises a {@code null} dependency list to an empty list. */
public HealthOutcome {
dependencies = List.copyOf(Objects.requireNonNullElse(dependencies, List.of()));
}
- /** {@code true} when every dependency is up (vacuously true for an empty list). */
+ /**
+ * Reports the aggregate health derived from {@link #dependencies()}.
+ *
+ * @return {@code true} when every dependency is up (vacuously true for an empty list)
+ */
public boolean up() {
return dependencies.stream().allMatch(Dependency::up);
}
diff --git a/src/main/java/com/retailsvc/http/Jackson2JsonTypeMapper.java b/src/main/java/com/retailsvc/http/Jackson2JsonTypeMapper.java
index 38e9ed1..2081615 100644
--- a/src/main/java/com/retailsvc/http/Jackson2JsonTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/Jackson2JsonTypeMapper.java
@@ -31,6 +31,11 @@ public final class Jackson2JsonTypeMapper implements TypedTypeMapper {
private final ObjectMapper mapper;
+ /**
+ * Creates a new mapper backed by the given Jackson {@link ObjectMapper}.
+ *
+ * @param mapper the fully-configured Jackson mapper to delegate to; must not be {@code null}
+ */
public Jackson2JsonTypeMapper(ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper, "mapper must not be null");
}
diff --git a/src/main/java/com/retailsvc/http/Jackson3JsonTypeMapper.java b/src/main/java/com/retailsvc/http/Jackson3JsonTypeMapper.java
index 57f232b..741a7df 100644
--- a/src/main/java/com/retailsvc/http/Jackson3JsonTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/Jackson3JsonTypeMapper.java
@@ -37,6 +37,11 @@ public final class Jackson3JsonTypeMapper implements TypedTypeMapper {
private final ObjectMapper mapper;
+ /**
+ * Creates a new mapper backed by the given Jackson 3 {@link ObjectMapper}.
+ *
+ * @param mapper the fully-configured Jackson 3 mapper to delegate to; must not be {@code null}
+ */
public Jackson3JsonTypeMapper(ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper, "mapper must not be null");
}
diff --git a/src/main/java/com/retailsvc/http/MethodNotAllowedException.java b/src/main/java/com/retailsvc/http/MethodNotAllowedException.java
index bc757fd..0371faf 100644
--- a/src/main/java/com/retailsvc/http/MethodNotAllowedException.java
+++ b/src/main/java/com/retailsvc/http/MethodNotAllowedException.java
@@ -3,14 +3,26 @@
import com.retailsvc.http.spec.HttpMethod;
import java.util.Set;
+/** Thrown when a request targets a known path with an HTTP method that is not declared for it. */
public final class MethodNotAllowedException extends RuntimeException {
+ /** The set of HTTP methods that the matched path accepts. */
private final Set allowed;
+ /**
+ * Creates a new exception carrying the methods the path actually accepts.
+ *
+ * @param allowed methods declared for the matched path
+ */
public MethodNotAllowedException(Set allowed) {
super("method not allowed; allowed=" + allowed);
this.allowed = Set.copyOf(allowed);
}
+ /**
+ * Returns the HTTP methods allowed for the matched path.
+ *
+ * @return immutable set of allowed methods
+ */
public Set allowed() {
return allowed;
}
diff --git a/src/main/java/com/retailsvc/http/MissingOperationHandlerException.java b/src/main/java/com/retailsvc/http/MissingOperationHandlerException.java
index 7b4f237..4532650 100644
--- a/src/main/java/com/retailsvc/http/MissingOperationHandlerException.java
+++ b/src/main/java/com/retailsvc/http/MissingOperationHandlerException.java
@@ -1,6 +1,12 @@
package com.retailsvc.http;
+/** Thrown when a request resolves to an OpenAPI operationId that has no registered handler. */
public final class MissingOperationHandlerException extends RuntimeException {
+ /**
+ * Creates a new exception identifying the unregistered operation.
+ *
+ * @param operationId the OpenAPI operationId with no handler bound
+ */
public MissingOperationHandlerException(String operationId) {
super("no handler registered for operationId=" + operationId);
}
diff --git a/src/main/java/com/retailsvc/http/NotFoundException.java b/src/main/java/com/retailsvc/http/NotFoundException.java
index ea3f82e..7159ece 100644
--- a/src/main/java/com/retailsvc/http/NotFoundException.java
+++ b/src/main/java/com/retailsvc/http/NotFoundException.java
@@ -1,6 +1,12 @@
package com.retailsvc.http;
+/** Thrown when a request path does not match any route declared in the OpenAPI spec. */
public final class NotFoundException extends RuntimeException {
+ /**
+ * Creates a new exception with the given detail message.
+ *
+ * @param message human-readable explanation of the missing route
+ */
public NotFoundException(String message) {
super(message);
}
diff --git a/src/main/java/com/retailsvc/http/OpenApiServer.java b/src/main/java/com/retailsvc/http/OpenApiServer.java
index 12371ef..ef2f5db 100644
--- a/src/main/java/com/retailsvc/http/OpenApiServer.java
+++ b/src/main/java/com/retailsvc/http/OpenApiServer.java
@@ -143,6 +143,11 @@ record HandlerConfig(
System.currentTimeMillis() - t0);
}
+ /**
+ * Returns the TCP port the server is listening on. Useful when bound to an ephemeral port.
+ *
+ * @return the active listen port
+ */
public int listenPort() {
return httpServer.getAddress().getPort();
}
@@ -150,6 +155,8 @@ public int listenPort() {
/**
* Returns the local address the server is bound to. For a wildcard-bound server this is the
* wildcard address; for a loopback-bound server this is the loopback address.
+ *
+ * @return the bound local {@link InetAddress}
*/
public InetAddress bindAddress() {
return httpServer.getAddress().getAddress();
@@ -175,6 +182,11 @@ public void close() {
stop(shutdownTimeoutSeconds);
}
+ /**
+ * Creates a new fluent {@link Builder} for configuring and starting an {@link OpenApiServer}.
+ *
+ * @return a fresh builder
+ */
public static Builder builder() {
return new Builder();
}
@@ -198,11 +210,24 @@ public static final class Builder {
private Builder() {}
+ /**
+ * Sets the parsed OpenAPI {@link Spec} the server will expose. Required.
+ *
+ * @param spec the parsed OpenAPI specification
+ * @return this builder
+ */
public Builder spec(Spec spec) {
this.spec = spec;
return this;
}
+ /**
+ * Registers a {@link TypeMapper} for the given request/response media type.
+ *
+ * @param mediaType the media type (e.g. {@code application/json}); case-insensitive
+ * @param mapper the mapper to use for that media type
+ * @return this builder
+ */
public Builder bodyMapper(String mediaType, TypeMapper mapper) {
requireNonNull(mediaType, "mediaType must not be null");
requireNonNull(mapper, "mapper must not be null");
@@ -210,10 +235,23 @@ public Builder bodyMapper(String mediaType, TypeMapper mapper) {
return this;
}
+ /**
+ * Convenience shortcut for {@link #bodyMapper(String, TypeMapper)
+ * bodyMapper("application/json", mapper)}.
+ *
+ * @param mapper the JSON {@link TypeMapper}
+ * @return this builder
+ */
public Builder jsonMapper(TypeMapper mapper) {
return bodyMapper(JSON, mapper);
}
+ /**
+ * Sets the map of OpenAPI {@code operationId} to {@link RequestHandler}. Required.
+ *
+ * @param handlers the operation-id to handler map
+ * @return this builder
+ */
public Builder handlers(Map handlers) {
this.handlers = handlers;
return this;
@@ -223,6 +261,9 @@ public Builder handlers(Map handlers) {
* Registers a {@link ResponseDecorator} that transforms the {@link Response} returned by the
* handler before it is rendered. Decorators compose in registration order; decorator-supplied
* headers override handler-supplied ones on conflict.
+ *
+ * @param decorator the response decorator to register
+ * @return this builder
*/
public Builder responseDecorator(ResponseDecorator decorator) {
decorators.add(requireNonNull(decorator, "decorator must not be null"));
@@ -232,6 +273,9 @@ public Builder responseDecorator(ResponseDecorator decorator) {
/**
* Registers a {@link RequestInterceptor} that wraps the handler invocation. Interceptors run in
* registration order; the first registered is the outermost.
+ *
+ * @param interceptor the request interceptor to register
+ * @return this builder
*/
public Builder interceptor(RequestInterceptor interceptor) {
interceptors.add(requireNonNull(interceptor, "interceptor must not be null"));
@@ -243,6 +287,9 @@ public Builder interceptor(RequestInterceptor interceptor) {
* request thread inside the library's request scope, in registration order, with all exceptions
* swallowed. Hooks fire only when a {@link Request} was successfully built — pre-request
* failures (404, 405, 400 validation) do not fire hooks.
+ *
+ * @param hook the after-response hook to register
+ * @return this builder
*/
public Builder afterResponseHook(AfterResponseHook hook) {
afterHooks.add(requireNonNull(hook, "hook must not be null"));
@@ -254,6 +301,10 @@ public Builder afterResponseHook(AfterResponseHook hook) {
* The library extracts a {@link Credential} per request and hands it to this callback; return a
* non-empty {@link Optional} carrying the principal on success, or {@link Optional#empty()} to
* deny. Library renders 401/403 on denial.
+ *
+ * @param schemeName the OpenAPI security scheme name this validator authenticates
+ * @param validator the validator that resolves the credential to a principal
+ * @return this builder
*/
public Builder securityValidator(String schemeName, SchemeValidator validator) {
requireNonNull(schemeName, "schemeName must not be null");
@@ -267,12 +318,21 @@ public Builder securityValidator(String schemeName, SchemeValidator validator) {
* authenticates requests upstream. The library still parses {@code securitySchemes} into the
* {@link Spec}, but {@code SecurityFilter} short-circuits and the boot-time
* validator-registration check is skipped.
+ *
+ * @return this builder
*/
public Builder useExternalAuthentication() {
this.externalAuth = true;
return this;
}
+ /**
+ * Sets the {@link ExceptionHandler} used to render uncaught handler exceptions. When unset, a
+ * default JSON {@code application/problem+json} handler is used.
+ *
+ * @param exceptionHandler the exception handler
+ * @return this builder
+ */
public Builder exceptionHandler(ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
return this;
@@ -281,6 +341,9 @@ public Builder exceptionHandler(ExceptionHandler exceptionHandler) {
/**
* Sets the TCP port to listen on. Defaults to {@value #DEFAULT_PORT} when not set. Use {@code
* 0} to bind on an ephemeral port (read it back via {@link OpenApiServer#listenPort()}).
+ *
+ * @param port the TCP port to bind, or {@code 0} for an ephemeral port
+ * @return this builder
*/
public Builder port(int port) {
this.port = port;
@@ -291,6 +354,9 @@ public Builder port(int port) {
* Restricts the server to a specific local interface. {@code null} (the default) binds to the
* wildcard address (all interfaces). Use {@link InetAddress#getLoopbackAddress()} to listen on
* loopback only.
+ *
+ * @param bindAddress the local interface to bind, or {@code null} for the wildcard address
+ * @return this builder
*/
public Builder bindAddress(InetAddress bindAddress) {
this.bindAddress = bindAddress;
@@ -301,6 +367,9 @@ public Builder bindAddress(InetAddress bindAddress) {
* Sets the default drain timeout used by {@link OpenApiServer#close()}. {@code 0} (the default)
* stops immediately; positive values wait up to that many seconds for in-flight exchanges to
* finish.
+ *
+ * @param shutdownTimeoutSeconds non-negative drain timeout in seconds
+ * @return this builder
*/
public Builder shutdownTimeoutSeconds(int shutdownTimeoutSeconds) {
if (shutdownTimeoutSeconds < 0) {
@@ -316,6 +385,10 @@ public Builder shutdownTimeoutSeconds(int shutdownTimeoutSeconds) {
* Use for side concerns like {@code /alive}, {@code /health}, or serving the spec itself —
* anything that isn't an OpenAPI {@code operationId}. For OpenAPI-described operations use
* {@link #handlers(Map)}.
+ *
+ * @param path the exact context path to expose
+ * @param handler the handler to invoke for that path
+ * @return this builder
*/
public Builder extraRoute(String path, RequestHandler handler) {
requireNonNull(path, "path must not be null");
@@ -327,6 +400,12 @@ public Builder extraRoute(String path, RequestHandler handler) {
return this;
}
+ /**
+ * Builds and starts the {@link OpenApiServer} with the configured settings.
+ *
+ * @return a started server bound to the configured address and port
+ * @throws IOException if the underlying {@link HttpServer} cannot be created or bound
+ */
public OpenApiServer build() throws IOException {
requireNonNull(spec, "Spec must not be null");
requireNonNull(handlers, "handlers must not be null");
diff --git a/src/main/java/com/retailsvc/http/Request.java b/src/main/java/com/retailsvc/http/Request.java
index 13d0404..de68947 100644
--- a/src/main/java/com/retailsvc/http/Request.java
+++ b/src/main/java/com/retailsvc/http/Request.java
@@ -179,12 +179,19 @@ public Request(
this.afterHooks = afterHooks;
}
+ /**
+ * Raw request body bytes.
+ *
+ * @return body bytes; never {@code null}, may be empty
+ */
public byte[] bytes() {
return body;
}
/**
* Loose structural view of the body (typically a {@code Map} / {@code List} / boxed primitive).
+ *
+ * @return parsed body, or {@code null} if there is no body
*/
public Object parsed() {
return parsed;
@@ -198,6 +205,9 @@ public Object parsed() {
* the loose {@link #parsed()} value already is an instance of {@code type}, it is returned
* directly without re-deserialising.
*
+ * @param target POJO type
+ * @param type target class to deserialise the body into
+ * @return body deserialised as {@code type}
* @throws NullPointerException if {@code type} is null
* @throws IllegalStateException if there is no body, or if the body mapper does not implement
* {@link TypedTypeMapper}
@@ -223,20 +233,37 @@ public T asPojo(Class type) {
/**
* Value of the {@code Content-Type} request header, or {@link Optional#empty()} if absent or
* blank. Convenience for {@code header("Content-Type")} — the most frequently inspected header.
+ *
+ * @return content type value, or empty if absent or blank
*/
public Optional contentType() {
return header(CONTENT_TYPE);
}
+ /**
+ * OpenAPI {@code operationId} the request was routed to.
+ *
+ * @return operation ID
+ */
public String operationId() {
return operationId;
}
+ /**
+ * Path variables extracted by the router, keyed by parameter name.
+ *
+ * @return path parameter map
+ */
public Map pathParams() {
return pathParameters;
}
- /** Value of the path parameter {@code name}, or {@code null} if absent. */
+ /**
+ * Value of the path parameter {@code name}, or {@code null} if absent.
+ *
+ * @param name path parameter name
+ * @return decoded value, or {@code null} if absent
+ */
public String pathParam(String name) {
return pathParameters.get(name);
}
@@ -245,6 +272,9 @@ public String pathParam(String name) {
* First value of the request header {@code name}, or {@link Optional#empty()} if absent or blank.
* Blank values are treated as missing so callers can write {@code req.header("X").map(...)}
* without the extra {@code filter(v -> !v.isBlank())} step.
+ *
+ * @param name header name (case-insensitive)
+ * @return first header value, or empty if absent or blank
*/
public Optional header(String name) {
String raw = headerLookup.apply(name);
@@ -254,6 +284,8 @@ public Optional header(String name) {
/**
* Raw (percent-encoded) query string from the request URI, or {@code null} if the URI has no
* query component.
+ *
+ * @return raw query string, or {@code null} if absent
*/
public String rawQuery() {
return rawQuery;
@@ -262,6 +294,8 @@ public String rawQuery() {
/**
* Decoded query parameters keyed by name. Empty if the URI has no query. For repeated keys, the
* first occurrence wins. Values are URL-decoded with UTF-8.
+ *
+ * @return decoded query parameter map
*/
public Map queryParams() {
if (queryParamCache == null) {
@@ -275,6 +309,9 @@ public Map queryParams() {
* blank. Blank values are treated as missing so callers can write {@code
* req.queryParam("limit").map(Integer::parseInt).orElse(DEFAULT)} without the extra {@code
* filter(v -> !v.isBlank())} step.
+ *
+ * @param name query parameter name
+ * @return first decoded value, or empty if absent or blank
*/
public Optional queryParam(String name) {
String raw = queryParams().get(name);
@@ -284,12 +321,19 @@ public Optional queryParam(String name) {
/**
* Principals stashed by {@code SecurityFilter}, keyed by securityScheme name. Empty when the
* request had no security requirements or when {@code useExternalAuthentication()} is set.
+ *
+ * @return principals keyed by security scheme name
*/
public Map principals() {
return principals;
}
- /** Convenience for the common single-scheme case. */
+ /**
+ * Convenience for the common single-scheme case.
+ *
+ * @param schemeName OpenAPI security scheme name
+ * @return principal for that scheme, or empty if absent
+ */
public Optional principal(String schemeName) {
return Optional.ofNullable(principals.get(schemeName));
}
@@ -298,6 +342,8 @@ public Optional principal(String schemeName) {
* HTTP method of the request. Never {@code null} for requests routed through the standard
* pipeline; {@code null} only when the {@code Request} was constructed via a legacy constructor
* without a method.
+ *
+ * @return HTTP method, or {@code null} for legacy constructions
*/
public HttpMethod method() {
return method;
@@ -307,6 +353,9 @@ public HttpMethod method() {
* Returns a new {@code Request} identical to this one except with the supplied principals. Used
* by {@code SecurityFilter} on success; the returned instance carries the principals through to
* the {@link RequestHandler}.
+ *
+ * @param principals principals keyed by security scheme name
+ * @return new {@code Request} carrying the supplied principals
*/
public Request withPrincipals(Map principals) {
return new Request(
@@ -330,6 +379,7 @@ public Request withPrincipals(Map principals) {
* Calls made after the runner has snapshotted the queue (e.g. from inside a running hook, or
* from a leaked {@code Request} reference held past the response) are silently ignored.
*
+ * @param runnable runnable to execute after the response is sent
* @throws NullPointerException if {@code runnable} is null
*/
public void afterResponse(Runnable runnable) {
@@ -338,9 +388,11 @@ public void afterResponse(Runnable runnable) {
}
/**
- * Returns an unmodifiable view of the queued after-response runnables. Intended for the framework
- * runner; consumers should use {@link #afterResponse(Runnable)} to register runnables rather than
- * inspecting this list directly.
+ * Returns an unmodifiable view of the queued after-response runnables. Intended for the server's
+ * dispatch runner; consumers should use {@link #afterResponse(Runnable)} to register runnables
+ * rather than inspecting this list directly.
+ *
+ * @return unmodifiable view of the queued runnables, in registration order
*/
public List afterHooks() {
return Collections.unmodifiableList(afterHooks);
diff --git a/src/main/java/com/retailsvc/http/RequestHandler.java b/src/main/java/com/retailsvc/http/RequestHandler.java
index 43493d6..c2ce702 100644
--- a/src/main/java/com/retailsvc/http/RequestHandler.java
+++ b/src/main/java/com/retailsvc/http/RequestHandler.java
@@ -5,12 +5,18 @@
* OpenApiServer.Builder#handlers(java.util.Map)} by operation ID.
*
* Handlers are pure functions of the {@link Request}: they read inputs and return a {@link
- * Response} describing what should be sent. The framework renders the response after applying any
+ * Response} describing what should be sent. The server renders the response after applying any
* registered {@link ResponseDecorator}s. Handlers may throw any {@link RuntimeException}; the
* configured {@link ExceptionHandler} renders it. Handlers that need to surface an {@code
* IOException} should wrap it as {@link java.io.UncheckedIOException}.
*/
@FunctionalInterface
public interface RequestHandler {
+ /**
+ * Handles the request and returns the response to render.
+ *
+ * @param request the incoming request
+ * @return the response to send
+ */
Response handle(Request request);
}
diff --git a/src/main/java/com/retailsvc/http/RequestInterceptor.java b/src/main/java/com/retailsvc/http/RequestInterceptor.java
index c855fbb..8aa5617 100644
--- a/src/main/java/com/retailsvc/http/RequestInterceptor.java
+++ b/src/main/java/com/retailsvc/http/RequestInterceptor.java
@@ -11,11 +11,23 @@
@FunctionalInterface
public interface RequestInterceptor {
+ /**
+ * Runs around the next interceptor or handler in the chain.
+ *
+ * @param request the incoming request
+ * @param next the continuation to invoke the rest of the chain
+ * @return the response produced by the chain, possibly transformed
+ */
Response around(Request request, Continuation next);
/** Continues the chain — calls the next interceptor, or the handler if this is the last one. */
@FunctionalInterface
interface Continuation {
+ /**
+ * Invokes the next link in the chain.
+ *
+ * @return the response from the downstream interceptor or handler
+ */
Response proceed();
}
}
diff --git a/src/main/java/com/retailsvc/http/Response.java b/src/main/java/com/retailsvc/http/Response.java
index f3e35b3..f1ba998 100644
--- a/src/main/java/com/retailsvc/http/Response.java
+++ b/src/main/java/com/retailsvc/http/Response.java
@@ -16,8 +16,8 @@
/**
* The value returned by every {@link RequestHandler}. Carries status, optional body, optional
- * content type, and headers. The framework renders it to the underlying {@code HttpExchange} after
- * any registered {@link ResponseDecorator}s have transformed it.
+ * content type, and headers. The server renders it to the underlying {@code HttpExchange} after any
+ * registered {@link ResponseDecorator}s have transformed it.
*
*
Body handling:
*
@@ -29,28 +29,58 @@
*
Any other object → serialised by the {@link TypeMapper} registered for the response's
* content type (default {@code application/json}).
*
+ *
+ * @param status the HTTP status code to send back to the client (e.g. {@code 200}, {@code 404}).
+ * @param body the response payload, or {@code null} for a status-only response. May be a {@code
+ * byte[]} for verbatim bytes, a {@link BodyWriter} for streaming, or any object that the
+ * configured {@link TypeMapper} for {@link #contentType()} can serialise.
+ * @param contentType the {@code Content-Type} header value for the response, or {@code null} to
+ * default to {@code application/json} when a body is present.
+ * @param headers additional response headers to emit. Never {@code null} after canonicalisation;
+ * the compact constructor defensively copies the supplied map (or substitutes {@link Map#of()}
+ * if {@code null}) so the resulting {@code Response} is effectively immutable.
*/
public record Response(int status, Object body, String contentType, Map headers) {
+ /**
+ * Canonical constructor that normalises {@code headers}: a {@code null} map is replaced with an
+ * empty immutable map and any non-null map is defensively copied so subsequent mutations of the
+ * caller's map cannot leak into this {@code Response}.
+ */
public Response {
headers = headers == null ? Map.of() : Map.copyOf(headers);
}
// -- one-shot, no-body --
- /** {@code 204 No Content} with no body. */
+ /**
+ * {@code 204 No Content} with no body.
+ *
+ * @return a new {@code Response} with status {@code 204} and no body, content type, or headers.
+ */
public static Response empty() {
return new Response(HTTP_NO_CONTENT, null, null, Map.of());
}
- /** Given status, no body. Use for {@code 200 OK} no body, {@code 404}, {@code 405}, etc. */
+ /**
+ * Given status, no body. Use for {@code 200 OK} no body, {@code 404}, {@code 405}, etc.
+ *
+ * @param status the HTTP status code to send.
+ * @return a new {@code Response} with the supplied status and no body, content type, or headers.
+ */
public static Response status(int status) {
return new Response(status, null, null, Map.of());
}
// -- one-shot, JSON body --
- /** {@code 200 OK} with {@code body} serialised as JSON. */
+ /**
+ * {@code 200 OK} with {@code body} serialised as JSON.
+ *
+ * @param body the payload to serialise via the {@link TypeMapper} registered for {@code
+ * application/json}.
+ * @return a new {@code Response} with status {@code 200} and the supplied body.
+ */
public static Response ok(Object body) {
return new Response(HTTP_OK, body, null, Map.of());
}
@@ -58,44 +88,86 @@ public static Response ok(Object body) {
/**
* {@code 201 Created} with {@code body} serialised as JSON. Add a {@code Location} header for the
* new resource via {@link #withHeader(String, String) withHeader("Location", uri)}.
+ *
+ * @param body the representation of the newly-created resource, serialised via the {@link
+ * TypeMapper} registered for {@code application/json}.
+ * @return a new {@code Response} with status {@code 201} and the supplied body.
*/
public static Response created(Object body) {
return new Response(HTTP_CREATED, body, null, Map.of());
}
- /** {@code 202 Accepted} with no body. Use for fire-and-forget async work. */
+ /**
+ * {@code 202 Accepted} with no body. Use for fire-and-forget async work.
+ *
+ * @return a new {@code Response} with status {@code 202} and no body, content type, or headers.
+ */
public static Response accepted() {
return new Response(HTTP_ACCEPTED, null, null, Map.of());
}
- /** {@code 202 Accepted} with {@code body} serialised as JSON (typically a job/poll URL). */
+ /**
+ * {@code 202 Accepted} with {@code body} serialised as JSON (typically a job/poll URL).
+ *
+ * @param body the payload describing where to poll for the async result, serialised via the
+ * {@link TypeMapper} registered for {@code application/json}.
+ * @return a new {@code Response} with status {@code 202} and the supplied body.
+ */
public static Response accepted(Object body) {
return new Response(HTTP_ACCEPTED, body, null, Map.of());
}
- /** {@code 404 Not Found} with no body. */
+ /**
+ * {@code 404 Not Found} with no body.
+ *
+ * @return a new {@code Response} with status {@code 404} and no body, content type, or headers.
+ */
public static Response notFound() {
return new Response(HTTP_NOT_FOUND, null, null, Map.of());
}
- /** {@code 404 Not Found} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
+ /**
+ * {@code 404 Not Found} with {@code body} serialised as JSON (e.g. a ProblemDetail).
+ *
+ * @param body the payload to serialise, typically an RFC 7807 problem detail, via the {@link
+ * TypeMapper} registered for {@code application/json}.
+ * @return a new {@code Response} with status {@code 404} and the supplied body.
+ */
public static Response notFound(Object body) {
return new Response(HTTP_NOT_FOUND, body, null, Map.of());
}
- /** {@code 501 Not Implemented} with no body. */
+ /**
+ * {@code 501 Not Implemented} with no body.
+ *
+ * @return a new {@code Response} with status {@code 501} and no body, content type, or headers.
+ */
public static Response notImplemented() {
return new Response(HTTP_NOT_IMPLEMENTED, null, null, Map.of());
}
- /** {@code status} with {@code body} serialised by the content-type's {@link TypeMapper}. */
+ /**
+ * {@code status} with {@code body} serialised by the content-type's {@link TypeMapper}.
+ *
+ * @param status the HTTP status code to send.
+ * @param body the payload to serialise via the {@link TypeMapper} registered for the response's
+ * content type (defaults to {@code application/json}).
+ * @return a new {@code Response} with the supplied status and body.
+ */
public static Response of(int status, Object body) {
return new Response(status, body, null, Map.of());
}
// -- one-shot, text / raw bytes --
- /** {@code status} with {@code body} written as UTF-8 with {@code Content-Type: text/plain}. */
+ /**
+ * {@code status} with {@code body} written as UTF-8 with {@code Content-Type: text/plain}.
+ *
+ * @param status the HTTP status code to send.
+ * @param body the text payload; encoded to bytes using {@link StandardCharsets#UTF_8}.
+ * @return a new {@code Response} carrying the UTF-8 encoded bytes with {@code Content-Type:
+ * text/plain; charset=UTF-8}.
+ */
public static Response text(int status, String body) {
return new Response(
status, body.getBytes(StandardCharsets.UTF_8), "text/plain; charset=UTF-8", Map.of());
@@ -103,6 +175,11 @@ public static Response text(int status, String body) {
/**
* {@code status} with pre-serialised {@code bytes} written verbatim under {@code contentType}.
+ *
+ * @param status the HTTP status code to send.
+ * @param bytes the pre-serialised payload to write verbatim to the response body.
+ * @param contentType the {@code Content-Type} header value to advertise for {@code bytes}.
+ * @return a new {@code Response} carrying {@code bytes} as the body under {@code contentType}.
*/
public static Response bytes(int status, byte[] bytes, String contentType) {
return new Response(status, bytes, contentType, Map.of());
@@ -110,12 +187,34 @@ public static Response bytes(int status, byte[] bytes, String contentType) {
// -- streaming --
- /** Streaming response with unknown length (chunked transfer encoding). */
+ /**
+ * Streaming response with unknown length (chunked transfer encoding).
+ *
+ * @param status the HTTP status code to send.
+ * @param contentType the {@code Content-Type} header value to advertise for the streamed body.
+ * @param writer callback invoked with the response {@link OutputStream}; the body is written
+ * incrementally and flushed using chunked transfer encoding because the total length is not
+ * known up-front.
+ * @return a new {@code Response} that will stream its body via {@code writer} when rendered.
+ */
public static Response stream(int status, String contentType, StreamingBody writer) {
return new Response(status, new BodyWriter.Chunked(writer::writeTo), contentType, Map.of());
}
- /** Streaming response with a known content length. */
+ /**
+ * Streaming response with a known content length.
+ *
+ * @param status the HTTP status code to send.
+ * @param length the exact number of bytes that {@code writer} will produce; sent as {@code
+ * Content-Length}. Must be non-negative.
+ * @param contentType the {@code Content-Type} header value to advertise for the streamed body.
+ * @param writer callback invoked with the response {@link OutputStream}; the body is written
+ * incrementally but the total length is advertised up-front so chunked transfer encoding is
+ * not used.
+ * @return a new {@code Response} that will stream exactly {@code length} bytes via {@code writer}
+ * when rendered.
+ * @throws IllegalArgumentException if {@code length} is negative.
+ */
public static Response stream(int status, long length, String contentType, StreamingBody writer) {
if (length < 0) {
throw new IllegalArgumentException("length must be non-negative");
@@ -126,20 +225,48 @@ public static Response stream(int status, long length, String contentType, Strea
// -- non-destructive mutators --
+ /**
+ * Returns a copy of this response with the status code replaced.
+ *
+ * @param newStatus the HTTP status code to use in the returned {@code Response}.
+ * @return a new {@code Response} identical to this one except for {@link #status()}.
+ */
public Response withStatus(int newStatus) {
return new Response(newStatus, body, contentType, headers);
}
+ /**
+ * Returns a copy of this response with the content type replaced.
+ *
+ * @param newContentType the {@code Content-Type} header value to use, or {@code null} to fall
+ * back to the library default ({@code application/json}) when serialising the body.
+ * @return a new {@code Response} identical to this one except for {@link #contentType()}.
+ */
public Response withContentType(String newContentType) {
return new Response(status, body, newContentType, headers);
}
+ /**
+ * Returns a copy of this response with an additional (or replaced) header. Existing headers are
+ * preserved; if {@code name} already exists its value is overwritten.
+ *
+ * @param name the header name to set.
+ * @param value the header value to set.
+ * @return a new {@code Response} with the merged headers.
+ */
public Response withHeader(String name, String value) {
LinkedHashMap merged = new LinkedHashMap<>(headers);
merged.put(name, value);
return new Response(status, body, contentType, merged);
}
+ /**
+ * Returns a copy of this response with the supplied headers merged on top of the existing ones.
+ * Entries in {@code additional} overwrite any existing header with the same name.
+ *
+ * @param additional the headers to merge into the returned {@code Response}.
+ * @return a new {@code Response} with the merged headers.
+ */
public Response withHeaders(Map additional) {
LinkedHashMap merged = new LinkedHashMap<>(headers);
merged.putAll(additional);
@@ -149,6 +276,13 @@ public Response withHeaders(Map additional) {
/** Writer signature for {@link #stream(int, String, StreamingBody)}. */
@FunctionalInterface
public interface StreamingBody {
+ /**
+ * Writes the streamed response body to {@code out}. Implementations should write all bytes and
+ * may flush as needed; the server closes the stream once this method returns.
+ *
+ * @param out the response output stream to write to.
+ * @throws IOException if writing to {@code out} fails.
+ */
void writeTo(OutputStream out) throws IOException;
}
}
diff --git a/src/main/java/com/retailsvc/http/ResponseDecorator.java b/src/main/java/com/retailsvc/http/ResponseDecorator.java
index 5603a98..ab796b0 100644
--- a/src/main/java/com/retailsvc/http/ResponseDecorator.java
+++ b/src/main/java/com/retailsvc/http/ResponseDecorator.java
@@ -1,7 +1,7 @@
package com.retailsvc.http;
/**
- * Transforms the {@link Response} returned by a handler before the framework renders it. Decorators
+ * Transforms the {@link Response} returned by a handler before the server renders it. Decorators
* run in registration order; the result of each is fed to the next. Use for cross-cutting headers
* (correlation id, tenant id, server identifier) or any other uniform response shaping.
*
@@ -11,5 +11,12 @@
*/
@FunctionalInterface
public interface ResponseDecorator {
+ /**
+ * Transforms the handler's response before rendering.
+ *
+ * @param request the originating request
+ * @param response the response produced so far
+ * @return the transformed response to pass on
+ */
Response decorate(Request request, Response response);
}
diff --git a/src/main/java/com/retailsvc/http/SchemeValidator.java b/src/main/java/com/retailsvc/http/SchemeValidator.java
index 00a6d13..0051f8b 100644
--- a/src/main/java/com/retailsvc/http/SchemeValidator.java
+++ b/src/main/java/com/retailsvc/http/SchemeValidator.java
@@ -8,5 +8,12 @@
*/
@FunctionalInterface
public interface SchemeValidator {
+ /**
+ * Validates the extracted credential for the given request.
+ *
+ * @param request the incoming request
+ * @param credential the credential extracted from the request
+ * @return the authenticated principal, or {@link Optional#empty()} to deny
+ */
Optional validate(Request request, Credential credential);
}
diff --git a/src/main/java/com/retailsvc/http/TypeMapper.java b/src/main/java/com/retailsvc/http/TypeMapper.java
index 8af27d0..a60c2a9 100644
--- a/src/main/java/com/retailsvc/http/TypeMapper.java
+++ b/src/main/java/com/retailsvc/http/TypeMapper.java
@@ -10,12 +10,22 @@
public interface TypeMapper {
/**
+ * Parses the raw request body bytes into a Java object suitable for OpenAPI schema validation
+ * (typically {@code Map} / {@code List} / String / Number / Boolean /
+ * null for JSON).
+ *
* @param body raw request body bytes
* @param contentTypeHeader the full raw {@code Content-Type} header, used for charset and other
* parameters (the JSON mapper ignores it)
+ * @return the parsed value, or {@code null} for an empty body
*/
Object readFrom(byte[] body, String contentTypeHeader);
- /** Serializes {@code value} to bytes suitable for writing as the response body. */
+ /**
+ * Serializes {@code value} to bytes suitable for writing as the response body.
+ *
+ * @param value the value to serialise; the accepted type is type-mapper-dependent
+ * @return the serialised bytes ready to write to the response output stream
+ */
byte[] writeTo(Object value);
}
diff --git a/src/main/java/com/retailsvc/http/TypedTypeMapper.java b/src/main/java/com/retailsvc/http/TypedTypeMapper.java
index 78126c0..1b613fa 100644
--- a/src/main/java/com/retailsvc/http/TypedTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/TypedTypeMapper.java
@@ -2,7 +2,7 @@
/**
* Optional capability for {@link TypeMapper}s that can deserialise a request body directly into a
- * caller-supplied target type. The framework uses this when handlers call {@link
+ * caller-supplied target type. The library uses this when handlers call {@link
* Request#asPojo(Class)}; mappers that cannot meaningfully honour a target type (e.g. the built-in
* form / text mappers) should not implement this interface.
*
@@ -17,6 +17,8 @@ public interface TypedTypeMapper extends TypeMapper {
* @param body raw request body bytes
* @param contentTypeHeader the full raw {@code Content-Type} header (for charset / params)
* @param type the target type
+ * @param the target type
+ * @return the deserialised instance
*/
T readAs(byte[] body, String contentTypeHeader, Class type);
}
diff --git a/src/main/java/com/retailsvc/http/ValidationException.java b/src/main/java/com/retailsvc/http/ValidationException.java
index 6bbd505..39eea91 100644
--- a/src/main/java/com/retailsvc/http/ValidationException.java
+++ b/src/main/java/com/retailsvc/http/ValidationException.java
@@ -3,6 +3,7 @@
import com.retailsvc.http.validate.ValidationError;
import java.util.concurrent.atomic.AtomicLong;
+/** Runtime exception raised when a request fails OpenAPI schema validation. */
public final class ValidationException extends RuntimeException {
/**
* Counts every {@code ValidationException} ever constructed. Used to assert that the validator
@@ -13,12 +14,22 @@ public final class ValidationException extends RuntimeException {
private final transient ValidationError error;
+ /**
+ * Creates a new validation exception wrapping the given error.
+ *
+ * @param error the validation error that triggered this exception
+ */
public ValidationException(ValidationError error) {
super(error.pointer() + " [" + error.keyword() + "] " + error.message());
this.error = error;
CONSTRUCTIONS.incrementAndGet();
}
+ /**
+ * Returns the underlying validation error.
+ *
+ * @return the validation error wrapped by this exception
+ */
public ValidationError error() {
return error;
}
diff --git a/src/main/java/com/retailsvc/http/internal/BodyWriter.java b/src/main/java/com/retailsvc/http/internal/BodyWriter.java
index ca6c7f7..83dad5e 100644
--- a/src/main/java/com/retailsvc/http/internal/BodyWriter.java
+++ b/src/main/java/com/retailsvc/http/internal/BodyWriter.java
@@ -9,9 +9,23 @@
*/
public sealed interface BodyWriter permits BodyWriter.Sized, BodyWriter.Chunked {
+ /**
+ * Writes the body bytes to the exchange output stream.
+ *
+ * Declares {@link IOException} so the renderer can let it propagate up to the server's
+ * exception filter rather than swallowing it at the body-writing layer.
+ *
+ * @param out the exchange output stream to write the response body to
+ * @throws IOException if writing to the output stream fails
+ */
void writeTo(OutputStream out) throws IOException;
- /** Known {@code Content-Length}. */
+ /**
+ * Known {@code Content-Length}.
+ *
+ * @param length the exact {@code Content-Length} to set on the response
+ * @param writer writes the body bytes to the exchange output stream
+ */
record Sized(long length, IOConsumer writer) implements BodyWriter {
@Override
public void writeTo(OutputStream out) throws IOException {
@@ -19,7 +33,14 @@ public void writeTo(OutputStream out) throws IOException {
}
}
- /** Unknown length — chunked transfer encoding. */
+ /**
+ * Unknown length — chunked transfer encoding.
+ *
+ *
The renderer uses chunked transfer encoding because the body length is unknown ahead of
+ * time.
+ *
+ * @param writer writes the body bytes to the exchange output stream
+ */
record Chunked(IOConsumer writer) implements BodyWriter {
@Override
public void writeTo(OutputStream out) throws IOException {
@@ -30,6 +51,12 @@ public void writeTo(OutputStream out) throws IOException {
/** {@code Consumer} that is allowed to throw {@link IOException}. */
@FunctionalInterface
interface IOConsumer {
+ /**
+ * Single write callback that streams bytes to the given output stream.
+ *
+ * @param out the exchange output stream to write to
+ * @throws IOException if writing to the output stream fails
+ */
void accept(OutputStream out) throws IOException;
}
}
diff --git a/src/main/java/com/retailsvc/http/internal/ContentTypeHeader.java b/src/main/java/com/retailsvc/http/internal/ContentTypeHeader.java
index d0858ca..4775d93 100644
--- a/src/main/java/com/retailsvc/http/internal/ContentTypeHeader.java
+++ b/src/main/java/com/retailsvc/http/internal/ContentTypeHeader.java
@@ -11,6 +11,10 @@ private ContentTypeHeader() {}
/**
* Returns the bare media type, stripping parameters and lower-casing for case-insensitive
* matching (RFC 9110 / 2045). {@code null} → {@code application/json}.
+ *
+ * @param header raw {@code Content-Type} header value (nullable; missing header is treated as
+ * {@code application/json}).
+ * @return the bare media type lower-cased with parameters stripped.
*/
public static String mediaType(String header) {
if (header == null) {
@@ -21,7 +25,13 @@ public static String mediaType(String header) {
return bare.trim().toLowerCase(Locale.ROOT);
}
- /** Returns the named parameter value (e.g. {@code charset}), or empty if absent. */
+ /**
+ * Returns the named parameter value (e.g. {@code charset}), or empty if absent.
+ *
+ * @param header raw {@code Content-Type} header value (nullable returns empty).
+ * @param name the parameter name to look up (case-insensitive, e.g. {@code charset}).
+ * @return the parameter value (unquoted if quoted), or empty if the header has no such parameter.
+ */
public static Optional parameter(String header, String name) {
if (header == null) {
return Optional.empty();
diff --git a/src/main/java/com/retailsvc/http/internal/DispatchHandler.java b/src/main/java/com/retailsvc/http/internal/DispatchHandler.java
index aeb9bee..c1baba2 100644
--- a/src/main/java/com/retailsvc/http/internal/DispatchHandler.java
+++ b/src/main/java/com/retailsvc/http/internal/DispatchHandler.java
@@ -12,9 +12,36 @@
import java.util.List;
import java.util.Map;
+/**
+ * Final {@link HttpHandler} that dispatches a fully-prepared {@link Request} (bound via {@link
+ * #CURRENT}) to the user-supplied {@link RequestHandler} for its {@code operationId}, wraps the
+ * chain in the registered {@link RequestInterceptor}s, applies the registered {@link
+ * ResponseDecorator}s to the result, then renders the final {@link Response} via the shared {@link
+ * ResponseRenderer}.
+ *
+ * If no handler is registered for the resolved {@code operationId}, this dispatcher throws
+ * {@link MissingOperationHandlerException}.
+ */
public final class DispatchHandler implements HttpHandler {
+ /**
+ * Thread-confined {@link ScopedValue} that binds the current {@link Request} for the lifetime of
+ * dispatch.
+ *
+ *
Read by request interceptors and after-response hooks that need access to the request
+ * without it being threaded through their signatures. The binding is not propagated across
+ * executor boundaries — code that hands work off to another thread must capture the value
+ * explicitly before doing so.
+ */
public static final ScopedValue CURRENT = ScopedValue.newInstance();
+
+ /**
+ * {@link com.sun.net.httpserver.HttpExchange} attribute key under which the rendered {@link
+ * Response} is stashed for downstream filters.
+ *
+ * Consumed by the access-log filter and after-response hooks that need to inspect the response
+ * produced by this dispatcher.
+ */
public static final String RESPONSE_ATTR = "com.retailsvc.http.response";
private final Map handlers;
@@ -22,6 +49,16 @@ public final class DispatchHandler implements HttpHandler {
private final List decorators;
private final ResponseRenderer renderer;
+ /**
+ * Creates a new dispatcher.
+ *
+ * @param handlers map of {@code operationId} to {@link RequestHandler} (defensively copied)
+ * @param interceptors registered {@link RequestInterceptor}s in registration order (defensively
+ * copied)
+ * @param decorators registered {@link ResponseDecorator}s in registration order (defensively
+ * copied)
+ * @param renderer the shared {@link ResponseRenderer} used to write responses to the exchange
+ */
public DispatchHandler(
Map handlers,
List interceptors,
diff --git a/src/main/java/com/retailsvc/http/internal/ExceptionFilter.java b/src/main/java/com/retailsvc/http/internal/ExceptionFilter.java
index 649a14e..1ed781a 100644
--- a/src/main/java/com/retailsvc/http/internal/ExceptionFilter.java
+++ b/src/main/java/com/retailsvc/http/internal/ExceptionFilter.java
@@ -6,11 +6,25 @@
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
+/**
+ * Outermost filter in the chain. Catches {@link RuntimeException} and {@link IOException} thrown by
+ * any downstream filter or handler, maps the throwable to a {@link Response} via the user-supplied
+ * {@link ExceptionHandler}, and renders it via the shared {@link ResponseRenderer}.
+ *
+ * Runs outside any interceptor {@link java.lang.ScopedValue} bindings — those are torn down as
+ * the exception unwinds.
+ */
public final class ExceptionFilter extends Filter {
private final ExceptionHandler handler;
private final ResponseRenderer renderer;
+ /**
+ * Creates a new exception filter.
+ *
+ * @param handler user-supplied exception-to-response mapper
+ * @param renderer the shared response renderer used to write the error response
+ */
public ExceptionFilter(ExceptionHandler handler, ResponseRenderer renderer) {
this.handler = handler;
this.renderer = renderer;
diff --git a/src/main/java/com/retailsvc/http/internal/ExtraRouteAdapter.java b/src/main/java/com/retailsvc/http/internal/ExtraRouteAdapter.java
index 95d496d..545b2dc 100644
--- a/src/main/java/com/retailsvc/http/internal/ExtraRouteAdapter.java
+++ b/src/main/java/com/retailsvc/http/internal/ExtraRouteAdapter.java
@@ -22,6 +22,12 @@ public final class ExtraRouteAdapter implements HttpHandler {
private final RequestHandler handler;
private final ResponseRenderer renderer;
+ /**
+ * Create an adapter wrapping a user-supplied extra-route handler.
+ *
+ * @param handler the user-supplied handler for this extra (out-of-spec) route
+ * @param renderer the shared response renderer
+ */
public ExtraRouteAdapter(RequestHandler handler, ResponseRenderer renderer) {
this.handler = handler;
this.renderer = renderer;
diff --git a/src/main/java/com/retailsvc/http/internal/FormTypeMapper.java b/src/main/java/com/retailsvc/http/internal/FormTypeMapper.java
index c5fea5b..db292e3 100644
--- a/src/main/java/com/retailsvc/http/internal/FormTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/internal/FormTypeMapper.java
@@ -11,6 +11,13 @@ public final class FormTypeMapper implements TypeMapper {
private final FormUrlEncodedParser parser = new FormUrlEncodedParser();
+ /**
+ * Creates a form-urlencoded type mapper. The mapper is stateless beyond a lazily-shared parser.
+ */
+ public FormTypeMapper() {
+ // State is held by the final parser field above.
+ }
+
@Override
public Object readFrom(byte[] body, String contentTypeHeader) {
return parser.parse(body, contentTypeHeader);
diff --git a/src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java b/src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java
index f3811a7..592dddd 100644
--- a/src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java
+++ b/src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java
@@ -15,7 +15,19 @@
/** Parses an {@code application/x-www-form-urlencoded} request body. */
public final class FormUrlEncodedParser {
- /** Parses the body to a {@code Map} ({@code String} or {@code List}). */
+ /** Creates a new parser. */
+ public FormUrlEncodedParser() {
+ // Stateless; nothing to initialise.
+ }
+
+ /**
+ * Parses the body to a {@code Map} ({@code String} or {@code List}).
+ *
+ * @param body raw request body bytes
+ * @param contentTypeHeader the request {@code Content-Type} header (used for the charset
+ * parameter); may be {@code null}
+ * @return decoded form fields preserving insertion order
+ */
public Map parse(byte[] body, String contentTypeHeader) {
Charset charset = resolveCharset(contentTypeHeader);
if (body.length == 0) {
diff --git a/src/main/java/com/retailsvc/http/internal/HealthRenderer.java b/src/main/java/com/retailsvc/http/internal/HealthRenderer.java
index cec7e45..c38f0ea 100644
--- a/src/main/java/com/retailsvc/http/internal/HealthRenderer.java
+++ b/src/main/java/com/retailsvc/http/internal/HealthRenderer.java
@@ -14,6 +14,13 @@ public final class HealthRenderer {
private HealthRenderer() {}
+ /**
+ * Renders the health outcome as a JSON document.
+ *
+ * @param up overall outcome ({@code true} = Up, {@code false} = Down)
+ * @param dependencies dependency outcomes to include
+ * @return a JSON string with {@code outcome} and {@code dependencies} fields
+ */
public static String renderJson(boolean up, List dependencies) {
StringBuilder sb = new StringBuilder(INITIAL_CAPACITY);
sb.append("{\"outcome\":\"").append(label(up)).append("\",\"dependencies\":[");
diff --git a/src/main/java/com/retailsvc/http/internal/NotFoundHandler.java b/src/main/java/com/retailsvc/http/internal/NotFoundHandler.java
index c4a31e5..ca3a116 100644
--- a/src/main/java/com/retailsvc/http/internal/NotFoundHandler.java
+++ b/src/main/java/com/retailsvc/http/internal/NotFoundHandler.java
@@ -6,9 +6,14 @@
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
-/** Returns 404 with no body. Used for the framework's catch-all {@code /} context. */
+/** Returns 404 with no body. Used for the server's catch-all {@code /} context. */
public final class NotFoundHandler implements HttpHandler {
+ /** Creates a new handler. */
+ public NotFoundHandler() {
+ // Stateless; nothing to initialise.
+ }
+
@Override
public void handle(HttpExchange exchange) throws IOException {
try (exchange) {
diff --git a/src/main/java/com/retailsvc/http/internal/ProblemDetail.java b/src/main/java/com/retailsvc/http/internal/ProblemDetail.java
index b34135e..a764ea5 100644
--- a/src/main/java/com/retailsvc/http/internal/ProblemDetail.java
+++ b/src/main/java/com/retailsvc/http/internal/ProblemDetail.java
@@ -8,17 +8,50 @@
* Carrier for an RFC 7807 problem+json document. Serialized by the registered JSON {@code
* TypeMapper}; the wire shape and field-order are whatever the configured mapper produces — title
* is advisory per RFC 7807 since {@code type} is always {@code about:blank}.
+ *
+ * @param type URI reference that identifies the problem type. This library always emits {@code
+ * about:blank}, which per RFC 7807 means {@code title} carries the advisory description of the
+ * problem.
+ * @param title short, human-readable summary of the problem type (e.g. {@code "Bad Request"}).
+ * Advisory only when {@code type} is {@code about:blank}, but still surfaced to clients.
+ * @param status HTTP status code generated by the origin server for this occurrence of the problem;
+ * mirrors the status written on the wire.
+ * @param detail human-readable explanation specific to this occurrence of the problem (e.g. the
+ * concrete validation message for the offending request).
+ * @param pointer Hii Retail extension to RFC 7807: a JSON Pointer ({@code RFC 6901}) into the
+ * request body identifying the offending property. {@code null} when no pointer applies (e.g.
+ * header / query / generic errors).
+ * @param keyword Hii Retail extension to RFC 7807: the JSON Schema (or OpenAPI) validation keyword
+ * that failed, such as {@code "required"}, {@code "type"} or {@code "pattern"}. {@code null}
+ * when the error did not originate from schema validation.
*/
public record ProblemDetail(
String type, String title, int status, String detail, String pointer, String keyword) {
private static final String DEFAULT_TYPE = "about:blank";
+ /**
+ * Builds a {@code ProblemDetail} for a schema/parameter validation failure. Uses {@code
+ * about:blank} as the type, {@code "Bad Request"} as the title, status {@code 400}, and copies
+ * the message, JSON Pointer and failing keyword from the {@link ValidationError}.
+ *
+ * @param e the validation error produced by the request validator
+ * @return a 400 problem document describing the validation failure
+ */
public static ProblemDetail forValidation(ValidationError e) {
return new ProblemDetail(
DEFAULT_TYPE, "Bad Request", 400, e.message(), e.pointer(), e.keyword());
}
+ /**
+ * Builds a {@code ProblemDetail} from a {@link BadRequestException} thrown by application code.
+ * The status is taken from the exception, the title is resolved from a standard HTTP reason
+ * phrase table (falling back to {@code "Bad Request"}), and the optional pointer/keyword are
+ * propagated as Hii Retail extensions when present.
+ *
+ * @param e the bad-request exception raised by the handler chain
+ * @return a problem document mirroring the exception's status, message and optional metadata
+ */
public static ProblemDetail forBadRequest(BadRequestException e) {
return new ProblemDetail(
DEFAULT_TYPE,
diff --git a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java
index f9283dc..22e61a8 100644
--- a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java
+++ b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java
@@ -29,6 +29,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Filter that reads the request body, resolves the OpenAPI operation, validates parameters and
+ * body, and exposes the parsed {@link Request} via {@link DispatchHandler#CURRENT} for downstream
+ * filters and the dispatch handler.
+ */
public final class RequestPreparationFilter extends Filter {
private static final Logger LOG = LoggerFactory.getLogger(RequestPreparationFilter.class);
@@ -42,6 +47,17 @@ public final class RequestPreparationFilter extends Filter {
private final ResponseRenderer renderer;
private final List afterHooks;
+ /**
+ * Creates a new request preparation filter.
+ *
+ * @param spec the parsed OpenAPI spec
+ * @param router routes requests to their {@link Operation}
+ * @param validator validates parameters and request bodies
+ * @param bodyMappers media-type to {@link TypeMapper} registry for parsing request bodies
+ * @param exceptionHandler handles exceptions thrown during request preparation and dispatch
+ * @param renderer renders the {@link Response} produced by the exception handler
+ * @param afterHooks hooks invoked after the response has been produced
+ */
@SuppressWarnings("java:S107")
public RequestPreparationFilter(
Spec spec,
diff --git a/src/main/java/com/retailsvc/http/internal/ResourceSource.java b/src/main/java/com/retailsvc/http/internal/ResourceSource.java
index 3e06f23..d02826b 100644
--- a/src/main/java/com/retailsvc/http/internal/ResourceSource.java
+++ b/src/main/java/com/retailsvc/http/internal/ResourceSource.java
@@ -15,14 +15,42 @@
*/
public sealed interface ResourceSource {
+ /**
+ * Returns the resource's byte length, resolved at construction.
+ *
+ * @return byte length of the resource
+ */
long length();
+ /**
+ * Returns the media type inferred from the resource's file extension.
+ *
+ * @return media type string
+ */
String contentType();
+ /**
+ * Returns a human-readable description used in error messages and logs.
+ *
+ * @return description of the resource
+ */
String describe();
+ /**
+ * Opens a fresh {@link InputStream} for reading the resource bytes.
+ *
+ * @return a new input stream the caller must close
+ * @throws IOException if the resource cannot be opened
+ */
InputStream open() throws IOException;
+ /**
+ * Creates a {@code ResourceSource} backed by a classpath resource.
+ *
+ * @param classpathResource absolute classpath path (must start with {@code /})
+ * @return a fail-fast handle to the resource
+ * @throws IllegalArgumentException if the resource is missing or unreadable
+ */
static ResourceSource ofClasspath(String classpathResource) {
Objects.requireNonNull(classpathResource, "classpathResource");
long length;
@@ -38,6 +66,14 @@ static ResourceSource ofClasspath(String classpathResource) {
return new Classpath(classpathResource, length, contentTypeFor(classpathResource));
}
+ /**
+ * Creates a {@code ResourceSource} backed by a filesystem file.
+ *
+ * @param file path to a regular file
+ * @return a fail-fast handle to the file
+ * @throws IllegalArgumentException if {@code file} is not a regular file or its size cannot be
+ * read
+ */
static ResourceSource ofFile(Path file) {
Objects.requireNonNull(file, "file");
if (!Files.isRegularFile(file)) {
@@ -52,6 +88,13 @@ static ResourceSource ofFile(Path file) {
return new File(file, length, contentTypeFor(file.getFileName().toString()));
}
+ /**
+ * Maps a file extension to a Content-Type string.
+ *
+ * @param path file name or path; only the extension is inspected (case-insensitive)
+ * @return the inferred media type, or {@code application/octet-stream} if the extension is
+ * unknown
+ */
static String contentTypeFor(String path) {
String lower = path.toLowerCase(Locale.ROOT);
if (lower.endsWith(".json")) {
@@ -75,6 +118,13 @@ static String contentTypeFor(String path) {
return "application/octet-stream";
}
+ /**
+ * Classpath-backed {@link ResourceSource}.
+ *
+ * @param path absolute classpath path (must start with {@code /})
+ * @param length pre-resolved byte length
+ * @param contentType media type inferred from the path's extension
+ */
record Classpath(String path, long length, String contentType) implements ResourceSource {
@Override
public InputStream open() throws IOException {
@@ -91,6 +141,13 @@ public String describe() {
}
}
+ /**
+ * Filesystem-backed {@link ResourceSource}.
+ *
+ * @param path file path
+ * @param length pre-resolved byte length
+ * @param contentType media type inferred from the file's extension
+ */
record File(Path path, long length, String contentType) implements ResourceSource {
@Override
public InputStream open() throws IOException {
diff --git a/src/main/java/com/retailsvc/http/internal/ResponseRenderer.java b/src/main/java/com/retailsvc/http/internal/ResponseRenderer.java
index 204935f..ca82fef 100644
--- a/src/main/java/com/retailsvc/http/internal/ResponseRenderer.java
+++ b/src/main/java/com/retailsvc/http/internal/ResponseRenderer.java
@@ -18,10 +18,23 @@ public final class ResponseRenderer {
private final Map mappers;
+ /**
+ * Creates a new renderer.
+ *
+ * @param mappers media-type to {@link TypeMapper} registry used to serialise response bodies
+ */
public ResponseRenderer(Map mappers) {
this.mappers = Map.copyOf(mappers);
}
+ /**
+ * Writes the response to the exchange, serialising the body using the registered mapper for the
+ * response content type.
+ *
+ * @param exchange the HTTP exchange to write to
+ * @param response the response to render
+ * @throws IOException if writing to the exchange fails
+ */
public void render(HttpExchange exchange, Response response) throws IOException {
try (exchange) {
Headers headers = exchange.getResponseHeaders();
diff --git a/src/main/java/com/retailsvc/http/internal/Router.java b/src/main/java/com/retailsvc/http/internal/Router.java
index 7c44796..70ba164 100644
--- a/src/main/java/com/retailsvc/http/internal/Router.java
+++ b/src/main/java/com/retailsvc/http/internal/Router.java
@@ -11,13 +11,25 @@
import java.util.Optional;
import java.util.Set;
+/** Resolves an OpenAPI {@link Operation} from an HTTP method and request path. */
public final class Router {
+ /**
+ * Successful route match.
+ *
+ * @param operation the matched OpenAPI operation
+ * @param pathParameters extracted path-template variables (empty for exact matches)
+ */
public record Match(Operation operation, Map pathParameters) {}
private final Map> exact = new EnumMap<>(HttpMethod.class);
private final Map> templated = new EnumMap<>(HttpMethod.class);
+ /**
+ * Indexes operations by method, splitting exact paths from templated paths for fast lookup.
+ *
+ * @param operations the operations declared in the OpenAPI spec
+ */
public Router(List operations) {
for (HttpMethod m : HttpMethod.values()) {
exact.put(m, new LinkedHashMap<>());
@@ -32,6 +44,13 @@ public Router(List operations) {
}
}
+ /**
+ * Finds the operation matching the given method and path.
+ *
+ * @param method the HTTP method
+ * @param path the request path with the base path stripped
+ * @return the matching operation and any extracted path parameters, or empty if none matches
+ */
public Optional match(HttpMethod method, String path) {
Operation hit = exact.get(method).get(path);
if (hit != null) {
@@ -46,6 +65,13 @@ public Optional match(HttpMethod method, String path) {
return Optional.empty();
}
+ /**
+ * Returns the set of HTTP methods that have an operation for the given path. Used to build the
+ * {@code Allow} header on 405 responses.
+ *
+ * @param path the request path with the base path stripped
+ * @return the methods declared for that path; empty if the path is unknown
+ */
public Set allowedMethods(String path) {
EnumSet out = EnumSet.noneOf(HttpMethod.class);
for (HttpMethod m : HttpMethod.values()) {
diff --git a/src/main/java/com/retailsvc/http/internal/SecurityFilter.java b/src/main/java/com/retailsvc/http/internal/SecurityFilter.java
index 22bd1d7..a3d7ac0 100644
--- a/src/main/java/com/retailsvc/http/internal/SecurityFilter.java
+++ b/src/main/java/com/retailsvc/http/internal/SecurityFilter.java
@@ -22,6 +22,11 @@
import java.util.Objects;
import java.util.Optional;
+/**
+ * Filter that enforces OpenAPI security requirements. Extracts credentials from the exchange,
+ * delegates validation to a per-scheme {@link SchemeValidator}, and renders an RFC 7807 problem
+ * response with appropriate {@code WWW-Authenticate} challenges on rejection.
+ */
public final class SecurityFilter extends Filter {
private final Map operationsById;
@@ -31,6 +36,17 @@ public final class SecurityFilter extends Filter {
private final boolean externalAuth;
private final TypeMapper jsonMapper;
+ /**
+ * Creates a new security filter.
+ *
+ * @param operationsById all operations indexed by {@code operationId}
+ * @param schemes named security schemes declared in the OpenAPI spec
+ * @param rootSecurity the spec-level security requirements applied when an operation does not
+ * declare its own
+ * @param validators per-scheme credential validators keyed by scheme name
+ * @param externalAuth if {@code true}, skip enforcement and delegate to a fronting authenticator
+ * @param jsonMapper mapper used to serialise the problem-details response body
+ */
public SecurityFilter(
Map operationsById,
Map schemes,
diff --git a/src/main/java/com/retailsvc/http/internal/TextPlainParser.java b/src/main/java/com/retailsvc/http/internal/TextPlainParser.java
index bcf5b6e..7d5e4fe 100644
--- a/src/main/java/com/retailsvc/http/internal/TextPlainParser.java
+++ b/src/main/java/com/retailsvc/http/internal/TextPlainParser.java
@@ -8,6 +8,18 @@
/** Decodes a text/plain request body using the charset declared on {@code Content-Type}. */
public final class TextPlainParser {
+ /** Creates a new parser. */
+ public TextPlainParser() {
+ // Stateless; nothing to initialise.
+ }
+
+ /**
+ * Decodes the body as text using the charset declared on {@code Content-Type} (default UTF-8).
+ *
+ * @param body raw request body bytes
+ * @param contentTypeHeader the request {@code Content-Type} header; may be {@code null}
+ * @return the decoded string
+ */
public String parse(byte[] body, String contentTypeHeader) {
Charset charset = resolveCharset(contentTypeHeader);
return new String(body, charset);
diff --git a/src/main/java/com/retailsvc/http/internal/TextTypeMapper.java b/src/main/java/com/retailsvc/http/internal/TextTypeMapper.java
index a6b58d6..c9c154e 100644
--- a/src/main/java/com/retailsvc/http/internal/TextTypeMapper.java
+++ b/src/main/java/com/retailsvc/http/internal/TextTypeMapper.java
@@ -11,6 +11,11 @@
*/
public final class TextTypeMapper implements TypeMapper {
+ /** Creates a new mapper. */
+ public TextTypeMapper() {
+ // State is held by the final parser field below.
+ }
+
private final TextPlainParser parser = new TextPlainParser();
@Override
diff --git a/src/main/java/com/retailsvc/http/internal/ValueCoercion.java b/src/main/java/com/retailsvc/http/internal/ValueCoercion.java
index 1b98491..3688804 100644
--- a/src/main/java/com/retailsvc/http/internal/ValueCoercion.java
+++ b/src/main/java/com/retailsvc/http/internal/ValueCoercion.java
@@ -12,6 +12,15 @@ public final class ValueCoercion {
private ValueCoercion() {}
+ /**
+ * Coerces a wire-format string to the Java type implied by the target schema.
+ *
+ * @param raw the raw string value (e.g. a query parameter or form field)
+ * @param schema the target schema
+ * @param pointer JSON pointer used in validation errors
+ * @return the coerced value ({@code Long}, {@code Double}, {@code Boolean}, or the raw string)
+ * @throws ValidationException if the value cannot be coerced to the schema type
+ */
public static Object coerce(String raw, Schema schema, String pointer) {
return switch (schema) {
case IntegerSchema _ -> {
diff --git a/src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java b/src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java
index 1deabdd..9c4c490 100644
--- a/src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java
+++ b/src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java
@@ -47,15 +47,25 @@ public final class GsonJsonMapper implements TypedTypeMapper {
private final Gson gson;
+ /** Creates a mapper backed by a default {@link Gson} instance with JSR-310 adapters. */
public GsonJsonMapper() {
this(defaultGson());
}
+ /**
+ * Creates a mapper backed by the supplied {@link Gson} instance.
+ *
+ * @param gson the Gson instance to delegate to; must not be {@code null}
+ */
public GsonJsonMapper(Gson gson) {
this.gson = Objects.requireNonNull(gson, "gson must not be null");
}
- /** Returns the wrapped {@link Gson} instance. */
+ /**
+ * Returns the wrapped {@link Gson} instance.
+ *
+ * @return the wrapped {@link Gson} instance
+ */
public Gson gson() {
return gson;
}
diff --git a/src/main/java/com/retailsvc/http/spec/HttpMethod.java b/src/main/java/com/retailsvc/http/spec/HttpMethod.java
index 920b48f..ea97652 100644
--- a/src/main/java/com/retailsvc/http/spec/HttpMethod.java
+++ b/src/main/java/com/retailsvc/http/spec/HttpMethod.java
@@ -2,17 +2,33 @@
import java.util.Locale;
+/** HTTP request methods supported by OpenAPI path operations. */
public enum HttpMethod {
+ /** HTTP {@code GET} method. */
GET,
+ /** HTTP {@code POST} method. */
POST,
+ /** HTTP {@code PUT} method. */
PUT,
+ /** HTTP {@code DELETE} method. */
DELETE,
+ /** HTTP {@code PATCH} method. */
PATCH,
+ /** HTTP {@code HEAD} method. */
HEAD,
+ /** HTTP {@code OPTIONS} method. */
OPTIONS,
+ /** HTTP {@code TRACE} method. */
TRACE,
+ /** HTTP {@code CONNECT} method. */
CONNECT;
+ /**
+ * Parses the given string into an {@link HttpMethod}, case-insensitively.
+ *
+ * @param s the method name
+ * @return the matching {@link HttpMethod}
+ */
public static HttpMethod parse(String s) {
return HttpMethod.valueOf(s.toUpperCase(Locale.ROOT));
}
diff --git a/src/main/java/com/retailsvc/http/spec/Info.java b/src/main/java/com/retailsvc/http/spec/Info.java
index ed97d90..27b42dc 100644
--- a/src/main/java/com/retailsvc/http/spec/Info.java
+++ b/src/main/java/com/retailsvc/http/spec/Info.java
@@ -2,4 +2,11 @@
import java.util.Map;
+/**
+ * OpenAPI {@code info} object describing the API's identity.
+ *
+ * @param title human-readable API title
+ * @param version API version string
+ * @param extensions OpenAPI {@code x-*} specification extensions
+ */
public record Info(String title, String version, Map extensions) {}
diff --git a/src/main/java/com/retailsvc/http/spec/MediaType.java b/src/main/java/com/retailsvc/http/spec/MediaType.java
index 4944aea..e207ad5 100644
--- a/src/main/java/com/retailsvc/http/spec/MediaType.java
+++ b/src/main/java/com/retailsvc/http/spec/MediaType.java
@@ -2,4 +2,9 @@
import com.retailsvc.http.spec.schema.Schema;
+/**
+ * OpenAPI {@code mediaType} object binding a content type to a payload schema.
+ *
+ * @param schema schema describing the payload for this media type
+ */
public record MediaType(Schema schema) {}
diff --git a/src/main/java/com/retailsvc/http/spec/Operation.java b/src/main/java/com/retailsvc/http/spec/Operation.java
index 912c802..d848463 100644
--- a/src/main/java/com/retailsvc/http/spec/Operation.java
+++ b/src/main/java/com/retailsvc/http/spec/Operation.java
@@ -5,6 +5,19 @@
import java.util.Map;
import java.util.Optional;
+/**
+ * Resolved OpenAPI operation: a single (method, path) pair with its validation metadata, used by
+ * the router and dispatcher to route requests to user-supplied handlers.
+ *
+ * @param operationId unique OpenAPI {@code operationId} identifying this operation
+ * @param method HTTP method for this operation
+ * @param path path template (possibly containing {@code {var}} placeholders)
+ * @param requestBody request body definition, if any
+ * @param parameters declared path, query, header and cookie parameters
+ * @param responses response definitions keyed by status code (or {@code default})
+ * @param extensions OpenAPI {@code x-*} specification extensions
+ * @param security security requirements that override the document-level security, if present
+ */
public record Operation(
String operationId,
HttpMethod method,
diff --git a/src/main/java/com/retailsvc/http/spec/Parameter.java b/src/main/java/com/retailsvc/http/spec/Parameter.java
index 3fba8c9..7d71bd6 100644
--- a/src/main/java/com/retailsvc/http/spec/Parameter.java
+++ b/src/main/java/com/retailsvc/http/spec/Parameter.java
@@ -3,16 +3,38 @@
import com.retailsvc.http.spec.schema.Schema;
import java.util.Locale;
+/**
+ * OpenAPI {@code parameter} object describing a single path, query, header or cookie parameter.
+ *
+ * @param name parameter name as it appears on the wire
+ * @param in where the parameter is carried in the request
+ * @param required whether the parameter must be present
+ * @param schema schema used to validate the parameter value
+ * @param pointer JSON Pointer-style location used in validation error messages
+ */
public record Parameter(String name, Location in, boolean required, Schema schema, String pointer) {
+ /**
+ * Convenience constructor that derives {@code pointer} from {@code in} and {@code name}.
+ *
+ * @param name parameter name as it appears on the wire
+ * @param in where the parameter is carried in the request
+ * @param required whether the parameter must be present
+ * @param schema schema used to validate the parameter value
+ */
public Parameter(String name, Location in, boolean required, Schema schema) {
this(name, in, required, schema, "/" + in.name().toLowerCase(Locale.ROOT) + "/" + name);
}
+ /** Where in an HTTP request a {@link Parameter} is carried. */
public enum Location {
+ /** Path parameter, substituted into a templated URL segment. */
PATH,
+ /** Query string parameter. */
QUERY,
+ /** Request header. */
HEADER,
+ /** Cookie value. */
COOKIE
}
}
diff --git a/src/main/java/com/retailsvc/http/spec/PathTemplate.java b/src/main/java/com/retailsvc/http/spec/PathTemplate.java
index 1430e15..88c7138 100644
--- a/src/main/java/com/retailsvc/http/spec/PathTemplate.java
+++ b/src/main/java/com/retailsvc/http/spec/PathTemplate.java
@@ -8,10 +8,23 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+/**
+ * A compiled OpenAPI path template that matches request paths and extracts named path parameters.
+ *
+ * @param raw the original template string, e.g. {@code /pets/{id}}
+ * @param compiled the regex compiled from {@code raw}
+ * @param parameterNames the parameter names in the order they appear in {@code raw}
+ */
public record PathTemplate(String raw, Pattern compiled, List parameterNames) {
private static final Pattern TOKEN = Pattern.compile("\\{([^/}]+)}");
+ /**
+ * Compiles a path template such as {@code /pets/{id}} into a {@link PathTemplate}.
+ *
+ * @param template the OpenAPI path template
+ * @return the compiled template
+ */
public static PathTemplate compile(String template) {
StringBuilder regex = new StringBuilder("^");
List names = new ArrayList<>();
@@ -28,6 +41,13 @@ public static PathTemplate compile(String template) {
return new PathTemplate(template, Pattern.compile(regex.toString()), List.copyOf(names));
}
+ /**
+ * Matches a concrete request path against this template.
+ *
+ * @param path the request path to match
+ * @return the extracted parameter values keyed by name, or {@link Optional#empty()} if {@code
+ * path} does not match this template
+ */
public Optional> match(String path) {
Matcher m = compiled.matcher(path);
if (!m.matches()) {
diff --git a/src/main/java/com/retailsvc/http/spec/RequestBody.java b/src/main/java/com/retailsvc/http/spec/RequestBody.java
index eae9f28..40d4581 100644
--- a/src/main/java/com/retailsvc/http/spec/RequestBody.java
+++ b/src/main/java/com/retailsvc/http/spec/RequestBody.java
@@ -2,4 +2,10 @@
import java.util.Map;
+/**
+ * OpenAPI {@code requestBody} object describing the payload an operation accepts.
+ *
+ * @param required whether the request body must be present
+ * @param content supported media types keyed by content-type string
+ */
public record RequestBody(boolean required, Map content) {}
diff --git a/src/main/java/com/retailsvc/http/spec/Response.java b/src/main/java/com/retailsvc/http/spec/Response.java
index b01a381..47da35a 100644
--- a/src/main/java/com/retailsvc/http/spec/Response.java
+++ b/src/main/java/com/retailsvc/http/spec/Response.java
@@ -2,4 +2,9 @@
import java.util.Map;
+/**
+ * OpenAPI {@code response} object describing the payload an operation returns for a given status.
+ *
+ * @param content supported media types keyed by content-type string
+ */
public record Response(Map content) {}
diff --git a/src/main/java/com/retailsvc/http/spec/Server.java b/src/main/java/com/retailsvc/http/spec/Server.java
index e5de557..e8d0caa 100644
--- a/src/main/java/com/retailsvc/http/spec/Server.java
+++ b/src/main/java/com/retailsvc/http/spec/Server.java
@@ -2,7 +2,18 @@
import java.net.URI;
+/**
+ * OpenAPI {@code server} entry. Only the {@code url} is retained; the first server's path becomes
+ * the HTTP context root.
+ *
+ * @param url full server URL as declared in the OpenAPI document
+ */
public record Server(String url) {
+ /**
+ * Returns the path component of {@link #url()}, used as the HTTP context root.
+ *
+ * @return the URL's path component
+ */
public String basePath() {
return URI.create(url).getPath();
}
diff --git a/src/main/java/com/retailsvc/http/spec/Spec.java b/src/main/java/com/retailsvc/http/spec/Spec.java
index 4f76fc3..be64b6e 100644
--- a/src/main/java/com/retailsvc/http/spec/Spec.java
+++ b/src/main/java/com/retailsvc/http/spec/Spec.java
@@ -22,6 +22,22 @@
import java.util.Optional;
import java.util.function.Function;
+/**
+ * Parsed OpenAPI 3.1 specification used to drive request routing and validation.
+ *
+ * @param openapi OpenAPI document version (e.g. {@code "3.1.0"})
+ * @param info top-level {@code info} object
+ * @param servers list of declared servers; the first one's path supplies {@link #basePath()}
+ * @param operations all operations across paths, in declaration order
+ * @param componentSchemas inline {@code components.schemas} map (raw name → schema)
+ * @param componentParameters inline {@code components.parameters} map (raw name → parameter)
+ * @param basePath URL path prefix derived from {@code servers[0].url}
+ * @param schemaRefIndex {@code $ref}-keyed view of {@code componentSchemas} for ref resolution
+ * @param parameterRefIndex {@code $ref}-keyed view of {@code componentParameters}
+ * @param extensions top-level {@code x-} extension keywords
+ * @param securitySchemes inline {@code components.securitySchemes} map
+ * @param security top-level security requirements applied to operations without their own
+ */
public record Spec(
String openapi,
Info info,
@@ -66,6 +82,8 @@ static Map extractExtensions(Map raw) {
* file's extension is not present, throws {@link IllegalStateException} — register your own
* parser and call {@link #from(Map)} instead.
*
+ * @param path filesystem path to the OpenAPI spec file
+ * @return the parsed specification
* @throws UncheckedIOException if the file cannot be read
* @throws IllegalStateException if the required parser is not on the classpath, or if the file
* has an unrecognised extension
@@ -109,6 +127,8 @@ public static Spec fromPath(Path path) {
* To avoid the Gson dependency (e.g. when using Jackson), use {@link #fromJson(InputStream,
* Function)} instead.
*
+ * @param in stream containing the JSON OpenAPI spec
+ * @return the parsed specification
* @throws NullPointerException if {@code in} is {@code null}
* @throws UncheckedIOException if the stream cannot be read
* @throws IllegalStateException if Gson is not on the classpath
@@ -129,6 +149,9 @@ public static Spec fromJson(InputStream in) {
* Spec spec = Spec.fromJson(in, bytes -> mapper.readValue(bytes, Map.class));
* }
*
+ * @param in stream containing the JSON OpenAPI spec
+ * @param parser function that decodes the raw bytes into a map
+ * @return the parsed specification
* @throws NullPointerException if {@code in} or {@code parser} is {@code null}
* @throws UncheckedIOException if the stream cannot be read
*/
@@ -142,6 +165,8 @@ public static Spec fromJson(InputStream in, Function
* classpath; otherwise throws {@link IllegalStateException}. The stream is fully consumed and
* closed before this method returns.
*
+ * @param in stream containing the YAML OpenAPI spec
+ * @return the parsed specification
* @throws NullPointerException if {@code in} is {@code null}
* @throws UncheckedIOException if the stream cannot be read
* @throws IllegalStateException if SnakeYAML is not on the classpath
@@ -205,6 +230,12 @@ private static Class> loadOptional(String className, String format, String lib
}
}
+ /**
+ * Builds a {@code Spec} from a pre-parsed OpenAPI document.
+ *
+ * @param raw map produced by decoding the JSON or YAML spec
+ * @return the parsed specification
+ */
@SuppressWarnings("unchecked")
public static Spec from(Map raw) {
String openapi = (String) raw.get("openapi");
@@ -257,6 +288,13 @@ private static Map indexByRef(Map components, String p
return Map.copyOf(out);
}
+ /**
+ * Resolves a schema by its {@code $ref} string.
+ *
+ * @param ref full {@code $ref} value, e.g. {@code #/components/schemas/Pet}
+ * @return the referenced schema
+ * @throws IllegalArgumentException if no schema is registered under {@code ref}
+ */
public Schema resolveSchema(String ref) {
Schema s = schemaRefIndex.get(ref);
if (s == null) {
@@ -265,6 +303,13 @@ public Schema resolveSchema(String ref) {
return s;
}
+ /**
+ * Resolves a parameter by its {@code $ref} string.
+ *
+ * @param ref full {@code $ref} value, e.g. {@code #/components/parameters/PetId}
+ * @return the referenced parameter
+ * @throws IllegalArgumentException if no parameter is registered under {@code ref}
+ */
public Parameter resolveParameter(String ref) {
Parameter p = parameterRefIndex.get(ref);
if (p == null) {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/AdditionalProperties.java b/src/main/java/com/retailsvc/http/spec/schema/AdditionalProperties.java
index d2b8d81..cfced50 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/AdditionalProperties.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/AdditionalProperties.java
@@ -1,9 +1,43 @@
package com.retailsvc.http.spec.schema;
+/**
+ * Models the OpenAPI / JSON Schema {@code additionalProperties} keyword, which controls whether
+ * properties not listed in an object schema's {@code properties} map are permitted, and if so,
+ * whether they must conform to a constraint schema.
+ *
+ * This sealed interface has three variants that map directly to the three forms the keyword can
+ * take in a schema document:
+ *
+ *
+ * {@link Allowed} — {@code additionalProperties: true} or omitted entirely.
+ * {@link Forbidden} — {@code additionalProperties: false}.
+ * {@link SchemaConstraint} — {@code additionalProperties: { ...schema... }}.
+ *
+ *
+ * @see JSON
+ * Schema 2020-12 — additionalProperties
+ */
public sealed interface AdditionalProperties {
+ /**
+ * Matches {@code additionalProperties: true} or the absence of the keyword (the JSON Schema
+ * default). Any extra property of any type is accepted during validation without further
+ * constraint.
+ */
record Allowed() implements AdditionalProperties {}
+ /**
+ * Matches {@code additionalProperties: false}. Any property present on an instance that is not
+ * declared in the schema's {@code properties} map causes validation to fail.
+ */
record Forbidden() implements AdditionalProperties {}
+ /**
+ * Matches {@code additionalProperties: {schema}}. Extra properties not declared in {@code
+ * properties} are permitted, but each such property's value must validate against the supplied
+ * constraint schema.
+ *
+ * @param schema the schema that every additional (undeclared) property value must satisfy
+ */
record SchemaConstraint(Schema schema) implements AdditionalProperties {}
}
diff --git a/src/main/java/com/retailsvc/http/spec/schema/AllOfSchema.java b/src/main/java/com/retailsvc/http/spec/schema/AllOfSchema.java
index 761e75a..30daf5c 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/AllOfSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/AllOfSchema.java
@@ -4,6 +4,20 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Schema composition for the JSON Schema {@code allOf} keyword.
+ *
+ * A value is valid against this schema if and only if it validates successfully against every
+ * subschema in {@link #parts()}. The composition is conjunctive: each part contributes its own
+ * constraints, and all must hold simultaneously.
+ *
+ *
{@link #types()} returns an empty set because {@code allOf} is itself type-agnostic — it does
+ * not declare a JSON type. The validator descends into each part and lets the parts assert any type
+ * constraints they carry.
+ *
+ * @param parts the subschemas that the value must all match
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node
+ */
public record AllOfSchema(List parts, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/AlwaysSchema.java b/src/main/java/com/retailsvc/http/spec/schema/AlwaysSchema.java
index 1077b7d..e35fbe6 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/AlwaysSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/AlwaysSchema.java
@@ -3,6 +3,20 @@
import java.util.Map;
import java.util.Set;
+/**
+ * The JSON Schema boolean schema {@code true} — accepts any value without constraint.
+ *
+ * In JSON Schema (and OpenAPI 3.1), a schema may be expressed as the literal boolean {@code
+ * true}, which is equivalent to an empty object schema {@code {}} and validates successfully
+ * against every instance. This record models that form. Its counterpart is {@link NeverSchema} (the
+ * boolean schema {@code false}), which rejects every instance.
+ *
+ *
Because no type constraint applies to an "always" schema, {@link #types()} returns an empty
+ * set.
+ *
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node, keyed by
+ * extension name (including the {@code x-} prefix) with their raw parsed values.
+ */
public record AlwaysSchema(Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/AnyOfSchema.java b/src/main/java/com/retailsvc/http/spec/schema/AnyOfSchema.java
index b2fe66a..0bb41a7 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/AnyOfSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/AnyOfSchema.java
@@ -4,6 +4,24 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Models the JSON Schema {@code anyOf} composition keyword: a value is valid when it validates
+ * against at least one of the listed option subschemas.
+ *
+ * Contrast with the sibling composition keywords:
+ *
+ *
+ * {@link OneOfSchema} — the value must validate against exactly one option.
+ * {@link AllOfSchema} — the value must validate against every option.
+ *
+ *
+ * {@link #types()} returns an empty set because an {@code anyOf} node does not itself constrain
+ * a JSON type; the validator descends into each option and lets the matching branch determine the
+ * effective type.
+ *
+ * @param options the candidate subschemas; at least one must match for the value to be valid
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node
+ */
public record AnyOfSchema(List options, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/ArraySchema.java b/src/main/java/com/retailsvc/http/spec/schema/ArraySchema.java
index fcd1805..6bd519e 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/ArraySchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/ArraySchema.java
@@ -3,6 +3,21 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Parsed JSON Schema node with {@code type: array}.
+ *
+ * Represents an array schema as defined by OpenAPI 3.1 / JSON Schema 2020-12. Instances are
+ * produced by the spec parser and consumed by the validator to enforce array-shaped payloads:
+ * element schema, cardinality bounds, and uniqueness.
+ *
+ * @param types the JSON Schema {@code type} set; typically the singleton {@code [ARRAY]}, or {@code
+ * [ARRAY, NULL]} when the schema is nullable
+ * @param items the schema applied to every element of the array
+ * @param minItems minimum number of elements; {@code null} means no lower bound
+ * @param maxItems maximum number of elements; {@code null} means no upper bound
+ * @param uniqueItems whether duplicate elements are rejected
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node
+ */
public record ArraySchema(
Set types,
Schema items,
diff --git a/src/main/java/com/retailsvc/http/spec/schema/BooleanSchema.java b/src/main/java/com/retailsvc/http/spec/schema/BooleanSchema.java
index 7a4e310..9e7aa23 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/BooleanSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/BooleanSchema.java
@@ -3,5 +3,19 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Parsed JSON Schema {@code type: boolean} node.
+ *
+ * A boolean schema accepts the JSON literals {@code true} or {@code false}. It carries no
+ * additional constraints beyond the type itself, so validation succeeds for any boolean value and
+ * fails for any other JSON kind.
+ *
+ * @param types the declared JSON Schema types for this node. Should be a singleton {@code
+ * [BOOLEAN]}, or include {@link TypeName#NULL} alongside {@code BOOLEAN} when the node is
+ * nullable (e.g. {@code type: [boolean, "null"]}).
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node, keyed by
+ * their full extension name (including the {@code x-} prefix). Never {@code null}; empty when
+ * no extensions are present.
+ */
public record BooleanSchema(Set types, Map extensions)
implements Schema {}
diff --git a/src/main/java/com/retailsvc/http/spec/schema/ConstSchema.java b/src/main/java/com/retailsvc/http/spec/schema/ConstSchema.java
index 1076a0e..7867bf6 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/ConstSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/ConstSchema.java
@@ -3,6 +3,15 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Models the JSON Schema {@code const} keyword: an instance is valid if and only if its value is
+ * deeply equal to {@link #value()} (compared via {@link java.util.Objects#equals(Object, Object)}).
+ *
+ * {@link #types()} returns an empty set because the type is implied by the const value itself.
+ *
+ * @param value the required value; may be {@code null} to require a JSON {@code null}
+ * @param extensions OpenAPI {@code x-} extension keywords declared on this schema node
+ */
public record ConstSchema(Object value, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/EnumSchema.java b/src/main/java/com/retailsvc/http/spec/schema/EnumSchema.java
index fe866af..31a10e8 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/EnumSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/EnumSchema.java
@@ -4,6 +4,20 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Models a JSON Schema {@code enum} constraint: an instance is valid only if it is deeply equal
+ * (via {@link java.util.Objects#equals(Object, Object)}) to one of the allowed {@link #values()}.
+ *
+ * {@link #types()} returns an empty set; the permitted type is implied by the enum values
+ * themselves rather than declared via a {@code type} keyword.
+ *
+ *
Note: most string enums in OpenAPI are modelled as {@link StringSchema} with {@code
+ * enumValues()} populated. This {@code EnumSchema} covers the case where {@code enum} appears
+ * without an explicit {@code type} keyword.
+ *
+ * @param values the permitted values (any JSON-mappable Java type)
+ * @param extensions OpenAPI {@code x-} extension keywords on this schema node
+ */
public record EnumSchema(List values, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/IntegerSchema.java b/src/main/java/com/retailsvc/http/spec/schema/IntegerSchema.java
index d088f4c..0f8d6ab 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/IntegerSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/IntegerSchema.java
@@ -3,6 +3,18 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code type: integer}.
+ *
+ * @param types the declared types (may include {@link TypeName#NULL} for nullable)
+ * @param minimum inclusive lower bound, or {@code null}
+ * @param maximum inclusive upper bound, or {@code null}
+ * @param exclusiveMinimum exclusive lower bound, or {@code null}
+ * @param exclusiveMaximum exclusive upper bound, or {@code null}
+ * @param multipleOf required divisor, or {@code null}
+ * @param format optional format hint (e.g. {@code int32}, {@code int64})
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record IntegerSchema(
Set types,
Long minimum,
diff --git a/src/main/java/com/retailsvc/http/spec/schema/NeverSchema.java b/src/main/java/com/retailsvc/http/spec/schema/NeverSchema.java
index 6600e24..66a1421 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/NeverSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/NeverSchema.java
@@ -3,6 +3,12 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema boolean {@code false}: accepts nothing. Pairs with {@link AlwaysSchema} (boolean
+ * {@code true}).
+ *
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record NeverSchema(Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/NotSchema.java b/src/main/java/com/retailsvc/http/spec/schema/NotSchema.java
index 8c99e8a..33de9a0 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/NotSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/NotSchema.java
@@ -3,6 +3,12 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code not}: the value must NOT match the inner schema.
+ *
+ * @param schema the schema that the value must fail to validate against
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record NotSchema(Schema schema, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/NullSchema.java b/src/main/java/com/retailsvc/http/spec/schema/NullSchema.java
index c2585ac..be1628f 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/NullSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/NullSchema.java
@@ -3,6 +3,11 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code type: null}: only accepts the JSON {@code null} value.
+ *
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record NullSchema(Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/NumberSchema.java b/src/main/java/com/retailsvc/http/spec/schema/NumberSchema.java
index af1008b..e1e20f8 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/NumberSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/NumberSchema.java
@@ -3,6 +3,18 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code type: number}.
+ *
+ * @param types the declared types (may include {@link TypeName#NULL} for nullable)
+ * @param minimum inclusive lower bound, or {@code null}
+ * @param maximum inclusive upper bound, or {@code null}
+ * @param exclusiveMinimum exclusive lower bound, or {@code null}
+ * @param exclusiveMaximum exclusive upper bound, or {@code null}
+ * @param multipleOf required divisor, or {@code null}
+ * @param format optional format hint (e.g. {@code float}, {@code double})
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record NumberSchema(
Set types,
Number minimum,
diff --git a/src/main/java/com/retailsvc/http/spec/schema/ObjectSchema.java b/src/main/java/com/retailsvc/http/spec/schema/ObjectSchema.java
index e2481e2..260ab44 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/ObjectSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/ObjectSchema.java
@@ -4,6 +4,17 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code type: object}.
+ *
+ * @param types the declared types (may include {@link TypeName#NULL} for nullable)
+ * @param properties declared property name to schema mapping
+ * @param required names of required properties
+ * @param additionalProperties policy for properties not declared in {@code properties}
+ * @param minProperties minimum property count, or {@code null}
+ * @param maxProperties maximum property count, or {@code null}
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record ObjectSchema(
Set types,
Map properties,
diff --git a/src/main/java/com/retailsvc/http/spec/schema/OneOfSchema.java b/src/main/java/com/retailsvc/http/spec/schema/OneOfSchema.java
index 07a28ac..d8de4dd 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/OneOfSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/OneOfSchema.java
@@ -4,6 +4,12 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code oneOf}: exactly one branch must match.
+ *
+ * @param options the candidate schemas
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record OneOfSchema(List options, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/RefSchema.java b/src/main/java/com/retailsvc/http/spec/schema/RefSchema.java
index c2b8b56..52dcb6a 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/RefSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/RefSchema.java
@@ -3,6 +3,12 @@
import java.util.Map;
import java.util.Set;
+/**
+ * A {@code $ref} to another schema. Resolved lazily by the validator.
+ *
+ * @param pointer the {@code $ref} pointer (e.g. {@code #/components/schemas/Foo})
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record RefSchema(String pointer, Map extensions) implements Schema {
@Override
public Set types() {
diff --git a/src/main/java/com/retailsvc/http/spec/schema/Schema.java b/src/main/java/com/retailsvc/http/spec/schema/Schema.java
index 46f1c3d..693d183 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/Schema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/Schema.java
@@ -3,6 +3,18 @@
import java.util.Map;
import java.util.Set;
+/**
+ * Root of the parsed OpenAPI / JSON Schema AST.
+ *
+ * This sealed interface has one variant per JSON Schema construct: {@link StringSchema}, {@link
+ * NumberSchema}, {@link IntegerSchema}, {@link BooleanSchema}, {@link ObjectSchema}, {@link
+ * ArraySchema}, {@link NullSchema}, {@link RefSchema}, {@link OneOfSchema}, {@link AnyOfSchema},
+ * {@link AllOfSchema}, {@link NotSchema}, {@link ConstSchema}, {@link EnumSchema}, {@link
+ * AlwaysSchema}, and {@link NeverSchema}.
+ *
+ *
Pattern-match against the variants in a {@code switch} to dispatch over the schema kind
+ * without resorting to {@code instanceof} chains.
+ */
public sealed interface Schema
permits StringSchema,
NumberSchema,
@@ -20,7 +32,19 @@ public sealed interface Schema
EnumSchema,
AlwaysSchema,
NeverSchema {
+ /**
+ * The JSON Schema {@code type} set for this node. Empty for combinator schemas ({@code allOf} /
+ * {@code anyOf} / {@code oneOf} / {@code not} / {@code const} / {@code enum} / {@code always} /
+ * {@code never}) where the type is derived from sub-schemas or is not applicable.
+ *
+ * @return the declared types
+ */
Set types();
+ /**
+ * OpenAPI {@code x-}-prefixed extension keywords kept verbatim.
+ *
+ * @return immutable map of extension name to raw value
+ */
Map extensions();
}
diff --git a/src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java b/src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java
index 3227a1f..bad4d88 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java
@@ -7,6 +7,7 @@
import java.util.Map;
import java.util.Set;
+/** Parses raw OpenAPI / JSON Schema input ({@code Map} or {@code Boolean}) into {@link Schema}. */
public final class SchemaParser {
private SchemaParser() {}
@@ -22,6 +23,13 @@ static Map extractExtensions(Map raw) {
return Map.copyOf(out);
}
+ /**
+ * Parses a raw schema node into a {@link Schema}.
+ *
+ * @param raw a {@link Boolean} (JSON Schema shorthand) or a {@link Map} of schema keywords
+ * @return the parsed schema
+ * @throws IllegalArgumentException if {@code raw} is neither a boolean nor a map
+ */
public static Schema parse(Object raw) {
if (raw instanceof Boolean b) {
boolean allow = b;
diff --git a/src/main/java/com/retailsvc/http/spec/schema/StringSchema.java b/src/main/java/com/retailsvc/http/spec/schema/StringSchema.java
index e022b9b..bd05c41 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/StringSchema.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/StringSchema.java
@@ -4,6 +4,17 @@
import java.util.Map;
import java.util.Set;
+/**
+ * JSON Schema {@code type: string}.
+ *
+ * @param types the declared types (may include {@link TypeName#NULL} for nullable)
+ * @param pattern regex the value must match, or {@code null}
+ * @param minLength minimum length, or {@code null}
+ * @param maxLength maximum length, or {@code null}
+ * @param format optional format hint (e.g. {@code date-time}, {@code uuid})
+ * @param enumValues allowed values, or {@code null} if unconstrained
+ * @param extensions vendor extensions ({@code x-*} keys)
+ */
public record StringSchema(
Set types,
String pattern,
diff --git a/src/main/java/com/retailsvc/http/spec/schema/TypeName.java b/src/main/java/com/retailsvc/http/spec/schema/TypeName.java
index ab20c98..7a12cdb 100644
--- a/src/main/java/com/retailsvc/http/spec/schema/TypeName.java
+++ b/src/main/java/com/retailsvc/http/spec/schema/TypeName.java
@@ -1,14 +1,29 @@
package com.retailsvc.http.spec.schema;
+/** The seven standard JSON Schema primitive types. */
public enum TypeName {
+ /** JSON string. */
STRING,
+ /** JSON number (any numeric value). */
NUMBER,
+ /** JSON integer (a number with no fractional part). */
INTEGER,
+ /** JSON boolean ({@code true} or {@code false}). */
BOOLEAN,
+ /** JSON object. */
OBJECT,
+ /** JSON array. */
ARRAY,
+ /** JSON null. */
NULL;
+ /**
+ * Maps a JSON Schema {@code type} string to the matching {@link TypeName}.
+ *
+ * @param name lowercase JSON Schema type name (e.g. {@code "string"})
+ * @return the corresponding constant
+ * @throws IllegalArgumentException if {@code name} is not a known JSON Schema type
+ */
public static TypeName fromJsonSchema(String name) {
return switch (name) {
case "string" -> STRING;
diff --git a/src/main/java/com/retailsvc/http/spec/security/SecurityRequirement.java b/src/main/java/com/retailsvc/http/spec/security/SecurityRequirement.java
index c7dd140..7459312 100644
--- a/src/main/java/com/retailsvc/http/spec/security/SecurityRequirement.java
+++ b/src/main/java/com/retailsvc/http/spec/security/SecurityRequirement.java
@@ -6,8 +6,14 @@
/**
* One OR-branch in a {@code security} list. Each entry in {@link #schemes} is AND-ed: every scheme
* name must be satisfied for the requirement to hold. Scopes are preserved but unused in v1.
+ *
+ * @param schemes map from security-scheme name (as declared in {@code components.securitySchemes})
+ * to the list of OAuth2 / OpenID Connect scopes required for that scheme. An empty list means
+ * "any scope" / "no scopes required" (also used for non-OAuth schemes such as API key or HTTP
+ * auth where scopes do not apply).
*/
public record SecurityRequirement(Map> schemes) {
+ /** Canonical constructor that defensively copies {@code schemes} into an unmodifiable map. */
public SecurityRequirement {
schemes = Map.copyOf(schemes);
}
diff --git a/src/main/java/com/retailsvc/http/spec/security/SecurityScheme.java b/src/main/java/com/retailsvc/http/spec/security/SecurityScheme.java
index facf62d..5af0a9b 100644
--- a/src/main/java/com/retailsvc/http/spec/security/SecurityScheme.java
+++ b/src/main/java/com/retailsvc/http/spec/security/SecurityScheme.java
@@ -2,24 +2,78 @@
import java.util.Optional;
+/**
+ * Models an OpenAPI 3.1 {@code securitySchemes} entry.
+ *
+ * This sealed interface enumerates the security scheme variants the library understands natively
+ * ({@link ApiKey}, {@link HttpBearer}, {@link HttpBasic}) and provides an {@link Unsupported}
+ * fallback so that specs declaring scheme types not yet implemented in v1 (such as {@code oauth2},
+ * {@code openIdConnect} or {@code mutualTLS}) can still be parsed without failing the whole spec
+ * load.
+ *
+ *
Each permitted variant is a {@code record}, enabling exhaustive pattern matching at the call
+ * site.
+ */
public sealed interface SecurityScheme
permits SecurityScheme.ApiKey,
SecurityScheme.HttpBearer,
SecurityScheme.HttpBasic,
SecurityScheme.Unsupported {
+ /**
+ * OpenAPI {@code type: apiKey} security scheme.
+ *
+ *
Represents an API key transported either as a header, query parameter or cookie. The {@code
+ * name} corresponds to the OpenAPI {@code name} property and identifies the header / query /
+ * cookie key whose value carries the credential.
+ *
+ * @param name the name of the header, query parameter or cookie that carries the API key
+ * @param location where the API key is transported on the request
+ */
record ApiKey(String name, Location location) implements SecurityScheme {
+
+ /**
+ * Transport location for an {@link ApiKey} credential, mirroring the OpenAPI {@code in}
+ * property of an {@code apiKey} security scheme.
+ */
public enum Location {
+ /** Credential is sent as an HTTP request header. */
HEADER,
+ /** Credential is sent as a URL query parameter. */
QUERY,
+ /** Credential is sent as an HTTP cookie. */
COOKIE
}
}
+ /**
+ * OpenAPI {@code type: http} security scheme with {@code scheme: bearer}.
+ *
+ *
Represents bearer-token authentication (typically {@code Authorization: Bearer
+ * <token>}). The optional {@code bearerFormat} is a free-form hint from the spec (e.g.
+ * {@code JWT}) and is informational only.
+ *
+ * @param bearerFormat optional OpenAPI {@code bearerFormat} hint describing the token format
+ */
record HttpBearer(Optional bearerFormat) implements SecurityScheme {}
+ /**
+ * OpenAPI {@code type: http} security scheme with {@code scheme: basic}.
+ *
+ * Represents HTTP Basic authentication as defined by RFC 7617. Carries no configuration beyond
+ * its type.
+ */
record HttpBasic() implements SecurityScheme {}
- /** Parsed but unsupported in v1 (oauth2, openIdConnect, mutualTLS). */
+ /**
+ * Fallback for security scheme types parsed from the spec but not yet supported in v1 (notably
+ * {@code oauth2}, {@code openIdConnect} and {@code mutualTLS}).
+ *
+ *
Keeping the original {@code type} string allows callers to surface a meaningful diagnostic
+ * (or to layer in custom handling) without rejecting the whole OpenAPI document at parse time.
+ *
+ * @param type the raw OpenAPI {@code type} string as it appeared in the spec (e.g. {@code
+ * oauth2}, {@code openIdConnect}, {@code mutualTLS})
+ */
record Unsupported(String type) implements SecurityScheme {}
}
diff --git a/src/main/java/com/retailsvc/http/spec/security/SecuritySchemeParser.java b/src/main/java/com/retailsvc/http/spec/security/SecuritySchemeParser.java
index 3fa3273..bc6e252 100644
--- a/src/main/java/com/retailsvc/http/spec/security/SecuritySchemeParser.java
+++ b/src/main/java/com/retailsvc/http/spec/security/SecuritySchemeParser.java
@@ -12,9 +12,20 @@
import java.util.Map;
import java.util.Optional;
+/**
+ * Parses raw {@code securitySchemes} and {@code security} fragments from an OpenAPI document into
+ * typed {@link SecurityScheme} and {@link SecurityRequirement} instances.
+ */
public final class SecuritySchemeParser {
private SecuritySchemeParser() {}
+ /**
+ * Parses a single {@code securitySchemes} entry into a {@link SecurityScheme}.
+ *
+ * @param raw the raw map from {@code components.securitySchemes.}
+ * @return the typed security scheme, or {@link SecurityScheme.Unsupported} for unknown types
+ * @throws IllegalArgumentException if required fields are missing
+ */
public static SecurityScheme parse(Map raw) {
String type = (String) raw.get("type");
if (type == null) {
@@ -36,6 +47,13 @@ private static SecurityScheme parseApiKey(Map raw) {
return new ApiKey(name, Location.valueOf(in.toUpperCase(Locale.ROOT)));
}
+ /**
+ * Parses an OpenAPI {@code security} list (operation- or root-level) into typed requirements.
+ *
+ * @param raw the raw list of requirement objects, may be {@code null} or empty
+ * @return an immutable list of {@link SecurityRequirement}s; empty if {@code raw} is null/empty
+ * @throws IllegalArgumentException if any entry is not a JSON object
+ */
public static List parseRequirements(List raw) {
if (raw == null || raw.isEmpty()) {
return List.of();
diff --git a/src/main/java/com/retailsvc/http/validate/DefaultValidator.java b/src/main/java/com/retailsvc/http/validate/DefaultValidator.java
index fb66f20..6ed9c2b 100644
--- a/src/main/java/com/retailsvc/http/validate/DefaultValidator.java
+++ b/src/main/java/com/retailsvc/http/validate/DefaultValidator.java
@@ -44,6 +44,42 @@
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+/**
+ * Library-provided implementation of {@link Validator}.
+ *
+ * Validates a parsed JSON value (a {@link java.util.Map}, {@link java.util.List}, {@link
+ * String}, {@link Number}, {@link Boolean}, or {@code null}) against a {@link Schema} AST using
+ * {@code switch} pattern-match dispatch — there are no {@code instanceof} chains. Dispatch is
+ * delegated to the sealed {@link Schema} hierarchy so each schema kind is handled by a dedicated
+ * arm.
+ *
+ *
On the first validation failure, this class throws a {@link ValidationException} carrying a
+ * {@link ValidationError} that records the JSON Pointer path to the offending node, the failing
+ * JSON Schema keyword (for example {@code "type"}, {@code "required"}, {@code "format"}), and the
+ * rejected value. The server's exception filter renders that error as an {@code RFC 7807} {@code
+ * 400 application/problem+json} response.
+ *
+ *
Supported {@code string} formats:
+ *
+ *
+ * {@code uuid}
+ * {@code date}
+ * {@code date-time}
+ * {@code email}
+ * {@code uri}
+ * {@code uri-reference}
+ * {@code hostname}
+ * {@code ipv4}
+ * {@code ipv6}
+ * {@code regex}
+ * {@code byte} (base64-encoded)
+ * {@code binary}
+ * {@code password}
+ *
+ *
+ * Supported {@code integer} formats: {@code int32}, {@code int64}. Supported {@code number}
+ * formats: {@code float}, {@code double}.
+ */
public final class DefaultValidator implements Validator {
private static final String FORMAT_KEYWORD = "format";
@@ -103,6 +139,19 @@ private record NumberFormatCheck(DoublePredicate isValid, String message) {}
private final Function refResolver;
private final ConcurrentMap compiledPatterns = new ConcurrentHashMap<>();
+ /**
+ * Creates a validator that resolves {@code $ref} references through the supplied function.
+ *
+ * The {@code refResolver} is consulted lazily during each call to {@link #validate}: whenever
+ * a {@link com.retailsvc.http.spec.schema.RefSchema} is encountered, its URI is passed to the
+ * resolver and validation continues against the returned target {@link Schema}. Because
+ * resolution happens per-validate (not eagerly at construction), the spec's ref map can be passed
+ * by reference and the validator will pick up its current contents on every request.
+ *
+ * @param refResolver function that maps a {@code $ref} URI string to the {@link Schema} it points
+ * to; must not be {@code null} and must return a non-{@code null} schema for every ref
+ * reachable from the validated documents
+ */
public DefaultValidator(Function refResolver) {
this.refResolver = refResolver;
}
diff --git a/src/main/java/com/retailsvc/http/validate/ValidationError.java b/src/main/java/com/retailsvc/http/validate/ValidationError.java
index 3cd3423..e3f1ca4 100644
--- a/src/main/java/com/retailsvc/http/validate/ValidationError.java
+++ b/src/main/java/com/retailsvc/http/validate/ValidationError.java
@@ -1,4 +1,12 @@
package com.retailsvc.http.validate;
+/**
+ * A single validation failure produced while checking a request against an OpenAPI schema.
+ *
+ * @param pointer JSON Pointer to the offending location in the request payload
+ * @param keyword the JSON Schema keyword that failed (e.g. {@code "required"}, {@code "type"})
+ * @param message human-readable description of the failure
+ * @param rejectedValue the value that failed validation, or {@code null} if not applicable
+ */
public record ValidationError(
String pointer, String keyword, String message, Object rejectedValue) {}
diff --git a/src/main/java/com/retailsvc/http/validate/Validator.java b/src/main/java/com/retailsvc/http/validate/Validator.java
index 872632d..64f4e54 100644
--- a/src/main/java/com/retailsvc/http/validate/Validator.java
+++ b/src/main/java/com/retailsvc/http/validate/Validator.java
@@ -1,8 +1,25 @@
package com.retailsvc.http.validate;
+import com.retailsvc.http.ValidationException;
import com.retailsvc.http.spec.schema.Schema;
+/**
+ * Validator contract: validates a value against a {@link Schema}, throwing {@link
+ * ValidationException} on the first failure.
+ *
+ * {@link DefaultValidator} is the library-provided implementation.
+ */
public interface Validator {
- /** Throws ValidationException on first failure. */
+ /**
+ * Validates {@code value} against {@code schema}, throwing {@link ValidationException} on the
+ * first failure.
+ *
+ * @param value the value to validate (may be {@code null} — a {@code null} is accepted only when
+ * the schema permits the {@code null} type)
+ * @param schema the schema to validate against
+ * @param pointer JSON Pointer prefix used to qualify the path of any failure (use empty string
+ * for root)
+ * @throws ValidationException on the first failure
+ */
void validate(Object value, Schema schema, String pointer);
}