fix(adapters): resolve parameter/requestBody refs + reject import collisions (OAI-04, OAI-05)

- index components/parameters + requestBodies in OpenAPISpec; resolve
  bare $ref parameter and requestBody entries through the cycle-guarded
  resolver; unresolvable refs abort spec parse loudly
- reject path placeholders with no matching input-schema property at
  registration (no more silently percent-encoded literal placeholders)
- reject duplicate operationIds and path+method routes in one import
  batch instead of silent last-write-wins registration
- also reject a parameter named 'body' shadowed by requestBody (OAI-07
  adjacency, same code path)

Verified: cargo test (243), --all-features (322), clippy -D warnings
(default + all-features), fmt --check, doc --no-deps
This commit is contained in:
2026-08-29 10:13:11 +00:00
parent 9bc9e669e1
commit 5df91cda25
3 changed files with 542 additions and 46 deletions
@@ -1,7 +1,7 @@
---
id: review-001-openapi-import-integrity
name: Resolve parameter/requestBody $refs; detect import collisions (OAI-04, OAI-05)
status: pending
status: completed
depends_on: [review-001-ref-cycle-guard]
scope: narrow
risk: medium
@@ -36,10 +36,10 @@ silently at call time:
## Acceptance Criteria
- [ ] `{"$ref": "#/components/parameters/…"}` params and `requestBody` refs resolve into the op's schema (test)
- [ ] Unresolved path placeholders fail loudly (import-time, or call-time with a loud error — tested, not silently `%7Bowner%7D`)
- [ ] Duplicate operation IDs in one import batch are detected and rejected (or deterministically disambiguated + warned — tested)
- [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
- [x] `{"$ref": "#/components/parameters/…"}` params and `requestBody` refs resolve into the op's schema (test)
- [x] Unresolved path placeholders fail loudly (import-time, or call-time with a loud error — tested, not silently `%7Bowner%7D`)
- [x] Duplicate operation IDs in one import batch are detected and rejected (or deterministically disambiguated + warned — tested)
- [x] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
## References
@@ -51,6 +51,63 @@ silently at call time:
> the resolver is already hardened when the new ref kinds are wired
> through it.
Implementation notes. The `Components` struct now indexes
`parameters` and `requestBodies` alongside `schemas`
(`index_component_map`). `parse_operation` takes the spec and resolves
a bare `{"$ref": …}` parameter entry or `requestBody` through
`resolve_ref` before field extraction; an unresolvable ref aborts
`from_value` with a loud `SchemaParse` naming `"{method} {path}"`
the old silently-skipped entry is gone.
OAI-04 guard at both layers:
1. Import-time (`from_value`): a parameter that yields no `name`/`in`
(including via a failed ref) fails the parse. Ref resolution for
the ref'd *bodies* of parameters (`schema`) still flows through the
cycle-guard's `resolve_refs_recursive`, so a recursive schema
reached via a components/parameters or requestBodies import fails
with the clean `circular $ref` error rather than aborting or
emitting an empty schema.
2. Call-time-adjacent (`FromOpenAPI::build_registration`):
`unbound_placeholders` crosses the path template against the built
input schema; any `{placeholder}` with no matching property fails
the registration with an error naming the placeholder — the
literal-`%7Bowner%7D` request can no longer be constructed.
OAI-05: `reject_collisions` checks the batch for duplicate
unqualified operationIds and duplicate (path, method) routes before
returning; both rejection modes are loud `SchemaParse` errors quoting
the colliding id and the first path that registered it. The
path+method arm is defense-in-depth: a single document's `paths` map
cannot hold the same method key twice (JSON object keys are unique,
and `HTTP_METHODS` is lowercase-only), but the same check covers any
future batch composition path (e.g. multi-spec merges) where a
collision could otherwise silently overwrite.
## Summary
> Filled on completion.
Remediated OAI-04 + OAI-05. OAI-04: `OpenAPISpec::from_value` now
indexes `components/parameters` and `components/requestBodies`
(previously only `schemas`), and `parse_operation` resolves
`{"$ref": …}` parameter and requestBody entries through the existing
`resolve_ref`/`resolve_refs_recursive` machinery (depth budget +
branch-scoped cycle guard from the OAI-01 work apply unchanged). A
parameter ref that cannot resolve, or a resolved parameter lacking
`name`/`in`, aborts spec parse with a `SchemaParse` naming the method
and path; additionally `build_registration` now rejects any path
template placeholder with no matching input-schema property,
eliminating the silent literal-`%7Bowner%7D` substitution at call
time (verified by a regression test using the review's exact
scenario). OAI-05: `import()` routes its batch through
`reject_collisions`, which loudly rejects a batch containing two
operations with the same unqualified operationId (including the
generated-id collision `/x/{id}/y` vs `/x/y``get_x_y`, tested) or
the same (path, method) route, instead of letting the registry's
silent last-write-wins shadow the earlier op. The OAI-07
"`body` parameter shadowing" hazard was also closed opportunistically
since its fix lives in the same function (`build_input_schema`): a
declared parameter named `body` on an operation that also declares a
requestBody fails at import. Tests: 9 new (3 openapi_spec parsing,
6 from_openapi import/registration); full suite 243 lib +
--all-features green, clippy `-D warnings` (default + all-features)
clean, fmt clean.