feat(adapters): loud unsupported-OpenAPI-feature handling (OAI-06)

- build_error_schemas: default/wildcard response keys dropped with a
  warn instead of emitting a dead HTTP_0 ErrorDefinition — /search
  never advertises a code that can't match (the runtime mapper already
  synthesizes HTTP_<actual> for unmapped statuses)
- check_parameter_style: non-default style/explode parameter forms
  (spaceDelimited, pipeDelimited, deepObject, form+explode:false,
  simple+explode:true) fail import with a feature-naming SchemaParse;
  wire-equivalent defaults (form, simple) import unchanged — no more
  silent "[1,2]" array mis-serialization
- servers overrides rejected at import at all three levels (document,
  path, operation) — the adapter pins one base_url at assembly time
- trace-only paths: skip is now logged (warn naming path + methods),
  documented-as-inert instead of silent
- detect_op_type + build_output_schema sweep 2XX/default keys for
  text/event-stream — a default-declared SSE stream classifies as Sub
  instead of returning one giant text body

Tests: 11 new (error-drop, style rejections + default accept, servers
3-level rejections + baseline, trace skip, SSE default/2XX detection).
Verified: cargo test (299), --all-features (370 + suites), clippy
--all-targets -D warnings (default + all-features), fmt --check.

Tasks: review-001-openapi-loud-degradation
This commit is contained in:
2026-08-30 08:10:40 +00:00
parent edbda6605b
commit 7ce1ca6fbd
3 changed files with 552 additions and 31 deletions
@@ -1,7 +1,7 @@
---
id: review-001-openapi-loud-degradation
name: Loud unsupported-OpenAPI-feature handling (OAI-06)
status: pending
status: completed
depends_on: []
scope: narrow
risk: low
@@ -42,10 +42,10 @@ defensible if logged).
## Acceptance Criteria
- [ ] A spec using each unsupported feature either imports with a documented, warned, tested behavior or fails import with a feature-naming error (tests per feature)
- [ ] `HTTP_0` no longer emitted (default responses mapped or dropped loudly) — `/search` never advertises a code that can't match
- [ ] `style`/`explode` non-default forms do not silently mis-serialize arrays
- [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
- [x] A spec using each unsupported feature either imports with a documented, warned, tested behavior or fails import with a feature-naming error (tests per feature)
- [x] `HTTP_0` no longer emitted (default responses mapped or dropped loudly) — `/search` never advertises a code that can't match
- [x] `style`/`explode` non-default forms do not silently mis-serialize arrays
- [x] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
## References
@@ -54,12 +54,76 @@ defensible if logged).
## Notes
> Agent fills during implementation. Deferred from the original
> decomposition for staleness reasons; unblocked once OAI-02/03/07/09
> landed (they did). Scope guard: this is import-time fidelity, not
> new feature support — do not implement `servers` overrides or style
> serialization here.
Per-feature decisions (the review's map, applied):
- **`default`/wildcard responses → dropped loudly (warn, not
reject)**: a catch-all code was considered and rejected — the runtime
mapper (`forward.rs::error_envelope`) already synthesizes
`HTTP_<actual>` for unmapped statuses, so a `default` ErrorDefinition
could never match anything: keeping it would only let `/search`
advertise a dead `HTTP_0` code. `build_error_schemas` now skips any
non-numeric response key at `tracing::warn` level, naming the
operation, namespace, and response key, with the remediation in the
message (declare explicit statuses). Wildcards (`5XX`, `4XX`) fall out
of the same `parse::<u16>()` check — they were the same dead entry
class.
- **`style`/`explode` non-default forms → reject at import**
(cookie-style). New `check_parameter_style` in `openapi_spec.rs`;
`parse_operation` now returns `Result<Option<Operation>,
ParameterStyleError>` so the caller converts the refusal into a
`SchemaParse` naming the parameter, the method+path, the offending
declaration, and the remediation. Wire-equivalent forms accepted
silently: `form` (query/path default, explode=true) and `simple`
(header/path default, explode=false) — a spec authoring these
explicitly gets identical serialization to omitting them. Rejected
with feature-naming errors: `spaceDelimited`, `pipeDelimited`,
`deepObject`, `matrix`, `label` (the generic arm), plus
`form+explode:false` (comma-glue, the `?a=1,2` mis-serialization the
review flagged) and `simple+explode:true`.
- **`servers` overrides → reject at import** at **all three levels**
(document root, per-path, per-operation) in one sweep in
`from_value`. The adapter pins one `base_url` at assembly time and
cannot honor per-location servers; the error names every offending
location and the remediation (remove the entries, or one import per
base URL). This check lives in `openapi_spec.rs` (parse time), so
both `from_openapi` and any future raw-doc consumer inherit it.
- **`trace` → documented-as-inert, now logged.** `HTTP_METHODS` remains
without `trace` (scope guard: no new feature support), but a path
entry carrying only unsupported methods was previously dropped
without a trace; `from_value` now emits a `tracing::warn` naming the
path and the skipped methods. Tested as "skips without erroring, the
rest of the doc imports" (the log line itself is the visibility
mechanism, consistent with the module's other warns).
- **`default`/non-200 2XX-declared SSE → supported (small superset).**
`detect_op_type` and `build_output_schema` sweep
`200..206, 226, default` for `text/event-stream` instead of only
`200`/`201` — a `default`-declared stream now classifies as `Sub`
with the SSE output schema instead of degrading to a giant single
text body. This was the one place where "reject" would have been
user-hostile: declaring streams under `default` is a real-world
pattern, and detection is a two-line change.
## Summary
> Filled on completion.
- `src/adapters/from_openapi.rs`: `build_error_schemas` drops
`default`/wildcard keys loudly (never emits `HTTP_0`); `detect_op_type`
+ `build_output_schema` sweep 2XX/`default` for `text/event-stream`.
- `src/adapters/openapi_spec.rs`: document/path/operation-level
`servers` rejection; `check_parameter_style` (`ParameterStyleError`)
rejecting non-default `style`/`explode` with feature-naming errors
while accepting the wire-equivalent defaults; unsupported-method-only
paths logged at warn.
- Tests (8 new): `default_response_key_is_dropped_not_advertised_as_http_0`,
`default_declared_sse_stream_classifies_as_subscription`,
`non_default_2xx_sse_stream_classifies_as_subscription`,
`non_default_style_parameter_fails_import_naming_the_feature`,
`deep_object_style_is_rejected_like_the_other_non_default_forms`,
`form_style_with_explode_false_is_rejected`,
`default_style_and_explode_forms_still_import`,
`servers_override_at_document_level_fails_import`,
`servers_override_at_path_and_operation_level_fails_import`,
`servers_absent_baseline_still_imports`,
`trace_only_path_is_skipped_and_documented_inert`.
- Verified: `cargo test` (299), `--all-features` (370 + suites),
`clippy --all-targets -- -D warnings` (default + all-features),
`fmt --check`.