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
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-30
|
||||
last_updated: 2026-07-06
|
||||
---
|
||||
|
||||
# alknet-http
|
||||
@@ -19,7 +19,7 @@ protocol), and hosts the HTTP-backed call-protocol adapters
|
||||
| [overview.md](overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
|
||||
| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1` + WS upgrade route), axum over QUIC, Bearer auth, stealth, `/healthz`; WS hands off to the native session spec |
|
||||
| [websocket.md](websocket.md) | draft | WebSocket browser bidirectional path — native `EventEnvelope` call-protocol session (not the gateway shape, ADR-048); framing, dispatch, bidirectionality, connection-local Layer 2 overlay, browsers-are-not-peers rationale, streaming (native `call.responded`, no SSE), deferred `from_wss` adapter |
|
||||
| [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client) and `to_openapi` (OpenAPI projection); no-env-vars invariant point |
|
||||
| [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client; JSON + YAML input per ADR-051) and `to_openapi` (OpenAPI projection); no-env-vars invariant point |
|
||||
| [http-mcp.md](http-mcp.md) | draft | `from_mcp` / `to_mcp` (feature-gated), streamable-HTTP-only, stdio exclusion |
|
||||
| [webtransport.md](webtransport.md) | deferred | `h3`/WebTransport handler — **deferred per ADR-044**; spec kept intact for revival |
|
||||
|
||||
@@ -53,6 +53,8 @@ protocol), and hosts the HTTP-backed call-protocol adapters
|
||||
| [046](../../decisions/046-assembly-layer-custom-http-routes.md) | Assembly-Layer Custom HTTP Routes on HttpAdapter | `extra_routes: Option<Router>` at construction; deployments add raw HTTP endpoints (e.g., OAI-compatible proxy, or a REST-like per-operation projection) that coexist with the default surface; default surface takes precedence on collision |
|
||||
| [047](../../decisions/047-remove-direct-call-http-surface.md) | Remove the Direct-Call HTTP Surface; Gateway Is the Sole Invoke Path | `POST /{service}/{op}` direct-call surface removed; the 5 gateway endpoints are the sole invoke path; per-caller `AccessControl`-filtered `/search` is the discovery; ADR-036's non-routing clauses survive |
|
||||
| [048](../../decisions/048-websocket-native-session-not-gateway.md) | WebSocket Carries the Native Call-Protocol Session, Not the Gateway Shape | WS is the native `EventEnvelope` session; the gateway endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are HTTP-only and do not appear on WS; discovery via `services/list`/`services/schema` as call-protocol ops |
|
||||
| [049](../../decisions/049-streaming-handler-for-subscriptions.md) | Streaming Handler for Subscription Operations | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE → `BoxStream<ResponseEnvelope>` |
|
||||
| [051](../../decisions/051-yaml-input-for-from-openapi.md) | YAML Input Format for from_openapi | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (correctness rule, §2); YAML dep is `yaml_serde` (maintained fork of deprecated `serde_yaml`); `to_openapi` output stays JSON (out of scope, §4) |
|
||||
|
||||
## Relevant Open Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-02
|
||||
last_updated: 2026-07-06
|
||||
---
|
||||
|
||||
# HTTP Adapters — from_openapi and to_openapi
|
||||
@@ -46,15 +46,27 @@ impl OperationAdapter for FromOpenAPI {
|
||||
#### Type definitions
|
||||
|
||||
```rust
|
||||
/// A parsed OpenAPI document. The concrete type is a two-way-door
|
||||
/// implementation detail (openapiv3::OpenApi, a local alknet-http
|
||||
/// type, or a serde_json::Value-based parse); the one-way constraint is
|
||||
/// that `from_openapi` accepts a standard OpenAPI 3.x JSON/YAML doc and
|
||||
/// A parsed OpenAPI document. The internal representation is
|
||||
/// `serde_json::Value`-based (ADR-051 §Assumptions #1) — both JSON and
|
||||
/// YAML parse paths produce the same `serde_json::Value` tree, then feed
|
||||
/// the existing `from_value` constructor. A future swap to
|
||||
/// `openapiv3::OpenApi` is a two-way door: both JSON and YAML constructors
|
||||
/// adapt in lockstep, since the constructor is the adapter between wire
|
||||
/// format and internal type. The one-way constraint is that
|
||||
/// `from_openapi` accepts a standard OpenAPI 3.x JSON/YAML doc and
|
||||
/// `to_openapi` produces one. Both directions share the same Rust type,
|
||||
/// but not the same document shape: `from_openapi` consumes traditional
|
||||
/// per-operation-paths docs (one path per operation), while `to_openapi`
|
||||
/// produces the 5-endpoint gateway doc (ADR-042). The type is shared;
|
||||
/// the shape is not.
|
||||
///
|
||||
/// Input formats (ADR-051): `from_openapi` accepts both JSON and YAML.
|
||||
/// JSON is parsed via `serde_json`; YAML via `yaml_serde` (the maintained
|
||||
/// fork of the deprecated `serde_yaml`). Both paths produce the same
|
||||
/// `serde_json::Value`-based internal type — there is one
|
||||
/// `OpenAPISpec`, not a JSON and a YAML variant. `from_str` detects
|
||||
/// format by trying JSON first and falling back to YAML (correctness
|
||||
/// rule — ADR-051 §2).
|
||||
pub struct OpenAPISpec {
|
||||
pub info: OpenAPIInfo,
|
||||
pub paths: BTreeMap<String, PathItem>,
|
||||
@@ -62,6 +74,13 @@ pub struct OpenAPISpec {
|
||||
// ... OpenAPI 3.x fields as needed
|
||||
}
|
||||
|
||||
impl OpenAPISpec {
|
||||
pub fn from_json(doc: &str) -> Result<Self, AdapterError>; // JSON input
|
||||
pub fn from_yaml(doc: &str) -> Result<Self, AdapterError>; // YAML input
|
||||
pub fn from_str(doc: &str) -> Result<Self, AdapterError>; // format-detecting (JSON-first, YAML-fallback — ADR-051 §2)
|
||||
pub fn from_value(raw: Value) -> Result<Self, AdapterError>; // pre-parsed serde_json::Value
|
||||
}
|
||||
|
||||
/// Configuration for an HTTP-backed adapter (`from_openapi`). Carries
|
||||
/// the base URL, auth credentials (from `Capabilities` at registration,
|
||||
/// not env vars — the no-env-vars invariant), and optional headers. The
|
||||
@@ -85,8 +104,10 @@ pub enum HttpAuthScheme {
|
||||
The adapter:
|
||||
|
||||
1. Parses the OpenAPI document (`OpenAPISpec` — `paths`, `components`,
|
||||
`$ref` resolution). On parse failure, returns
|
||||
`AdapterError::SchemaParse`. The TS prior art
|
||||
`$ref` resolution). Accepts JSON or YAML (ADR-051 — JSON via
|
||||
`serde_json`, YAML via `yaml_serde`; `from_str` detects format
|
||||
JSON-first/YAML-fallback, a correctness rule — ADR-051 §2). On parse
|
||||
failure, returns `AdapterError::SchemaParse`. The TS prior art
|
||||
(`@alkdev/operations/src/from_openapi.ts`) shows the parsing patterns:
|
||||
`resolveRef` for `$ref`, `resolveRefsRecursive` for nested refs,
|
||||
`buildInputSchema` (parameters + request body → input JSON Schema),
|
||||
@@ -396,8 +417,17 @@ once published, the 5-endpoint gateway shape is one-way.
|
||||
The handler that composes them is `External`.
|
||||
- **`from_openapi` error codes are prefixed `HTTP_<status>`.** No
|
||||
collision with protocol-level codes (ADR-023, review #002 W20).
|
||||
- **`from_openapi` accepts JSON and YAML; `from_str` detects format
|
||||
JSON-first.** JSON-first is a correctness rule (ADR-051 §2), not a
|
||||
style preference. `from_str` tries JSON first (strict grammar), falls
|
||||
back to YAML only if JSON parse fails. `from_json`/`from_yaml` are
|
||||
the explicit constructors for callers that know the format.
|
||||
- **`to_openapi` is a pure projection.** It consumes the registry, does
|
||||
not produce entries for it. Not an `OperationAdapter`.
|
||||
- **`to_openapi` output is JSON.** The published gateway doc is served at
|
||||
`GET /openapi.json`. YAML output is out of scope (ADR-051 §4); the gap
|
||||
this fills is on the consume side (importing external YAML schemas),
|
||||
not the publish side.
|
||||
- **Published `to_openapi` specs are compatibility contracts.** The
|
||||
generated gateway doc carries `info.version` (semver) tracking the
|
||||
**gateway endpoint contract**, not the operation set — per-caller
|
||||
@@ -432,6 +462,7 @@ once published, the 5-endpoint gateway shape is one-way.
|
||||
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered. Supersedes ADR-036's original `to_openapi` "paths mirror `/{service}/{op}`" clause |
|
||||
| `to_openapi` published-spec versioning | [ADR-045](../../decisions/045-to-openapi-gateway-spec-versioning.md) | `info.version` semver tracks the gateway endpoint contract, not the operation set; consumers detect breaking changes via the major version |
|
||||
| Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE response → `BoxStream<ResponseEnvelope>`; `Query`/`Mutation` stay `HandlerKind::Once` |
|
||||
| YAML input + JSON-first format detection | [ADR-051](../../decisions/051-yaml-input-for-from-openapi.md) | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (correctness rule, §2); YAML dep is `yaml_serde`; `to_openapi` output stays JSON (out of scope, §4) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-30
|
||||
last_updated: 2026-07-06
|
||||
---
|
||||
|
||||
# alknet-http — Overview
|
||||
@@ -90,6 +90,7 @@ alknet-http
|
||||
├── axum (HTTP server — Router, extractors, middleware, WebSocket upgrade)
|
||||
├── reqwest (HTTP client — from_openapi/from_mcp forwarding)
|
||||
├── hyper (HTTP/1.1 + HTTP/2 framing; axum is built on hyper)
|
||||
├── yaml_serde (YAML parse for from_openapi YAML input — ADR-051; maintained fork of serde_yaml)
|
||||
└── rmcp (MCP streamable HTTP — feature-gated behind `mcp`)
|
||||
```
|
||||
|
||||
@@ -199,6 +200,12 @@ browser bidirectional path is WebSocket, native to axum, no `h3` feature
|
||||
gate needed. A deployment that wants MCP tool import/export enables
|
||||
`mcp`.
|
||||
|
||||
**`yaml_serde` is not feature-gated** — YAML OpenAPI is a first-class
|
||||
input format (vast.ai publishes a YAML schema), not an edge case. A
|
||||
deployment that imports a YAML-publishing provider must not have to
|
||||
remember a feature flag (ADR-051 §3). The dependency is small (pure
|
||||
Rust, no native code), consistent with the default-features philosophy.
|
||||
|
||||
## The No-Env-Vars Invariant
|
||||
|
||||
The `from_openapi`/`from_mcp` forwarding handlers are the **credential
|
||||
|
||||
Reference in new issue
Block a user