Skip to content

feat!: multi-decoder routing (decoders=[...])#41

Merged
lesnik512 merged 11 commits into
mainfrom
feat/multi-decoder-routing
Jun 10, 2026
Merged

feat!: multi-decoder routing (decoders=[...])#41
lesnik512 merged 11 commits into
mainfrom
feat/multi-decoder-routing

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Replaces the single-decoder slot on AsyncClient/Client with a type-dispatched decoders=[...] list. Reverses the 0.3.0 fail-fast for missing pydantic — AsyncClient() no longer raises on missing extras; failure is deferred to the first response_model= use site via the new MissingDecoderError (fires before the HTTP call).

Headline changes

  • ResponseDecoder Protocol gains can_decode(model) -> bool. The client walks _decoders in order and picks the first whose predicate returns True. Built-in decoders claim broadly within their library; native types of the other library are rejected (pydantic rejects msgspec.Struct; msgspec rejects pydantic.BaseModel via msgspec.inspect.type_info + CustomType filter).
  • decoders: Sequence[ResponseDecoder] | None = None replaces the old decoder= kwarg on both AsyncClient.__init__ and Client.__init__. Default None resolves via _build_default_decoders() against installed extras (pydantic-first when both present, empty tuple when neither).
  • MissingDecoderError (sibling of DecodeError under ClientError) carries model: type and registered_names: tuple[str, ...]. Pre-flight check at .send() / .send_with_response() entry — distinct from DecodeError (decoder ran, payload bad).
  • Mixed pydantic + msgspec in one client. AsyncClient(decoders=[PydanticDecoder(), MsgspecDecoder()]) is the new default; BaseModel routes to pydantic, Struct to msgspec, shared shapes (dict, list, dataclass, primitive) go to the first decoder in the list.
  • Sync/async surfaces are true mirrors — both classes refactored atomically; legacy _default_pydantic_decoder() helper deleted.

Behavioral changes for users

  • AsyncClient(decoder=...) now raises TypeError: unexpected keyword argument 'decoder'. Migration: AsyncClient(decoders=[...]).
  • AsyncClient() / Client() no longer raise ImportError when pydantic is missing. Errors now surface only when response_model= is used.
  • Custom ResponseDecoder implementations must add can_decode. Trivial catch-all migration: def can_decode(self, model): return True.

Spec + plan

  • Spec: planning/specs/2026-06-09-multi-decoder-design.md
  • Plan: planning/plans/2026-06-09-multi-decoder-plan.md
  • Seam B updated in planning/engineering.md.

Target release

0.9.0 — minor bump, breaking surface (! in commit subjects).

Test plan

  • just lint clean (ruff format + ruff check + ty)
  • just test green — 485 passed, 100% coverage
  • mkdocs build --strict clean (no warnings on new content)
  • Sync/async symmetry verified by code reviewer (AsyncClient and Client read identically modulo await)
  • Pre-flight contract verified — MissingDecoderError fires before _dispatch(request) (asserted via pytest.fail in MockTransport handlers in 4+ tests)
  • Cross-task coherence reviewed — no residue of old decoder= kwarg anywhere in src/ / tests/ / docs/
  • Manual smoke test: pip install httpware[pydantic,msgspec] and verify mixed-decoder client works end-to-end
  • Manual smoke test: pip uninstall pydantic, verify AsyncClient() constructs without raising

lesnik512 added 11 commits June 9, 2026 23:22
Reframes the decoder seam from one-decoder-per-client to a typed-dispatched
`decoders=[...]` list. Removes the 0.3.0 eager-import fail-fast: `AsyncClient()`
no longer raises on missing pydantic. Adds `MissingDecoderError` (fires before
the HTTP call). Target release: 0.9.0.
Nine-task TDD sequence covering the AsyncClient/Client migration to
`decoders=[...]`, the new MissingDecoderError, and the docs sweep. Phases A
(new surfaces), B (atomic client refactors), C (new test files), D (docs).
@lesnik512 lesnik512 self-assigned this Jun 10, 2026
@lesnik512 lesnik512 merged commit bed715f into main Jun 10, 2026
5 checks passed
@lesnik512 lesnik512 deleted the feat/multi-decoder-routing branch June 10, 2026 07:30
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.

1 participant