docs(arch): ADR-051 — YAML input format for from_openapi (gap-fill; JSON-first detection, yaml_serde dep)
from_openapi's spec already stated the contract as "JSON/YAML doc" but the implementation only delivered the JSON half. vast.ai publishes a YAML OpenAPI schema, which surfaces the gap. This is gap-filling against an existing constraint — no architecture invariants touched (OpenAPISpec stays serde_json::Value-based, forwarding handler / no-env-vars / error fidelity / to_openapi output unchanged). ADR-051 records: (1) from_yaml + format-detecting from_str constructors; (2) JSON-first/YAML-fallback detection as a correctness rule (YAML 1.1 coerces yes/no/on/off to booleans — running JSON through YAML would silently corrupt string fields); (3) yaml_serde dependency (maintained official-YAML-org fork of deprecated serde_yaml, not feature-gated); (4) to_openapi output stays JSON (scope boundary). Specs updated to reference ADR-051 by number (http-adapters, overview, both README ADR tables). Implementation task added at tasks/http/adapters/from-openapi-yaml-input.md.
This commit is contained in:
1 parent
d0e3711b46
commit
193f3a0e24
6 files changed
+420
-12
No files matched your search
@@ -0,0 +1,180 @@
|
||||
---
|
||||
id: http/adapters/from-openapi-yaml-input
|
||||
name: Add YAML input format to from_openapi (from_yaml + format-detecting from_str, JSON-first per ADR-051)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Add YAML OpenAPI document support to `from_openapi`. The spec already
|
||||
stated the contract as "JSON/YAML doc" but the implementation only ever
|
||||
delivered the JSON half (`OpenAPISpec::from_json`). YAML was always
|
||||
intended; vast.ai's `openapi.yaml` surfaces the gap. This is gap-filling
|
||||
against an existing constraint, not a new direction — no architecture
|
||||
invariants are touched (`OpenAPISpec` stays `serde_json::Value`-based,
|
||||
forwarding handler unchanged, no-env-vars unchanged, error fidelity
|
||||
unchanged, `to_openapi` output stays JSON). See ADR-051 for the full
|
||||
rationale.
|
||||
|
||||
### The three new constructors (ADR-051 §1)
|
||||
|
||||
`OpenAPISpec` gains two constructors alongside the existing `from_json`:
|
||||
|
||||
```rust
|
||||
impl OpenAPISpec {
|
||||
pub fn from_json(doc: &str) -> Result<Self, AdapterError>; // existing
|
||||
pub fn from_yaml(doc: &str) -> Result<Self, AdapterError>; // new — parses YAML
|
||||
pub fn from_str(doc: &str) -> Result<Self, AdapterError>; // new — detects format
|
||||
pub fn from_value(raw: Value) -> Result<Self, AdapterError>; // existing, unchanged
|
||||
}
|
||||
```
|
||||
|
||||
`from_yaml` parses YAML to `serde_json::Value` via `yaml_serde`, then
|
||||
feeds the existing `from_value` path — there is one `OpenAPISpec`, not a
|
||||
JSON and a YAML variant. `from_str` is the convenience for callers that
|
||||
have a raw doc string of unknown format (e.g., fetched from a URL with no
|
||||
Content-Type hint).
|
||||
|
||||
### JSON-first format detection — a correctness rule, not style (ADR-051 §2)
|
||||
|
||||
`from_str`'s detection rule is **JSON-first, YAML-fallback**: attempt
|
||||
`serde_json::from_str`; if it parses, use the result; if it fails,
|
||||
attempt YAML. This ordering is a correctness guard, not a style preference.
|
||||
YAML 1.1 (the version the maintained Rust YAML crates implement) coerces
|
||||
the bare tokens `yes`/`no`/`on`/`off`/`y`/`n` to booleans. OpenAPI specs
|
||||
routinely have **string** fields with these values (e.g., a query
|
||||
parameter named `active` with value `"yes"`, or an enum of
|
||||
`["on", "off", "auto"]`). If a JSON document is parsed through the YAML
|
||||
parser, `{"active": "yes"}` silently becomes `{"active": true}` — the
|
||||
string is lost, the schema is wrong, and the failure is silent (no error,
|
||||
just a mutated value the forwarding handler then sends as a boolean
|
||||
where a string was expected). JSON-first avoids this: a JSON parse
|
||||
succeeds for any valid JSON doc, and JSON's stricter grammar (`"yes"` is
|
||||
a string, `true` is a boolean — no ambiguity) avoids YAML 1.1 coercion.
|
||||
Only if JSON parse fails does `from_str` fall back to YAML. A YAML-only
|
||||
doc (with `openapi: 3.0.0` at the top, no JSON braces) fails JSON parse
|
||||
immediately and goes to the YAML path. Net result: JSON docs are never
|
||||
exposed to YAML's type coercion; YAML docs are parsed as YAML.
|
||||
|
||||
`from_yaml` (the explicit constructor) does **not** try JSON first — the
|
||||
caller has declared the format. If the caller is wrong (passes JSON to
|
||||
`from_yaml`), the YAML parser handles it (JSON is a syntactic subset of
|
||||
YAML, so it parses) but with the same silent string→boolean coercion as
|
||||
the `from_str` footgun above. The caller opted in by naming the format;
|
||||
`from_str` exists for the unsure caller.
|
||||
|
||||
### Dependency: yaml_serde (ADR-051 §3)
|
||||
|
||||
The original `serde_yaml` crate (dtolnay) is no longer maintained. The
|
||||
official YAML organization maintains a continuation published as
|
||||
`yaml_serde` (crate name `yaml_serde`, v0.10), a drop-in fork with full
|
||||
API compatibility. `alknet-http` uses the direct form
|
||||
(`yaml_serde = "0.10"`, `use yaml_serde::`).
|
||||
|
||||
Add to `crates/alknet-http/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
yaml_serde = "0.10"
|
||||
```
|
||||
|
||||
The dependency is **not feature-gated**. YAML OpenAPI schemas are a
|
||||
first-class input format (vast.ai publishes one), not an edge case. Gating
|
||||
it behind a feature would mean a deployment that imports vast.ai must
|
||||
remember to enable the feature — the kind of friction the no-surprises
|
||||
default-features model avoids. The dependency is small (pure Rust, no
|
||||
native code).
|
||||
|
||||
### to_openapi output stays JSON (ADR-051 §4 — scope boundary)
|
||||
|
||||
This task is consume-side only. `to_openapi` generates the published
|
||||
gateway doc, served at `GET /openapi.json`, and stays JSON. YAML publish
|
||||
output is out of scope; it would be a separate ADR if a concrete consumer
|
||||
requires it. Do not add a `GET /openapi.yaml` endpoint or a YAML serializer
|
||||
to `to_openapi`.
|
||||
|
||||
### What this task does NOT do
|
||||
|
||||
- **No forwarding handler changes.** The handler operates on the parsed
|
||||
`OpenAPISpec`, which is format-independent. JSON and YAML both produce
|
||||
the same `serde_json::Value`-based internal type.
|
||||
- **No `to_openapi` changes.** Output stays JSON.
|
||||
- **No `from_mcp`/`to_mcp` changes.** Unrelated adapters.
|
||||
- **No provenance / visibility / error-fidelity changes.** All
|
||||
unchanged — they operate on the parsed spec, not the wire format.
|
||||
- **No new ADRs or OQs.** ADR-051 is Accepted; this task implements it.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `yaml_serde = "0.10"` added to `crates/alknet-http/Cargo.toml`
|
||||
`[dependencies]` (not feature-gated)
|
||||
- [ ] `OpenAPISpec::from_yaml(doc: &str) -> Result<Self, AdapterError>`
|
||||
— parses YAML to `serde_json::Value` via `yaml_serde::from_str`,
|
||||
then feeds the existing `from_value` path
|
||||
- [ ] `OpenAPISpec::from_str(doc: &str) -> Result<Self, AdapterError>`
|
||||
— tries `serde_json::from_str` first; on failure, falls back to
|
||||
`from_yaml`. This is the correctness guard.
|
||||
- [ ] `from_json` and `from_value` unchanged (existing behavior preserved)
|
||||
- [ ] New constructors re-exported from `adapters/mod.rs` if needed
|
||||
(they're methods on the already-re-exported `OpenAPISpec`, so likely
|
||||
no change required — verify)
|
||||
- [ ] YAML parse failure → `AdapterError::SchemaParse` (same variant as
|
||||
JSON parse failure; the error message should name YAML)
|
||||
- [ ] Unit test: `from_yaml` parses a minimal YAML OpenAPI doc → one
|
||||
`HandlerRegistration` (mirror of `import_minimal_doc_yields_one_registration`
|
||||
with a YAML fixture)
|
||||
- [ ] Unit test: `from_yaml` parses a YAML doc with `$ref` resolution
|
||||
(mirror of `ref_resolution_in_input_schema` with YAML)
|
||||
- [ ] Unit test: `from_str` on a JSON doc → succeeds via the JSON path
|
||||
- [ ] Unit test: `from_str` on a YAML doc (no JSON braces) → succeeds via
|
||||
the YAML fallback path
|
||||
- [ ] Unit test: **the correctness guard** — `from_str` on a JSON doc
|
||||
with a string field `"yes"` preserves it as a string (not coerced
|
||||
to boolean by the YAML path). E.g.,
|
||||
`{"active": "yes"}` → `input_schema` has `"active"` as a string,
|
||||
not a boolean. This is the test that proves JSON-first works.
|
||||
- [ ] Unit test: `from_yaml` on a JSON doc with `"yes"` string → the
|
||||
string IS coerced to boolean (documents the opt-in behavior — the
|
||||
caller explicitly chose YAML, so YAML's rules apply)
|
||||
- [ ] Unit test: malformed YAML → `AdapterError::SchemaParse`
|
||||
- [ ] Unit test: `from_str` on malformed-both (not valid JSON, not valid
|
||||
YAML) → `AdapterError::SchemaParse`
|
||||
- [ ] Existing JSON tests still pass (no regression)
|
||||
- [ ] `cargo test -p alknet-http` succeeds
|
||||
- [ ] `cargo clippy -p alknet-http --all-targets` succeeds with no warnings
|
||||
- [ ] `cargo fmt --check -p alknet-http` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/051-yaml-input-for-from-openapi.md — ADR-051
|
||||
(full rationale: API shape, JSON-first correctness rule, yaml_serde
|
||||
choice, to_openapi stays JSON)
|
||||
- docs/architecture/crates/http/http-adapters.md — §"Type definitions"
|
||||
(constructor signatures), §Constraints (the two new bullets), §Design
|
||||
Decisions table row
|
||||
- docs/architecture/crates/http/overview.md — dependency list (yaml_serde
|
||||
added), Feature Gates section (yaml_serde not feature-gated)
|
||||
- crates/alknet-http/src/adapters/from_openapi.rs — existing `from_json`
|
||||
and `from_value` to mirror; existing tests to mirror with YAML fixtures
|
||||
- crates/alknet-http/Cargo.toml — add the `yaml_serde` dependency
|
||||
- https://github.com/yaml/yaml-serde — `yaml_serde` crate (maintained
|
||||
official-YAML-org fork of the deprecated `serde_yaml`)
|
||||
|
||||
## Notes
|
||||
|
||||
> The non-obvious part is the JSON-first detection rule — it's a
|
||||
> correctness guard against YAML 1.1 boolean coercion, not a style
|
||||
> preference. The test that matters most is the one proving a JSON doc
|
||||
> with `"yes"` string fields survives `from_str` unchanged (JSON path
|
||||
> wins, no YAML coercion). The complementary test documents that
|
||||
> `from_yaml` (explicit) *does* coerce — the caller opted in. The
|
||||
> existing `from_json`/`from_value` constructors and the entire
|
||||
> forwarding-handler / error-fidelity / no-env-vars path are unchanged —
|
||||
> this task only adds new parse entry points into the same internal type.
|
||||
> `to_openapi` output stays JSON (the gap is consume-side, not
|
||||
> publish-side). No feature gate on `yaml_serde` — YAML OpenAPI is
|
||||
> first-class.
|
||||
Reference in new issue
Block a user