Full-surface integration suite (tests/full_surface.rs, mcp feature): - one HttpAdapter over real TCP (ProtocolHandler::handle path) serving gateway endpoints, /openapi.json, /mcp, and the WS channels session - gateway: search/schema/call/subscribe/batch/publish presence, envelope shapes, error fidelity end-to-end - from_openapi import -> Internal-by-default invisible from the wire -> External facade composes it via env.invoke -> upstream HTTP API called end-to-end (ADR-015 composition model exercised) - to_openapi 6-path doc validated against openapiv3 over the wire - to_mcp: MCP client connects to /mcp on the served adapter, lists the 4 gateway tools, search returns ACL-filtered ops (Sub excluded) Production fix: the WS upgrade route was reserved but never wired into HttpAdapter's router (the ws-upgrade-session tests built their own router). Now wired with ws_bearer_auth (401 without a resolvable token) around ws_upgrade_handler. Docs sync: all 28 'Port notes' sections/blockquotes stripped from ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in overview.md, websocket.md, and the README table (open-questions.md was already current). Publish prep: cargo publish --dry-run --allow-dirty succeeds; cargo doc --no-deps warning-free (ADR link targets fixed); feature combinations (default / test-support / mcp / wss / all) compile warning-free under clippy -D warnings. Verified: cargo test (182 lib default), --all-features (227 lib + 29 integration), clippy -D warnings x3 feature sets, fmt, doc, publish --dry-run.
12 KiB
ADR-051: YAML Input Format for from_openapi
Ported from alknet ADR-051 (YAML Input Format for from_openapi); re-targeted to alkhttp.
Status
Accepted
Context
from_openapi imports external HTTP APIs as call-protocol operations by
parsing an OpenAPI document. The http-adapters.md spec (now in this crate's
docs/architecture/) already states the
one-way constraint as "from_openapi accepts a standard OpenAPI 3.x
JSON/YAML doc" — YAML was always part of the intended input contract. The
implementation, however, only ever delivered the JSON half:
OpenAPISpec::from_json(&str). This was fine for providers that publish JSON
OpenAPI schemas (e.g., runpod's openapi.json) but blocks providers that
publish YAML schemas (e.g., vast.ai's openapi.yaml). A coming consumer crate
needs to import vast.ai's operations, which surfaces the gap.
This is gap-filling against an existing constraint, not a new architectural
direction. None of the architecture invariants are touched: OpenAPISpec
stays serde_json::Value-based, the forwarding handler is unchanged, the
no-env-vars credential injection is unchanged, error fidelity is unchanged,
to_openapi output stays JSON. The change is a new parse path into the
same internal type.
Two decisions need recording: the parse strategy (JSON-first, not
parse-everything-as-YAML) and the dependency choice (the maintained
yaml_serde fork, not the deprecated serde_yaml).
Decision
1. from_openapi accepts YAML via a from_yaml constructor and a format-detecting from_str
OpenAPISpec gains two constructors alongside the existing from_json:
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_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). The
detection rule is JSON-first, YAML-fallback (see §2 for why the order
matters): attempt serde_json::from_str; if it parses, use the result; if
it fails, attempt YAML. from_json and from_yaml remain for callers that
know the format and want a precise error on mismatch.
This is an additive API surface change (two-way door — constructors can be
renamed/added; nothing downstream breaks). The constructors produce the same
OpenAPISpec; the rest of the adapter is format-agnostic.
2. Format detection is JSON-first, YAML-fallback — a defensive default, not a style preference
Amendment (2026-07-06): The original §2 cited YAML 1.1 boolean-coercion (
yes/no/on/off→ booleans) as a present hazard with the maintained Rust YAML crates, framing JSON-first as a correctness guard against silent string→boolean mutation. A probe during implementation verified this is factually wrong for the chosen dependency:yaml_serde0.10.x (and the deprecatedserde_yaml0.9) implement the YAML 1.2 core schema, where onlytrue/false(and case variants) are booleans — the bare tokensyes/no/on/off/y/nare plain strings. The coercion hazard the original rationale cited does not exist with this dependency version. The JSON-first rule is retained (Accepted ADR) — the rationale is reframed below as a defensive default, not a guard against a present hazard. The decision did not change; the rationale did.
JSON's grammar is a strict subset of YAML (under YAML 1.2) and never
exhibits any YAML-specific type interpretation. Running a JSON document
through a YAML parser is currently safe with yaml_serde 0.10.x — a
JSON doc like {"active": "yes"} parses through the YAML path with
"yes" intact as a string (YAML 1.2 core schema, verified by the
from_yaml_preserves_bare_yes_as_string_yaml_1_2_behavior test).
JSON-first detection is therefore not guarding against a present hazard
with this dependency; it is a defensive default that locks in the
contract against a future YAML-parser swap. If yaml_serde is ever
swapped for a YAML 1.1 crate (where yes/no/on/off coerce to
booleans), or if a future yaml_serde version tightens its core schema
in a way that introduces type interpretation JSON doesn't have, the
JSON-first rule ensures JSON input cannot be silently mutated by the
YAML path. The contract is durable; the dependency is a two-way door
(§3).
The rule is cheap: from_str tries serde_json::from_str first (strict
grammar, no YAML-specific interpretation), and only on JSON parse failure
falls back to the YAML parser. A YAML-only document (with openapi: 3.0.0
at the top, no JSON braces) fails JSON parse immediately and goes to the
YAML path. The cost is one wasted parse attempt for YAML docs, paid once
at adapter-import time (not per forwarded call — see Consequences).
from_yaml (the explicit constructor) does not try JSON first — the caller
has declared the format. This is correct: a caller that explicitly says
"this is YAML" wants the YAML parse, including whatever type
interpretation the YAML parser applies. 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, with whatever interpretation the YAML
parser's schema applies (currently none for yes/no under YAML 1.2; a
future YAML 1.1 swap would coerce). The caller opted in by naming the
format; from_str exists for the unsure caller.
3. The YAML dependency is yaml_serde (the official YAML org fork of serde_yaml), not the deprecated serde_yaml
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. The migration path is either
serde_yaml = { package = "yaml_serde", version = "0.10" } (keeps
use serde_yaml:: imports) or direct yaml_serde = "0.10" with updated
imports. alkhttp uses the direct form (yaml_serde = "0.10",
use yaml_serde::).
The dependency is a two-way door: yaml_serde can be swapped for another
maintained YAML-serde fork (or a future replacement) by changing the
Cargo line and the imports. The one-way constraint is that alkhttp
owns its YAML parse and produces serde_json::Value (the shared internal
type) — which dependency does the parse is an implementation detail.
yaml_serde is chosen because it is the maintained continuation under
the official YAML umbrella, not because its API is irreversibly
load-bearing.
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 (a pure-Rust YAML parser, no native code), consistent with the existing default-features philosophy of the crate.
4. Scope boundary: to_openapi output is not affected
to_openapi generates the published gateway doc, served at GET /openapi.json. It stays JSON. This ADR fills a gap on the consume side
(importing external YAML schemas); the publish side serves our own
gateway contract and JSON is the standard exchange format for OpenAPI
tooling (code generators, validators, fetch-based clients all consume
JSON). A GET /openapi.yaml additive output is not part of this decision:
it is a separate scope (publish-side format, not consume-side), would be a
separate ADR if a concrete consumer requires YAML output, and is
additive (a new endpoint, no breaking change to the JSON path). The
OpenAPISpec type is shared, but the output serialization is JSON-only.
Consequences
Positive:
from_openapiconsumes both JSON and YAML OpenAPI schemas — the intended contract (spec line: "JSON/YAML doc") is finally delivered. vast.ai and any other YAML-publishing provider can be imported.- Format detection (
from_str) makes fetch-and-import ergonomic: a caller that fetched a schema from a URL with no reliable Content-Type doesn't have to sniff the format itself. - JSON-first detection is a defensive default that locks in the
contract against a future YAML-parser swap. With
yaml_serde0.10.x (YAML 1.2 core schema) the coercion hazard the original rationale cited is not present; JSON-first nonetheless ensures JSON input is never exposed to YAML-specific type interpretation, present or future. The rule is cheap (one wasted parse for YAML docs, paid once at import time) and the contract is durable. - The maintained
yaml_serdefork keeps the dependency off the archivedserde_yaml; the swap is documented so a future maintainer doesn't re-derive why the crate name doesn't match the obvious name.
Negative:
- A new pure-Rust dependency (
yaml_serde) in alkhttp. Small, but non-zero. The trade is first-class YAML support without a feature gate — accepted because YAML OpenAPI is a real input format, not an edge case. from_str's JSON-first detection does one wasted parse attempt for YAML docs (the JSON parse fails, then the YAML parse runs). The cost is trivial —from_openapiruns once at adapter-import time (not per forwarded call), so the double-parse happens once per imported service, not per request. Callers that know the format usefrom_json/from_yamldirectly and pay no double-parse. The defensive benefit (JSON input never reaches the YAML parser, immune to any YAML-specific interpretation present or future) is worth the one-time cost.
Assumptions
-
The
OpenAPISpecinternal type staysserde_json::Value-based. YAML parses toserde_json::Valueviayaml_serde, then feeds the existingfrom_valuepath. No second internal representation. If a future switch toopenapiv3::OpenApihappens (the two-way-door the spec already notes), both JSON and YAML constructors adapt in lockstep — the constructor is the adapter between wire format and internal type. -
yaml_serde0.10.x implements the YAML 1.2 core schema. Verified by a probe during implementation: bareyes/no/on/off/y/nare plain strings, not booleans (codified by thefrom_yaml_preserves_bare_yes_as_string_yaml_1_2_behaviortest). The original §2 rationale cited YAML 1.1 coercion as a present hazard; it is not, with this dependency version. JSON-first detection is retained as a defensive default (§2 as amended): a future swap to a YAML 1.1 crate, or a futureyaml_serdeschema tightening, cannot silently regress JSON input because JSON never reaches the YAML path underfrom_str. If the dependency swaps to a YAML 1.1 crate, the defensive default becomes a load-bearing correctness guard — the rule is the same either way, which is why it is stated as a contract rather than as a workaround for a specific crate version.
References
- ADR-017 —
from_openapiis anOperationAdapter; publishedto_*specs are compatibility contracts (the publish side stays JSON; the decision record is alkcall ADR-022) - ADR-023 — error fidelity is unaffected
(error schemas come from the parsed
OpenAPISpec, format-independent; the decision record is alkcall ADR-016) - ADR-039 — alkhttp owns both HTTP directions and their dependencies
- http-adapters.md — the spec that
states the "JSON/YAML doc" constraint, the
OpenAPISpectype, and the Constraints/Design Decisions entries this ADR backs (see the "Input formats" doc-comment, the Constraints §"from_openapiaccepts JSON and YAML", and the Design Decisions table row; now indocs/architecture/) yaml_serdecrate (https://github.com/yaml/yaml-serde) — the maintained official-YAML-org fork of the deprecatedserde_yaml