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 AsyncSend =
  fn(Request) -> Async(Result(Response, TransportError))
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 tchar production 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

pub type Response {
  Response(
    status: Int,
    headers: List(#(String, String)),
    body: Body,
  )
}

Constructors

  • Response(
      status: Int,
      headers: List(#(String, String)),
      body: Body,
    )
pub type SecurityAlternative {
  SecurityAlternative(requirements: List(SecurityRequirement))
}

Constructors

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 await(
  async async: Async(a),
  next next: fn(a) -> Async(b),
) -> Async(b)
pub fn credentials() -> Credentials
pub fn from_callback(
  register register: fn(fn(value) -> Nil) -> Nil,
) -> Async(value)
pub fn map(
  async async: Async(a),
  with with_: fn(a) -> b,
) -> Async(b)
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 resolve(value value: a) -> Async(a)
pub fn run(async async: Async(a), done done: fn(a) -> Nil) -> Nil
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_base_url(
  send send: fn(Request) -> a,
  base_url base_url: String,
) -> fn(Request) -> a
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
Search Document