docs(adapters): pin routing, SSE-payload, and percent contracts in the module doc (FWD-18/19)

Module doc gains three sections: the FWD-18 routing rule (placeholder
keys never double-emit as query; structural path values are
INVALID_INPUT), the FWD-19 percent trade-off (% in values always
encoded; % in assembly-supplied template/base text survives verbatim —
documented instead of rejected, per the ADR-066 assembly trust
boundary), and the FWD-17 streaming payload contract. FWD-19 behavior
pinned by a wire-level template test.
This commit is contained in:
2026-08-30 20:56:47 +00:00
parent 857aedb987
commit e3fd4443ab
+79
View File
@@ -38,6 +38,29 @@
//! the buffer grows, so the reassembly buffer can never exceed the
//! line cap.
//!
//! # Streaming payload contract (FWD-17)
//!
//! Each parsed SSE frame becomes one success envelope:
//!
//! - A `data:` payload that is valid JSON surfaces as the decoded value
//! itself — `123` as a number, `"123"` as a string (the raw-text
//! fallback this replaces erased that distinction). This holds even
//! when the frame carries an `event:` name.
//! - Any other payload surfaces as the
//! `{"data": <raw payload>, "event": <name|null>}` wrapper: the raw
//! text stays field-addressable and an upstream's named-event
//! conventions (`event: error`, `event: ping`, …) are visible on
//! non-JSON frames. The name is the frame's `event:` field (WHATWG
//! last-wins); `null` when the upstream sent none — the spec's
//! implicit `message` default is *not* substituted, so "upstream
//! named it" and "default name" remain distinguishable.
//! - An empty payload is `Null`.
//!
//! Known limitation: a JSON body under a named event surfaces as the
//! decoded value only — the event name is not carried on JSON frames;
//! a non-JSON body under a named event is the only combination where
//! both are visible.
//!
//! # Credentials and input routing
//!
//! The forwarding handler is the no-env-vars credential injection point
@@ -56,6 +79,36 @@
//! request header, the declared `GATEWAY_BODY_KEY` property becomes the
//! request body, and every other declared key becomes an upstream query
//! parameter.
//!
//! # Input routing and path placeholders (FWD-18)
//!
//! A key that matches a `{placeholder}` in the path template is consumed
//! by the path and never also emits as a query parameter, regardless of
//! its value's shape — the placeholder check precedes query routing in
//! the request builder. A placeholder renders exactly one literal path
//! segment, so its value must be a scalar: object/array values fail with
//! `INVALID_INPUT` (structural values have no faithful single-segment
//! rendering; splicing the minified JSON into the path was the
//! pre-decision behavior and is rejected now).
//!
//! # Percent handling in the rendered path (FWD-19)
//!
//! Two different contracts apply, by design:
//!
//! - A `%` arriving inside a *value* is always encoded (`%` → `%25`),
//! so values carrying `%2F` cannot be mistaken for this crate's own
//! escapes and a value can never inject URL structure.
//! - A `%` in *template/base text* survives verbatim. Templates and
//! base URLs are assembly-supplied (ADR-066 trust boundary), so an
//! assembly that writes `/s3%2Fkeys` is presumed to mean a
//! pre-encoded segment for upstreams that route `%2F` differently
//! from `/` — that upstream-semantics choice belongs to the assembly,
//! not this crate. No injection results: the surviving `%2F` still
//! forms a single segment (the origin check plus the two-pass
//! percent-encoding over template text see to that), and the
//! value-side rule above means every bare `%` in a rendered path
//! traces to template text the assembly author wrote.
//!
use std::collections::HashMap;
use std::sync::Arc;
@@ -1608,6 +1661,32 @@ mod tests {
}
}
/// FWD-19 pin (documented trade-off, not a rejection): literal `%`
/// in template text is preserved as-is, so an assembly-supplied
/// pre-encoded template like `/s3%2Fkeys` reaches the upstream
/// verbatim (most stacks route `%2F` differently from `/` — the
/// assembly layer owns that choice, ADR-066). A `%` arriving in a
/// *value* is always encoded to `%25`, so the only bare `%` in a
/// rendered path is one the template author placed.
#[test]
fn percent_in_template_text_survives_and_percent_in_values_are_always_encoded() {
let url = request_url(
"https://api.example.com",
"/s3%2Fkeys/{name}",
json!({"name": "x"}),
)
.expect("template text may carry literal percent escapes");
assert_eq!(url.path(), "/s3%2Fkeys/x");
let url = request_url(
"https://api.example.com",
"/files/{name}",
json!({"name": "a%2Fb"}),
)
.expect("value percent is encoded, not preserved");
assert_eq!(url.path(), "/files/a%252Fb");
}
fn ctx_with_capability(namespace: &str, value: String) -> OperationContext {
let mut ctx = noop_context();
ctx.capabilities = Capabilities::new().with_http_token(namespace, value);