feat: add a ResponseHandler seam with lazy parse-once ParsedResponse#96
Merged
Conversation
…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.
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
ResponseHandler<T>seam and a lazy, parse-onceParsedResponse<T>:ResponseHandler<T>maps a rawResponseto a typed result; companion factoriesstring()/empty()cover the dependency-free cases.ParsedResponse<T>exposes the rawstatus/headers/request/protocolwithout parsing, andvalue()runs the handler lazily and exactly once — memoized race-safely under aReentrantLock, with a sentinel so anullresult or a thrown failure is also cached (never re-consuming the body).close()forwards to the underlying response.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.
Closes #36