Skip to content

feat: Type mapper request handler#56

Merged
thced merged 50 commits into
masterfrom
feat/type-mapper-request-handler
May 18, 2026
Merged

feat: Type mapper request handler#56
thced merged 50 commits into
masterfrom
feat/type-mapper-request-handler

Conversation

@thced

@thced thced commented May 13, 2026

Copy link
Copy Markdown
Contributor

No description provided.

thced added 23 commits May 13, 2026 12:32
Copilot AI review requested due to automatic review settings May 13, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 through OpenApiServer + RequestPreparationFilter.
  • Replace handler API with RequestHandler + new Request/ResponseBuilder response 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.

Comment thread src/main/java/com/retailsvc/http/internal/DefaultResponseBuilder.java Outdated
Comment thread src/main/java/com/retailsvc/http/Request.java Outdated
thced added 23 commits May 14, 2026 17:04
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.
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.
@sonarqubecloud

Copy link
Copy Markdown

@thced thced merged commit e41d0d7 into master May 18, 2026
4 checks passed
@thced thced deleted the feat/type-mapper-request-handler branch May 18, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants