Packages
oaspec
0.68.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
src/oaspec@transport.erl
-module(oaspec@transport).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/oaspec/transport.gleam").
-export([method_to_wire/1, method_from_string/1, from_callback/1, resolve/1, run/2, map/2, await/2, map_try/2, try_await/2, credentials/0, with_api_key/3, with_bearer_token/3, with_basic_auth/3, with_digest_auth/3, with_base_url/2, with_default_header/3, with_default_headers/2, with_security/2]).
-export_type([method/0, method_error/0, body/0, security_requirement/0, security_alternative/0, request/0, response/0, transport_error/0, async/1, credentials/0, credential_entry/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Pure, runtime-agnostic transport contract for generated OpenAPI clients.\n"
"\n"
" Generated client code depends on this module instead of any concrete\n"
" HTTP runtime. Adapters (e.g. `oaspec/httpc`, `oaspec/fetch`) bridge\n"
" `Send` / `AsyncSend` to a real runtime; tests can plug in arbitrary\n"
" fake transport values via `oaspec/mock`.\n"
).
-type method() :: get |
post |
put |
delete |
patch |
head |
options |
trace |
connect |
{other, binary()}.
-type method_error() :: {invalid_method, binary()}.
-type body() :: empty_body | {text_body, binary()} | {bytes_body, bitstring()}.
-type security_requirement() :: {api_key_header, binary(), binary()} |
{api_key_query, binary(), binary()} |
{api_key_cookie, binary(), binary()} |
{http_authorization, binary(), binary()}.
-type security_alternative() :: {security_alternative,
list(security_requirement())}.
-type request() :: {request,
method(),
gleam@option:option(binary()),
binary(),
list({binary(), binary()}),
list({binary(), binary()}),
body(),
list(security_alternative())}.
-type response() :: {response, integer(), list({binary(), binary()}), body()}.
-type transport_error() :: {connection_failed, binary()} |
timeout |
{invalid_base_url, binary()} |
{tls_failure, binary()} |
{unsupported, binary()}.
-opaque async(ACQC) :: {async, fun((fun((ACQC) -> nil)) -> nil)}.
-opaque credentials() :: {credentials, list(credential_entry())}.
-type credential_entry() :: {cred_api_key, binary(), binary()} |
{cred_bearer, binary(), binary()} |
{cred_basic, binary(), binary()} |
{cred_digest, binary(), binary()}.
-file("src/oaspec/transport.gleam", 54).
?DOC(
" Convert a `Method` to its on-wire string. Well-known variants\n"
" produce their canonical RFC 9110 spelling (`Get` → `\"GET\"`);\n"
" `Other(s)` returns `s` verbatim — pre-normalise via\n"
" `method_from_string` if the source is in a non-canonical case.\n"
).
-spec method_to_wire(method()) -> binary().
method_to_wire(Method) ->
case Method of
get ->
<<"GET"/utf8>>;
post ->
<<"POST"/utf8>>;
put ->
<<"PUT"/utf8>>;
delete ->
<<"DELETE"/utf8>>;
patch ->
<<"PATCH"/utf8>>;
head ->
<<"HEAD"/utf8>>;
options ->
<<"OPTIONS"/utf8>>;
trace ->
<<"TRACE"/utf8>>;
connect ->
<<"CONNECT"/utf8>>;
{other, S} ->
S
end.
-file("src/oaspec/transport.gleam", 113).
-spec is_tchar(integer()) -> boolean().
is_tchar(Cp) ->
Codepoint = gleam_stdlib:identity(Cp),
case Codepoint of
16#21 ->
true;
16#23 ->
true;
16#24 ->
true;
16#25 ->
true;
16#26 ->
true;
16#27 ->
true;
16#2A ->
true;
16#2B ->
true;
16#2D ->
true;
16#2E ->
true;
16#5E ->
true;
16#5F ->
true;
16#60 ->
true;
16#7C ->
true;
16#7E ->
true;
_ ->
(((Codepoint >= 16#30) andalso (Codepoint =< 16#39)) orelse ((Codepoint
>= 16#41)
andalso (Codepoint =< 16#5A)))
orelse ((Codepoint >= 16#61) andalso (Codepoint =< 16#7A))
end.
-file("src/oaspec/transport.gleam", 104).
-spec is_valid_token(binary()) -> boolean().
is_valid_token(S) ->
_pipe = gleam@string:to_utf_codepoints(S),
gleam@list:all(_pipe, fun is_tchar/1).
-file("src/oaspec/transport.gleam", 78).
?DOC(
" Smart constructor for `Method`. Routes case-insensitively to the\n"
" nine RFC 9110 §9 variants for known names; for everything else,\n"
" validates the input against the `tchar` charset (RFC 9110 §5.6.2)\n"
" and uppercases the result before wrapping in `Other` so the wire\n"
" representation is canonical.\n"
"\n"
" Empty input or a byte outside `tchar` (control bytes, whitespace,\n"
" or separators like `(`, `)`, `,`, `;`, `:`, `/`, `[`, etc.) returns\n"
" `Error(InvalidMethod(detail))`.\n"
).
-spec method_from_string(binary()) -> {ok, method()} | {error, method_error()}.
method_from_string(S) ->
case string:lowercase(S) of
<<"get"/utf8>> ->
{ok, get};
<<"post"/utf8>> ->
{ok, post};
<<"put"/utf8>> ->
{ok, put};
<<"delete"/utf8>> ->
{ok, delete};
<<"patch"/utf8>> ->
{ok, patch};
<<"head"/utf8>> ->
{ok, head};
<<"options"/utf8>> ->
{ok, options};
<<"trace"/utf8>> ->
{ok, trace};
<<"connect"/utf8>> ->
{ok, connect};
<<""/utf8>> ->
{error,
{invalid_method,
<<"method string is empty (RFC 9110 §5.6.2)"/utf8>>}};
_ ->
case is_valid_token(S) of
true ->
{ok, {other, string:uppercase(S)}};
false ->
{error,
{invalid_method,
<<<<"method `"/utf8, S/binary>>/binary,
"` contains a byte outside the tchar charset (RFC 9110 §5.6.2)"/utf8>>}}
end
end.
-file("src/oaspec/transport.gleam", 215).
-spec from_callback(fun((fun((ACQK) -> nil)) -> nil)) -> async(ACQK).
from_callback(Register) ->
{async, Register}.
-file("src/oaspec/transport.gleam", 221).
-spec resolve(ACQM) -> async(ACQM).
resolve(Value) ->
{async, fun(Done) -> Done(Value) end}.
-file("src/oaspec/transport.gleam", 225).
-spec run(async(ACQO), fun((ACQO) -> nil)) -> nil.
run(Async, Done) ->
{async, Register} = Async,
Register(Done).
-file("src/oaspec/transport.gleam", 230).
-spec map(async(ACQQ), fun((ACQQ) -> ACQS)) -> async(ACQS).
map(Async, With_) ->
{async, fun(Done) -> run(Async, fun(Value) -> Done(With_(Value)) end) end}.
-file("src/oaspec/transport.gleam", 234).
-spec await(async(ACQU), fun((ACQU) -> async(ACQW))) -> async(ACQW).
await(Async, Next) ->
{async,
fun(Done) -> run(Async, fun(Value) -> run(Next(Value), Done) end) end}.
-file("src/oaspec/transport.gleam", 240).
-spec map_try(
async({ok, ACQZ} | {error, ACRA}),
fun((ACQZ) -> {ok, ACRE} | {error, ACRA})
) -> async({ok, ACRE} | {error, ACRA}).
map_try(Async, With_) ->
_pipe = Async,
map(_pipe, fun(Result) -> case Result of
{ok, Value} ->
With_(Value);
{error, Error} ->
{error, Error}
end end).
-file("src/oaspec/transport.gleam", 253).
-spec try_await(
async({ok, ACRK} | {error, ACRL}),
fun((ACRK) -> async({ok, ACRP} | {error, ACRL}))
) -> async({ok, ACRP} | {error, ACRL}).
try_await(Async, Next) ->
_pipe = Async,
await(_pipe, fun(Result) -> case Result of
{ok, Value} ->
Next(Value);
{error, Error} ->
resolve({error, Error})
end end).
-file("src/oaspec/transport.gleam", 284).
-spec credentials() -> credentials().
credentials() ->
{credentials, []}.
-file("src/oaspec/transport.gleam", 320).
-spec add_credential(credentials(), credential_entry()) -> credentials().
add_credential(Creds, Entry) ->
{credentials, Entries} = Creds,
{credentials, lists:append(Entries, [Entry])}.
-file("src/oaspec/transport.gleam", 288).
-spec with_api_key(credentials(), binary(), binary()) -> credentials().
with_api_key(Creds, Scheme_name, Value) ->
add_credential(Creds, {cred_api_key, Scheme_name, Value}).
-file("src/oaspec/transport.gleam", 296).
-spec with_bearer_token(credentials(), binary(), binary()) -> credentials().
with_bearer_token(Creds, Scheme_name, Token) ->
add_credential(Creds, {cred_bearer, Scheme_name, Token}).
-file("src/oaspec/transport.gleam", 304).
-spec with_basic_auth(credentials(), binary(), binary()) -> credentials().
with_basic_auth(Creds, Scheme_name, Value) ->
add_credential(Creds, {cred_basic, Scheme_name, Value}).
-file("src/oaspec/transport.gleam", 312).
-spec with_digest_auth(credentials(), binary(), binary()) -> credentials().
with_digest_auth(Creds, Scheme_name, Value) ->
add_credential(Creds, {cred_digest, Scheme_name, Value}).
-file("src/oaspec/transport.gleam", 349).
-spec with_base_url(fun((request()) -> ACRW), binary()) -> fun((request()) -> ACRW).
with_base_url(Send, Base_url) ->
fun(Req) ->
Send(
{request,
erlang:element(2, Req),
{some, Base_url},
erlang:element(4, Req),
erlang:element(5, Req),
erlang:element(6, Req),
erlang:element(7, Req),
erlang:element(8, Req)}
)
end.
-file("src/oaspec/transport.gleam", 491).
-spec has_header(list({binary(), binary()}), binary()) -> boolean().
has_header(Headers, Name) ->
Lowered = string:lowercase(Name),
gleam@list:any(
Headers,
fun(H) ->
{K, _} = H,
string:lowercase(K) =:= Lowered
end
).
-file("src/oaspec/transport.gleam", 542).
-spec forbidden_byte(binary()) -> gleam@option:option(binary()).
forbidden_byte(S) ->
gleam@bool:guard(
gleam_stdlib:contains_string(S, <<"\r"/utf8>>),
{some, <<"CR (\\r)"/utf8>>},
fun() ->
gleam@bool:guard(
gleam_stdlib:contains_string(S, <<"\n"/utf8>>),
{some, <<"LF (\\n)"/utf8>>},
fun() ->
gleam@bool:guard(
gleam_stdlib:contains_string(S, <<"\x{0000}"/utf8>>),
{some, <<"NUL (\\u{0000})"/utf8>>},
fun() -> none end
)
end
)
end
).
-file("src/oaspec/transport.gleam", 505).
-spec validate_header_name(binary(), binary()) -> nil.
validate_header_name(Api_name, Name) ->
case forbidden_byte(Name) of
none ->
nil;
{some, Byte_label} ->
erlang:error(#{gleam_error => panic,
message => (<<<<<<Api_name/binary,
": header name contains forbidden control byte "/utf8>>/binary,
Byte_label/binary>>/binary,
" — header names must not include CR, LF, or NUL"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"oaspec/transport"/utf8>>,
function => <<"validate_header_name"/utf8>>,
line => 509})
end.
-file("src/oaspec/transport.gleam", 522).
-spec validate_header_value(binary(), binary(), binary()) -> nil.
validate_header_value(Api_name, Name, Value) ->
case forbidden_byte(Value) of
none ->
nil;
{some, Byte_label} ->
erlang:error(#{gleam_error => panic,
message => (<<<<<<<<<<<<Api_name/binary,
": header value for `"/utf8>>/binary,
Name/binary>>/binary,
"` contains forbidden control byte "/utf8>>/binary,
Byte_label/binary>>/binary,
" (CR/LF/NUL enable header injection per RFC 9112 §2.2);"/utf8>>/binary,
" pre-encode binary values via Base64 or RFC 8187"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"oaspec/transport"/utf8>>,
function => <<"validate_header_value"/utf8>>,
line => 530})
end.
-file("src/oaspec/transport.gleam", 382).
?DOC(
" Inject a single default header when the request does not already\n"
" declare it. Header-name comparison is case-insensitive (per RFC 7230),\n"
" so a request that already carries `x-trace-id` blocks a default\n"
" `X-Trace-Id`. Explicit request headers always win — middleware never\n"
" clobbers them — and the helper works with both sync and async send\n"
" functions.\n"
"\n"
" **Validation.** Both `name` and `value` are checked at construction\n"
" time for the absence of CR (`\\r`), LF (`\\n`), and NUL (`\\u{0000}`)\n"
" bytes. Those bytes enable HTTP response-splitting / header\n"
" injection if they reach the wire — see RFC 9112 §2.2. A value that\n"
" contains any of them panics with a structured message naming the\n"
" offending byte and the recommendation: pre-encode binary values\n"
" via Base64 or RFC 8187 before passing them in. The check fires at\n"
" the outer call (i.e. when the wrapper is built), so a static\n"
" misconfiguration surfaces immediately at startup rather than\n"
" per-request.\n"
"\n"
" **Composition order.** Each call wraps the previous send. When two\n"
" `with_default_header` wrappers target the same name (case-insensitive),\n"
" the **outermost** wrapper (the one most recently piped in) wins,\n"
" because the request reaches it first and inserts before the inner\n"
" check runs. This is the *opposite* of the list form below — see\n"
" `with_default_headers` for the in-list rule. Reach for the\n"
" `with_default_headers([...])` shape if you want a single source of\n"
" truth for the dedup ordering.\n"
).
-spec with_default_header(fun((request()) -> ACRX), binary(), binary()) -> fun((request()) -> ACRX).
with_default_header(Send, Name, Value) ->
validate_header_name(<<"oaspec.transport.with_default_header"/utf8>>, Name),
validate_header_value(
<<"oaspec.transport.with_default_header"/utf8>>,
Name,
Value
),
fun(Req) -> case has_header(erlang:element(6, Req), Name) of
true ->
Send(Req);
false ->
Send(
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
erlang:element(5, Req),
lists:append(erlang:element(6, Req), [{Name, Value}]),
erlang:element(7, Req),
erlang:element(8, Req)}
)
end end.
-file("src/oaspec/transport.gleam", 433).
?DOC(
" Inject a list of default headers when the request does not already\n"
" declare them. Iteration order is preserved so callers get\n"
" deterministic ordering on the wire, and the helper works with both\n"
" sync and async send functions.\n"
"\n"
" **Validation.** Every `name` and every `value` in `headers` is\n"
" checked at construction time for the absence of CR, LF, and NUL\n"
" bytes (see `with_default_header` for the rationale). The first\n"
" invalid entry panics with a structured message naming the\n"
" offending byte; the check fires at the outer call, so a static\n"
" misconfiguration surfaces immediately rather than per-request.\n"
"\n"
" **Duplicate names within `headers`.** Header-name comparison is\n"
" case-insensitive (per RFC 7230). When the supplied list contains the\n"
" same name twice (e.g. `[#(\"X-Env\", \"staging\"), #(\"X-Env\", \"prod\")]`),\n"
" the **first occurrence is kept** and subsequent entries with the\n"
" same name are silently dropped. Headers already present on the\n"
" inbound request always win over every entry in `headers` regardless\n"
" of position.\n"
"\n"
" This is the *opposite* of the wrapper form's composition rule (see\n"
" `with_default_header`, where the outermost wrapper wins). The two\n"
" rules are each correct in isolation: the list form picks the first\n"
" caller-supplied entry; the wrapper form picks the most recently\n"
" piped wrapper. Pick one shape per code path and stick to it to\n"
" avoid surprises.\n"
).
-spec with_default_headers(fun((request()) -> ACRY), list({binary(), binary()})) -> fun((request()) -> ACRY).
with_default_headers(Send, Headers) ->
gleam@list:each(
Headers,
fun(Kv) ->
{Name, Value} = Kv,
validate_header_name(
<<"oaspec.transport.with_default_headers"/utf8>>,
Name
),
validate_header_value(
<<"oaspec.transport.with_default_headers"/utf8>>,
Name,
Value
)
end
),
fun(Req) ->
Merged_rev = gleam@list:fold(
Headers,
lists:reverse(erlang:element(6, Req)),
fun(Acc_rev, Kv@1) ->
{Name@1, Value@1} = Kv@1,
case has_header(Acc_rev, Name@1) of
true ->
Acc_rev;
false ->
[{Name@1, Value@1} | Acc_rev]
end
end
),
Send(
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
erlang:element(5, Req),
lists:reverse(Merged_rev),
erlang:element(7, Req),
erlang:element(8, Req)}
)
end.
-file("src/oaspec/transport.gleam", 576).
-spec credential_matches(credential_entry(), security_requirement()) -> boolean().
credential_matches(Cred, Req) ->
case {Cred, Req} of
{{cred_api_key, S, _}, {api_key_header, T, _}} when S =:= T ->
true;
{{cred_api_key, S@1, _}, {api_key_query, T@1, _}} when S@1 =:= T@1 ->
true;
{{cred_api_key, S@2, _}, {api_key_cookie, T@2, _}} when S@2 =:= T@2 ->
true;
{{cred_bearer, S@3, _}, {http_authorization, T@3, Prefix}} when S@3 =:= T@3 ->
string:lowercase(Prefix) =:= <<"bearer"/utf8>>;
{{cred_basic, S@4, _}, {http_authorization, T@4, Prefix@1}} when S@4 =:= T@4 ->
string:lowercase(Prefix@1) =:= <<"basic"/utf8>>;
{{cred_digest, S@5, _}, {http_authorization, T@5, Prefix@2}} when S@5 =:= T@5 ->
string:lowercase(Prefix@2) =:= <<"digest"/utf8>>;
{_, _} ->
false
end.
-file("src/oaspec/transport.gleam", 567).
-spec find_credential(credentials(), security_requirement()) -> gleam@option:option(credential_entry()).
find_credential(Creds, Req) ->
{credentials, Entries} = Creds,
_pipe = gleam@list:find(Entries, fun(C) -> credential_matches(C, Req) end),
gleam@option:from_result(_pipe).
-file("src/oaspec/transport.gleam", 563).
-spec satisfiable(security_alternative(), credentials()) -> boolean().
satisfiable(Alt, Creds) ->
gleam@list:all(
erlang:element(2, Alt),
fun(Req) -> find_credential(Creds, Req) /= none end
).
-file("src/oaspec/transport.gleam", 549).
-spec pick_alternative(list(security_alternative()), credentials()) -> gleam@option:option(security_alternative()).
pick_alternative(Alts, Creds) ->
case Alts of
[] ->
none;
[Alt | Rest] ->
case satisfiable(Alt, Creds) of
true ->
{some, Alt};
false ->
pick_alternative(Rest, Creds)
end
end.
-file("src/oaspec/transport.gleam", 632).
-spec set_authorization(request(), binary(), binary()) -> request().
set_authorization(Req, Prefix, Value) ->
Header_value = <<<<Prefix/binary, " "/utf8>>/binary, Value/binary>>,
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
erlang:element(5, Req),
lists:append(
erlang:element(6, Req),
[{<<"authorization"/utf8>>, Header_value}]
),
erlang:element(7, Req),
erlang:element(8, Req)}.
-file("src/oaspec/transport.gleam", 644).
-spec merge_cookie(request(), binary(), binary()) -> request().
merge_cookie(Req, Cookie_name, Value) ->
Pair = <<<<Cookie_name/binary, "="/utf8>>/binary, Value/binary>>,
Lowered = <<"cookie"/utf8>>,
{Existing, Others} = gleam@list:partition(
erlang:element(6, Req),
fun(H) ->
{K, _} = H,
string:lowercase(K) =:= Lowered
end
),
Merged_value = case Existing of
[] ->
Pair;
[_ | _] ->
_pipe = gleam@list:map(
Existing,
fun(H@1) ->
{_, V} = H@1,
V
end
),
_pipe@1 = lists:append(_pipe, [Pair]),
gleam@string:join(_pipe@1, <<"; "/utf8>>)
end,
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
erlang:element(5, Req),
lists:append(Others, [{<<"Cookie"/utf8>>, Merged_value}]),
erlang:element(7, Req),
erlang:element(8, Req)}.
-file("src/oaspec/transport.gleam", 604).
-spec apply_one(request(), security_requirement(), credential_entry()) -> request().
apply_one(Req, Requirement, Cred) ->
case {Requirement, Cred} of
{{api_key_header, _, Header_name}, {cred_api_key, _, Value}} ->
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
erlang:element(5, Req),
lists:append(erlang:element(6, Req), [{Header_name, Value}]),
erlang:element(7, Req),
erlang:element(8, Req)};
{{api_key_query, _, Query_name}, {cred_api_key, _, Value@1}} ->
{request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
lists:append(erlang:element(5, Req), [{Query_name, Value@1}]),
erlang:element(6, Req),
erlang:element(7, Req),
erlang:element(8, Req)};
{{api_key_cookie, _, Cookie_name}, {cred_api_key, _, Value@2}} ->
merge_cookie(Req, Cookie_name, Value@2);
{{http_authorization, _, Prefix}, {cred_bearer, _, Token}} ->
set_authorization(Req, Prefix, Token);
{{http_authorization, _, Prefix@1}, {cred_basic, _, Value@3}} ->
set_authorization(Req, Prefix@1, Value@3);
{{http_authorization, _, Prefix@2}, {cred_digest, _, Value@4}} ->
set_authorization(Req, Prefix@2, Value@4);
{_, _} ->
Req
end.
-file("src/oaspec/transport.gleam", 591).
-spec apply_alternative(request(), security_alternative(), credentials()) -> request().
apply_alternative(Req, Alt, Creds) ->
gleam@list:fold(
erlang:element(2, Alt),
Req,
fun(Acc, Requirement) -> case find_credential(Creds, Requirement) of
{some, Cred} ->
apply_one(Acc, Requirement, Cred);
none ->
Acc
end end
).
-file("src/oaspec/transport.gleam", 474).
-spec with_security(fun((request()) -> ACSA), credentials()) -> fun((request()) -> ACSA).
with_security(Send, Creds) ->
fun(Req) ->
Prepared = case pick_alternative(erlang:element(8, Req), Creds) of
{some, Alt} ->
apply_alternative(Req, Alt, Creds);
none ->
Req
end,
Send(Prepared)
end.