-module(multipartkit@encoder). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/multipartkit/encoder.gleam"). -export([encode/2, encode_stream/2, encode_form/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -file("src/multipartkit/encoder.gleam", 39). -spec build_header_block(list({binary(), binary()})) -> bitstring(). build_header_block(Headers) -> gleam@list:fold( Headers, <<>>, fun(Acc, Entry) -> Line = gleam_stdlib:bit_array_concat( [gleam_stdlib:identity(erlang:element(1, Entry)), <<": "/utf8>>, gleam_stdlib:identity(erlang:element(2, Entry)), <<"\r\n"/utf8>>] ), gleam@bit_array:append(Acc, Line) end ). -file("src/multipartkit/encoder.gleam", 17). ?DOC( " Encode parts using the supplied boundary.\n" "\n" " The boundary must satisfy the RFC 2046 grammar; otherwise the resulting\n" " message will be invalid. To keep this function pure and total, the encoder\n" " does not validate the boundary.\n" ). -spec encode(binary(), list(multipartkit@part:part())) -> bitstring(). encode(Boundary, Parts) -> Dash = <<"--"/utf8>>, Crlf = <<"\r\n"/utf8>>, Boundary_bytes = gleam_stdlib:identity(Boundary), Initial = <<>>, Body = gleam@list:fold( Parts, Initial, fun(Acc, The_part) -> Header_block = build_header_block(erlang:element(2, The_part)), gleam_stdlib:bit_array_concat( [Acc, Dash, Boundary_bytes, Crlf, Header_block, Crlf, erlang:element(6, The_part), Crlf] ) end ), gleam_stdlib:bit_array_concat([Body, Dash, Boundary_bytes, Dash, Crlf]). -file("src/multipartkit/encoder.gleam", 103). -spec build_stream_header_block(multipartkit@stream:stream_part()) -> bitstring(). build_stream_header_block(Stream_part) -> build_header_block(erlang:element(2, Stream_part)). -file("src/multipartkit/encoder.gleam", 107). -spec stop_after_first_error( gleam@yielder:yielder({ok, bitstring()} | {error, multipartkit@error:multipart_error()}) ) -> gleam@yielder:yielder({ok, bitstring()} | {error, multipartkit@error:multipart_error()}). stop_after_first_error(Source) -> gleam@yielder:transform( Source, false, fun(Seen_error, Item) -> gleam@bool:guard( Seen_error, done, fun() -> {next, Item, gleam@result:is_error(Item)} end ) end ). -file("src/multipartkit/encoder.gleam", 74). ?DOC( " Encode a stream of `StreamPart`s into a yielder of byte chunks.\n" "\n" " Errors propagated from a `StreamPart`'s body iterator are forwarded as\n" " `Error(_)`. After the first error, the yielder is exhausted.\n" ). -spec encode_stream( binary(), gleam@yielder:yielder(multipartkit@stream:stream_part()) ) -> gleam@yielder:yielder({ok, bitstring()} | {error, multipartkit@error:multipart_error()}). encode_stream(Boundary, Parts) -> Boundary_bytes = gleam_stdlib:identity(Boundary), Body = gleam@yielder:flat_map( Parts, fun(The_part) -> Header_block = build_stream_header_block(The_part), Prefix = gleam@yielder:from_list( [{ok, <<"--"/utf8>>}, {ok, Boundary_bytes}, {ok, <<"\r\n"/utf8>>}, {ok, Header_block}, {ok, <<"\r\n"/utf8>>}] ), Suffix = gleam@yielder:from_list([{ok, <<"\r\n"/utf8>>}]), gleam@yielder:append( gleam@yielder:append(Prefix, erlang:element(6, The_part)), Suffix ) end ), Closing = gleam@yielder:from_list( [{ok, <<"--"/utf8>>}, {ok, Boundary_bytes}, {ok, <<"--"/utf8>>}, {ok, <<"\r\n"/utf8>>}] ), stop_after_first_error(gleam@yielder:append(Body, Closing)). -file("src/multipartkit/encoder.gleam", 118). -spec boundary_char(integer()) -> binary(). boundary_char(Index) -> case Index of 0 -> <<"0"/utf8>>; 1 -> <<"1"/utf8>>; 2 -> <<"2"/utf8>>; 3 -> <<"3"/utf8>>; 4 -> <<"4"/utf8>>; 5 -> <<"5"/utf8>>; 6 -> <<"6"/utf8>>; 7 -> <<"7"/utf8>>; 8 -> <<"8"/utf8>>; 9 -> <<"9"/utf8>>; 10 -> <<"a"/utf8>>; 11 -> <<"b"/utf8>>; 12 -> <<"c"/utf8>>; 13 -> <<"d"/utf8>>; 14 -> <<"e"/utf8>>; 15 -> <<"f"/utf8>>; 16 -> <<"g"/utf8>>; 17 -> <<"h"/utf8>>; 18 -> <<"i"/utf8>>; 19 -> <<"j"/utf8>>; 20 -> <<"k"/utf8>>; 21 -> <<"l"/utf8>>; 22 -> <<"m"/utf8>>; 23 -> <<"n"/utf8>>; 24 -> <<"o"/utf8>>; 25 -> <<"p"/utf8>>; 26 -> <<"q"/utf8>>; 27 -> <<"r"/utf8>>; 28 -> <<"s"/utf8>>; 29 -> <<"t"/utf8>>; 30 -> <<"u"/utf8>>; 31 -> <<"v"/utf8>>; 32 -> <<"w"/utf8>>; 33 -> <<"x"/utf8>>; 34 -> <<"y"/utf8>>; _ -> <<"z"/utf8>> end. -file("src/multipartkit/encoder.gleam", 163). -spec random_chars(integer(), binary()) -> binary(). random_chars(Remaining, Acc) -> gleam@bool:guard( Remaining =< 0, Acc, fun() -> Char_index = gleam@int:random(36), random_chars( Remaining - 1, <> ) end ). -file("src/multipartkit/encoder.gleam", 159). -spec generate_boundary() -> binary(). generate_boundary() -> <<"----multipartkit-"/utf8, (random_chars(24, <<""/utf8>>))/binary>>. -file("src/multipartkit/encoder.gleam", 63). ?DOC( " Encode a `Form` and return `#(content_type, body)`.\n" "\n" " `content_type` is the full value to set on the HTTP `Content-Type` header\n" " — for example `multipart/form-data; boundary=----abc123`. The boundary is\n" " generated freshly per call. Two calls on the same `Form` therefore\n" " produce different `content_type` values.\n" "\n" " Note: v0.1.0 uses `gleam/int.random` for boundary character generation.\n" " This is sufficient for collision avoidance with normal payloads but is\n" " not cryptographically strong; do not rely on the boundary for security\n" " invariants.\n" ). -spec encode_form(multipartkit@form:form()) -> {binary(), bitstring()}. encode_form(The_form) -> Boundary = generate_boundary(), Body = encode(Boundary, multipartkit@form:parts(The_form)), Content_type = <<"multipart/form-data; boundary="/utf8, Boundary/binary>>, {Content_type, Body}.