-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(ACGH) :: {async, fun((fun((ACGH) -> 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((ACGP) -> nil)) -> nil)) -> async(ACGP). from_callback(Register) -> {async, Register}. -file("src/oaspec/transport.gleam", 221). -spec resolve(ACGR) -> async(ACGR). resolve(Value) -> {async, fun(Done) -> Done(Value) end}. -file("src/oaspec/transport.gleam", 225). -spec run(async(ACGT), fun((ACGT) -> nil)) -> nil. run(Async, Done) -> {async, Register} = Async, Register(Done). -file("src/oaspec/transport.gleam", 230). -spec map(async(ACGV), fun((ACGV) -> ACGX)) -> async(ACGX). map(Async, With_) -> {async, fun(Done) -> run(Async, fun(Value) -> Done(With_(Value)) end) end}. -file("src/oaspec/transport.gleam", 234). -spec await(async(ACGZ), fun((ACGZ) -> async(ACHB))) -> async(ACHB). 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, ACHE} | {error, ACHF}), fun((ACHE) -> {ok, ACHJ} | {error, ACHF}) ) -> async({ok, ACHJ} | {error, ACHF}). 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, ACHP} | {error, ACHQ}), fun((ACHP) -> async({ok, ACHU} | {error, ACHQ})) ) -> async({ok, ACHU} | {error, ACHQ}). 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()) -> ACIB), binary()) -> fun((request()) -> ACIB). 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 => (<<<<<>/binary, Byte_label/binary>>/binary, " — header names must not include CR, LF, or NUL"/utf8>>), file => <>, 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 => (<<<<<<<<<<<>/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 => <>, 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()) -> ACIC), binary(), binary()) -> fun((request()) -> ACIC). 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()) -> ACID), list({binary(), binary()})) -> fun((request()) -> ACID). 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 = <<<>/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 = <<<>, {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()) -> ACIF), credentials()) -> fun((request()) -> ACIF). 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.