Skip to content

feat: add a ResponseHandler seam with lazy parse-once ParsedResponse#96

Merged
OmarAlJarrah merged 5 commits into
mainfrom
feat/response-handler-seam
Jun 16, 2026
Merged

feat: add a ResponseHandler seam with lazy parse-once ParsedResponse#96
OmarAlJarrah merged 5 commits into
mainfrom
feat/response-handler-seam

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

Adds a ResponseHandler<T> seam and a lazy, parse-once ParsedResponse<T>:

  • ResponseHandler<T> maps a raw Response to a typed result; companion factories string() / empty() cover the dependency-free cases.
  • ParsedResponse<T> exposes the raw status/headers/request/protocol without parsing, and value() runs the handler lazily and exactly once — memoized race-safely under a ReentrantLock, with a sentinel so a null result or a thrown failure is also cached (never re-consuming the body). close() forwards to the underlying response.
  • In sdk-serde-jackson, jsonHandler(serde, Class<T>) / jsonHandler(serde, TypeReference<T>) stream the body through the deserializer and surface failures as the SDK serde error type.

New public API; tests cover lazy parsing, parse-once memoization (including a memoized failure), single body consumption, and concurrent first access.

Stacked on #93 (which introduces SerdeException). This PR targets that branch and will automatically retarget to main once #93 merges; reviewing #93 first is recommended.

Closes #36

…cion

Two hardening changes to the serde layer.

1. Stable SPI failure type. The `serde` SPI declared no exception type, so the
   Jackson adapter let `com.fasterxml.jackson.*` exceptions escape across the
   zero-dependency `Serde` boundary. Callers coding against the SPI could not
   catch a stable type, and the abstraction leaked its backing library. Add an
   open `SerdeException : RuntimeException` to `sdk-core`'s serde package, plus
   `SerializationException` / `DeserializationException` for the write and read
   directions. The Jackson adapter now catches `JsonProcessingException` (the
   root of Jackson's parse/mapping failure hierarchy) at every SPI method and
   rethrows as the matching SDK type with the original chained as the cause. A
   genuine stream `IOException` still propagates unchanged, and the buffer
   overload's bounds checks remain `IndexOutOfBoundsException`.

2. Strict scalar coercion on the default mapper. Jackson's defaults silently
   reshape mismatched scalars: a wire string "5" coerced into a numeric field,
   numbers into strings, booleans across types. That masks malformed payloads.
   The SDK's default `ObjectMapper` now disables
   `MapperFeature.ALLOW_COERCION_OF_SCALARS` and sets per-type coercion to
   `Fail` for the cross-shape pairs (string -> int/float/boolean, boolean <->
   int, and int/float/boolean -> string), built through
   `JsonMapper.builder().withCoercionConfig(...)`. Numeric widening
   (int -> floating-point) and correctly typed payloads are unaffected, and the
   auto-detect features Kotlin data-class binding relies on are left untouched.
   This applies only to the SDK default mapper, never to a caller-supplied one.

This is a pre-1.0 behaviour change: payloads whose JSON shape does not match the
target type now fail instead of binding to a quietly wrong value.
Introduce a raw-vs-parsed response seam so callers can read status and
headers without forcing deserialization, and parse the typed body lazily
and exactly once.

- ResponseHandler<T>: a fun interface mapping a raw Response to a typed
  result. The handler owns consuming and closing the body. Dep-free
  string() and empty() handlers ship in sdk-core.
- ParsedResponse<T>: pairs a Response with a ResponseHandler and exposes
  the raw request/protocol/status/message/headers without parsing. value()
  runs the handler on first access and memoizes the outcome — success or
  failure — behind a ReentrantLock, so repeated access never re-parses or
  re-reads the now-consumed body. A null result memoizes correctly via a
  sentinel holder. Closeable, forwarding to the raw response.
- SerdeException: an unchecked failure in the serde package that adapters
  translate codec errors into, keeping the concrete codec from leaking past
  the Serde seam.
- jsonHandler(serde, Class<T>) and jsonHandler(serde, TypeReference<T>) in
  sdk-serde-jackson stream the body through the deserializer, close the
  response, and surface deserialization failures (and a missing body) as a
  SerdeException with the original cause preserved.

The Jackson tests build response bodies over the I/O seam, so the module
gains a test-only dependency on sdk-io-okio3.
…d handler docs

Remove the unused reified `deserialize` import in JsonResponseHandler — the only
call site binds to the member `deserialize(stream, Class<T>)` overload, so the
extension was dead and ktlint's no-unused-imports broke the build.

Documentation and ergonomics follow-ups:
- ParsedResponse.value() now states that handler failures of any type (commonly
  unchecked, e.g. SerdeException from the Jackson handler) propagate and are
  memoized, so callers do not assume IOException is the only escape.
- Add a comment explaining the deliberate catch(Throwable) in the memoization
  path: the single-use body cannot be re-read, so even an Error is pinned rather
  than masked by a re-parse.
- Make ParsedResponse's primary constructor internal (factories `of` /
  `parsedWith` remain the entry points), matching the immutable-model convention;
  api snapshot regenerated.
- Warn on ResponseHandler.string() that it reads an unbounded body into memory,
  unsuitable for untrusted or large payloads.
- Soften the DRAIN_CHUNK_BYTES comment so it cannot drift from the I/O segment
  size, and include the target type name in the null-body SerdeException message.
@OmarAlJarrah OmarAlJarrah changed the base branch from feat/serde-error-and-coercion to main June 16, 2026 20:12
…ullability; release empty() drain buffer deterministically
…-seam

# Conflicts:
#	sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonObjectMappers.kt
#	sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonSerde.kt
#	sdk-serde-jackson/src/test/kotlin/org/dexpace/sdk/serde/jackson/JacksonObjectMappersTest.kt
@OmarAlJarrah OmarAlJarrah merged commit 7b0138b into main Jun 16, 2026
1 check passed
@OmarAlJarrah OmarAlJarrah deleted the feat/response-handler-seam branch June 16, 2026 20:16
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.

Add a ResponseHandler<T> seam with lazy parse-once ParsedResponse<T>

1 participant