oaspec/transport
Pure, runtime-agnostic transport contract for generated OpenAPI clients.
Generated client code depends on this module instead of any concrete
HTTP runtime. Adapters (e.g. oaspec/httpc, oaspec/fetch) bridge
Send / AsyncSend to a real runtime; tests can plug in arbitrary
fake transport values via oaspec/mock.
Types
Cross-target async value used by generated async clients and adapters. JavaScript adapters can back this with promises; other runtimes can bridge from callbacks or scheduling primitives.
pub opaque type Async(value)
pub type Body {
EmptyBody
TextBody(String)
BytesBody(BitArray)
}
Constructors
-
EmptyBody -
TextBody(String) -
BytesBody(BitArray)
pub opaque type Credentials
pub type Method {
Get
Post
Put
Delete
Patch
Head
Options
Trace
Connect
Other(String)
}
Constructors
-
Get -
Post -
Put -
Delete -
Patch -
Head -
Options -
Trace -
Connect -
Other(String)
Construction error for method_from_string.
pub type MethodError {
InvalidMethod(detail: String)
}
Constructors
-
InvalidMethod(detail: String)The supplied string is empty or contains a byte outside the
tcharproduction from RFC 9110 §5.6.2 (control bytes, whitespace, separators like(,),<,>,@,,,;,:,\,",/,[,],?,=,{,}).
pub type Request {
Request(
method: Method,
base_url: option.Option(String),
path: String,
query: List(#(String, String)),
headers: List(#(String, String)),
body: Body,
security: List(SecurityAlternative),
)
}
Constructors
-
Request( method: Method, base_url: option.Option(String), path: String, query: List(#(String, String)), headers: List(#(String, String)), body: Body, security: List(SecurityAlternative), )
pub type SecurityAlternative {
SecurityAlternative(requirements: List(SecurityRequirement))
}
Constructors
-
SecurityAlternative(requirements: List(SecurityRequirement))
pub type SecurityRequirement {
ApiKeyHeader(scheme_name: String, header_name: String)
ApiKeyQuery(scheme_name: String, query_name: String)
ApiKeyCookie(scheme_name: String, cookie_name: String)
HttpAuthorization(scheme_name: String, prefix: String)
}
Constructors
-
ApiKeyHeader(scheme_name: String, header_name: String) -
ApiKeyQuery(scheme_name: String, query_name: String) -
ApiKeyCookie(scheme_name: String, cookie_name: String) -
HttpAuthorization(scheme_name: String, prefix: String)
pub type Send =
fn(Request) -> Result(Response, TransportError)
pub type TransportError {
ConnectionFailed(detail: String)
Timeout
InvalidBaseUrl(detail: String)
TlsFailure(detail: String)
Unsupported(detail: String)
}
Constructors
-
ConnectionFailed(detail: String) -
Timeout -
InvalidBaseUrl(detail: String) -
TlsFailure(detail: String) -
Unsupported(detail: String)
Values
pub fn credentials() -> Credentials
pub fn from_callback(
register register: fn(fn(value) -> Nil) -> Nil,
) -> Async(value)
pub fn map_try(
async async: Async(Result(a, error)),
with with_: fn(a) -> Result(b, error),
) -> Async(Result(b, error))
pub fn method_from_string(
s: String,
) -> Result(Method, MethodError)
Smart constructor for Method. Routes case-insensitively to the
nine RFC 9110 §9 variants for known names; for everything else,
validates the input against the tchar charset (RFC 9110 §5.6.2)
and uppercases the result before wrapping in Other so the wire
representation is canonical.
Empty input or a byte outside tchar (control bytes, whitespace,
or separators like (, ), ,, ;, :, /, [, etc.) returns
Error(InvalidMethod(detail)).
pub fn method_to_wire(method: Method) -> String
Convert a Method to its on-wire string. Well-known variants
produce their canonical RFC 9110 spelling (Get → "GET");
Other(s) returns s verbatim — pre-normalise via
method_from_string if the source is in a non-canonical case.
pub fn try_await(
async async: Async(Result(a, error)),
next next: fn(a) -> Async(Result(b, error)),
) -> Async(Result(b, error))
pub fn with_api_key(
creds creds: Credentials,
scheme_name scheme_name: String,
value value: String,
) -> Credentials
pub fn with_basic_auth(
creds creds: Credentials,
scheme_name scheme_name: String,
value value: String,
) -> Credentials
pub fn with_bearer_token(
creds creds: Credentials,
scheme_name scheme_name: String,
token token: String,
) -> Credentials
pub fn with_default_header(
send send: fn(Request) -> a,
name name: String,
value value: String,
) -> fn(Request) -> a
Inject a single default header when the request does not already
declare it. Header-name comparison is case-insensitive (per RFC 7230),
so a request that already carries x-trace-id blocks a default
X-Trace-Id. Explicit request headers always win — middleware never
clobbers them — and the helper works with both sync and async send
functions.
Validation. Both name and value are checked at construction
time for the absence of CR (\r), LF (\n), and NUL (\u{0000})
bytes. Those bytes enable HTTP response-splitting / header
injection if they reach the wire — see RFC 9112 §2.2. A value that
contains any of them panics with a structured message naming the
offending byte and the recommendation: pre-encode binary values
via Base64 or RFC 8187 before passing them in. The check fires at
the outer call (i.e. when the wrapper is built), so a static
misconfiguration surfaces immediately at startup rather than
per-request.
Composition order. Each call wraps the previous send. When two
with_default_header wrappers target the same name (case-insensitive),
the outermost wrapper (the one most recently piped in) wins,
because the request reaches it first and inserts before the inner
check runs. This is the opposite of the list form below — see
with_default_headers for the in-list rule. Reach for the
with_default_headers([...]) shape if you want a single source of
truth for the dedup ordering.
pub fn with_default_headers(
send send: fn(Request) -> a,
headers headers: List(#(String, String)),
) -> fn(Request) -> a
Inject a list of default headers when the request does not already declare them. Iteration order is preserved so callers get deterministic ordering on the wire, and the helper works with both sync and async send functions.
Validation. Every name and every value in headers is
checked at construction time for the absence of CR, LF, and NUL
bytes (see with_default_header for the rationale). The first
invalid entry panics with a structured message naming the
offending byte; the check fires at the outer call, so a static
misconfiguration surfaces immediately rather than per-request.
Duplicate names within headers. Header-name comparison is
case-insensitive (per RFC 7230). When the supplied list contains the
same name twice (e.g. [#("X-Env", "staging"), #("X-Env", "prod")]),
the first occurrence is kept and subsequent entries with the
same name are silently dropped. Headers already present on the
inbound request always win over every entry in headers regardless
of position.
This is the opposite of the wrapper form’s composition rule (see
with_default_header, where the outermost wrapper wins). The two
rules are each correct in isolation: the list form picks the first
caller-supplied entry; the wrapper form picks the most recently
piped wrapper. Pick one shape per code path and stick to it to
avoid surprises.
pub fn with_digest_auth(
creds creds: Credentials,
scheme_name scheme_name: String,
value value: String,
) -> Credentials
pub fn with_security(
send send: fn(Request) -> a,
credentials creds: Credentials,
) -> fn(Request) -> a