feat: Type mapper request handler#56
Conversation
There was a problem hiding this comment.
Pull request overview
This PR replaces the prior JsonMapper + raw HttpExchange handler model with a per-media-type TypeMapper abstraction and a new RequestHandler API that receives a structured Request object, including a fluent ResponseBuilder for writing responses. It also adds an optional Gson-backed JSON mapper that can be auto-registered when Gson is present on the classpath, and updates tests, examples, and documentation accordingly.
Changes:
- Introduce
TypeMapper(with built-in form/text mappers and optional Gson JSON fallback) and wire it throughOpenApiServer+RequestPreparationFilter. - Replace handler API with
RequestHandler+ newRequest/ResponseBuilderresponse gateway, refactoring dispatch and examples/tests. - Update documentation, tests, and build configuration (Gson becomes an optional dependency).
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/retailsvc/http/OpenApiServer.java | Builder now registers TypeMappers (incl. Gson fallback) and dispatches to RequestHandlers. |
| src/main/java/com/retailsvc/http/Request.java | New per-request handle passed to handlers; exposes request data + respond(...). |
| src/main/java/com/retailsvc/http/RequestHandler.java | New functional handler interface: handle(Request). |
| src/main/java/com/retailsvc/http/ResponseBuilder.java | New response-writing API returned by Request.respond(int). |
| src/main/java/com/retailsvc/http/TypeMapper.java | New per-media-type read/write contract for bodies. |
| src/main/java/com/retailsvc/http/internal/DefaultResponseBuilder.java | Concrete implementation of ResponseBuilder for HttpExchange. |
| src/main/java/com/retailsvc/http/internal/DispatchHandler.java | Dispatch now reads current Request from ScopedValue and invokes RequestHandler. |
| src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java | Uses TypeMapper registry for parsing; constructs/binds Request. |
| src/main/java/com/retailsvc/http/internal/FormBodyCoercion.java | Extracted form-body schema coercion helper. |
| src/main/java/com/retailsvc/http/internal/FormTypeMapper.java | Built-in application/x-www-form-urlencoded mapper (read-only). |
| src/main/java/com/retailsvc/http/internal/TextTypeMapper.java | Built-in text/plain mapper (charset-aware read; UTF-8 write). |
| src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java | Removes schema-aware coercion from the parser (parsing only). |
| src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java | Gson-backed JSON TypeMapper (integer-preserving read; JSR-310 ISO-8601 write). |
| src/main/java/com/retailsvc/http/JsonMapper.java | Removed legacy JSON mapper interface. |
| src/main/java/com/retailsvc/http/internal/RequestContext.java | Removed legacy per-request context record. |
| src/test/java/com/retailsvc/http/TypeMapperShapeTest.java | Verifies TypeMapper interface usability. |
| src/test/java/com/retailsvc/http/TypeMapperRegistrationTest.java | Covers mapper registration + Gson fallback behavior. |
| src/test/java/com/retailsvc/http/RequestResponseGatewayTest.java | Exercises ResponseBuilder terminals (json/empty/stream). |
| src/test/java/com/retailsvc/http/RequestTest.java | Updates request-scoping expectations to new DispatchHandler.CURRENT model. |
| src/test/java/com/retailsvc/http/ServerBaseTest.java | Test server setup updated to builder + RequestHandler. |
| src/test/java/com/retailsvc/http/OpenApiServerTest.java | Updates unit tests to new builder and handler types. |
| src/test/java/com/retailsvc/http/OpenApiServerBuilderTest.java | Updates builder tests; adds null-arg checks for bodyMapper. |
| src/test/java/com/retailsvc/http/OpenApiServerIT.java | Migrates integration tests to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/ExtraHandlersIT.java | Keeps extra handlers as raw HttpHandler; updates server build path. |
| src/test/java/com/retailsvc/http/NonJsonBodyIT.java | Migrates non-JSON ITs to RequestHandler. |
| src/test/java/com/retailsvc/http/JsonMapperTest.java | Removed test for deleted JsonMapper. |
| src/test/java/com/retailsvc/http/internal/DispatchHandlerTest.java | Updates dispatch tests for RequestHandler and new scoping. |
| src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java | Updates filter tests to pass a TypeMapper registry and new request access. |
| src/test/java/com/retailsvc/http/internal/FormUrlEncodedParserTest.java | Updates tests to use FormBodyCoercion + parse-only behavior. |
| src/test/java/com/retailsvc/http/internal/FormTypeMapperTest.java | Tests built-in form mapper behavior. |
| src/test/java/com/retailsvc/http/internal/TextTypeMapperTest.java | Tests built-in text mapper behavior (charset + write). |
| src/test/java/com/retailsvc/http/internal/gson/GsonJsonMapperTest.java | Tests Gson mapper numeric handling + JSR-310 serialization. |
| src/test/java/com/retailsvc/http/start/ServerLauncher.java | Updates example launcher to builder + RequestHandler. |
| src/test/java/com/retailsvc/http/start/EchoHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/FormEchoHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/GetDataHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/ParamHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/PostDataHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/PostListObjectsHandler.java | Migrates example handler to RequestHandler + response gateway. |
| src/test/java/com/retailsvc/http/start/TextEchoHandler.java | Migrates example handler to RequestHandler + response gateway. |
| README.md | Documents TypeMapper, Gson fallback, and new handler/response APIs. |
| pom.xml | Moves Gson to optional (non-transitive) dependency. |
| docs/superpowers/specs/2026-05-13-type-mapper-request-handler-design.md | Design spec for the new APIs. |
| docs/superpowers/plans/2026-05-13-type-mapper-request-handler.md | Implementation plan for the refactor. |
Comments suppressed due to low confidence (1)
src/main/java/com/retailsvc/http/internal/DefaultResponseBuilder.java:98
- The streaming terminals return the raw exchange OutputStream but never close the HttpExchange. Today handlers used
try (exchange)to guarantee cleanup; with the new API, callers will typically only close the returned OutputStream. If closing the stream doesn't reliably close the exchange, this can leak connections. Consider returning an OutputStream wrapper whose close() also closes the exchange (or otherwise guaranteeing exchange.close() on stream completion).
@Override
public OutputStream stream() throws IOException {
terminate();
applyHeaders();
exchange.sendResponseHeaders(status, 0);
return exchange.getResponseBody();
}
@Override
public OutputStream stream(long length) throws IOException {
if (length < 0) {
throw new IllegalArgumentException("length must be non-negative");
}
terminate();
applyHeaders();
exchange.sendResponseHeaders(status, length);
return exchange.getResponseBody();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Handlers now return an immutable Response record instead of mutating an HttpExchange via the ResponseBuilder fluent API. Response is a pure value: status + body + contentType + headers, with factories for common shapes (empty/status/ok/of/text/bytes/stream). ResponseDecorator becomes a (Request, Response) -> Response transform applied after the handler returns; interceptors and continuations return Response too so cross-cutting work composes cleanly. Removed: ResponseBuilder, DefaultResponseBuilder, Request.respond(int), the per-Request responseSent flag, and the IllegalStateException state machine - single-shot is enforced by the return type. Renderer (internal ResponseRenderer) handles byte[] / BodyWriter / mapper.writeTo dispatch, including null-body -> responseLength=-1.
…ceptor + decorator
Mirrors Request.queryParam(name); avoids the round-trip through pathParams().get(name) for the common single-lookup case.
bodyMapper("application/json", mapper) is the 95% case; jsonMapper(mapper)
removes the repeated media-type literal. Generic bodyMapper(mediaType, mapper)
stays for text/csv, application/xml, etc.
… as absent
Every list handler was writing:
int limit = Optional.ofNullable(req.queryParam("limit"))
.filter(v -> !v.isBlank())
.map(Integer::parseInt)
.orElse(DEFAULT_LIMIT);
The blank-vs-absent question is the kind of HTTP-API trap that's better
solved once in the library than copied across every consumer. Returning
Optional<String> with blank already filtered shrinks the call site to:
int limit = req.queryParam("limit").map(Integer::parseInt).orElse(DEFAULT_LIMIT);
Same treatment for header(name). pathParam(name) is unchanged (still
nullable) — path parameters come from the route template, not from the
client, so blank is genuinely a different case.
Reads more honestly. "handlers" and "addHandler" sounded like the same
concept but they aren't — handlers(Map) is operationId-keyed and validated
against the OpenAPI spec; the other is path-keyed and bypasses validation.
"extraRoute" makes that distinction visible at the call site:
.handlers(handlerMap) // OpenAPI operations
.extraRoute("/alive", Handlers.aliveHandler()) // side route
Location had a dedicated parameter slot; every other header went through
withHeader. A real footgun — callers reached for Response.created(body) and
forgot the Location header. Forcing .withHeader("Location", uri) for
symmetry removes the trap.
…nsports Request now holds transport-neutral primitives: body bytes, parsed body, TypeMapper, operationId, path parameters, raw query string, and a Function<String,String> header lookup. The JDK HttpServer adapter (RequestPreparationFilter) is the only place that touches HttpExchange. Why: keeps the door open to a Netty / Helidon Nima / Jetty backend later if JDK HttpServer's throughput becomes the bottleneck. The handler-facing API (Request, Response, RequestHandler, RequestInterceptor, ResponseDecorator, TypeMapper) is now genuinely transport-neutral — a future adapter would live in com.retailsvc.http.internal and leave handler code untouched. README's 'Performance and caveats' section documents the rationale.
- OpenApiServer: bundle handlers/interceptors/decorators/exceptionHandler/ extras into a private HandlerConfig record, dropping the constructor from 9 params to 5 (under Sonar's brain-overload threshold of 7). - OpenApiServer.jsonMapper: use the existing JSON constant instead of duplicating the "application/json" literal. - Request: switch headerLookup from Function<String,String> to the more specialised UnaryOperator<String>. - DispatchHandlerTest.withRequest: drop the unused HttpExchange parameter (no longer needed now that Request is built from primitives).
Returns the Content-Type header as Optional<String> (blank treated as absent, matching header(name)). The most frequently inspected header by handler code; saves the magic-string typo risk.
|



No description provided.