src/lightspeed@form.erl

-module(lightspeed@form).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/form.gleam").
-export([empty/0, from_entries/1, parse_payload/1, fields/1, to_entries/1, value/2, values/2, require/2, int/2, bool/2, checked/2, put/3, error_to_string/1]).
-export_type([field/0, form_data/0, form_error/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(" Form binding helpers for event payloads and typed form values.\n").

-type field() :: {field, binary(), binary()}.

-type form_data() :: {form_data, list(field())}.

-type form_error() :: {missing_field, binary()} |
    {invalid_integer, binary(), binary()} |
    {invalid_boolean, binary(), binary()}.

-file("src/lightspeed/form.gleam", 26).
?DOC(" Empty form data.\n").
-spec empty() -> form_data().
empty() ->
    {form_data, []}.

-file("src/lightspeed/form.gleam", 130).
-spec from_entries_loop(list({binary(), binary()}), list(field())) -> list(field()).
from_entries_loop(Entries, Fields_rev) ->
    case Entries of
        [] ->
            lists:reverse(Fields_rev);

        [{Name, Value} | Rest] ->
            from_entries_loop(Rest, [{field, Name, Value} | Fields_rev])
    end.

-file("src/lightspeed/form.gleam", 31).
?DOC(" Build form data from key/value entries.\n").
-spec from_entries(list({binary(), binary()})) -> form_data().
from_entries(Entries) ->
    {form_data, from_entries_loop(Entries, [])}.

-file("src/lightspeed/form.gleam", 183).
-spec normalize_component(binary()) -> binary().
normalize_component(Value) ->
    _pipe = Value,
    gleam@string:replace(_pipe, <<"+"/utf8>>, <<" "/utf8>>).

-file("src/lightspeed/form.gleam", 175).
-spec join_with_equals(list(binary())) -> binary().
join_with_equals(Parts) ->
    case Parts of
        [] ->
            <<""/utf8>>;

        [Part] ->
            Part;

        [Part@1 | Rest] ->
            <<<<Part@1/binary, "="/utf8>>/binary,
                (join_with_equals(Rest))/binary>>
    end.

-file("src/lightspeed/form.gleam", 166).
-spec split_pair(binary()) -> {binary(), binary()}.
split_pair(Pair) ->
    case gleam@string:split(Pair, <<"="/utf8>>) of
        [] ->
            {<<""/utf8>>, <<""/utf8>>};

        [Name] ->
            {Name, <<""/utf8>>};

        [Name@1, Value] ->
            {Name@1, Value};

        [Name@2, Value@1 | Rest] ->
            {Name@2,
                <<<<Value@1/binary, "="/utf8>>/binary,
                    (join_with_equals(Rest))/binary>>}
    end.

-file("src/lightspeed/form.gleam", 240).
-spec concat(list(HTW), list(HTW)) -> list(HTW).
concat(Left, Right) ->
    case Left of
        [] ->
            Right;

        [Entry | Rest] ->
            [Entry | concat(Rest, Right)]
    end.

-file("src/lightspeed/form.gleam", 141).
-spec parse_pairs(list(binary()), list(field()), list(field())) -> list(field()).
parse_pairs(Pairs, Fields_rev, Buffer) ->
    case Pairs of
        [] ->
            lists:reverse(concat(Buffer, Fields_rev));

        [Pair | Rest] ->
            case split_pair(Pair) of
                {Name, Value} ->
                    parse_pairs(
                        Rest,
                        [{field,
                                normalize_component(Name),
                                normalize_component(Value)} |
                            Fields_rev],
                        Buffer
                    )
            end
    end.

-file("src/lightspeed/form.gleam", 39).
?DOC(
    " Parse a URL-form-encoded style payload (`name=value&next=...`).\n"
    "\n"
    " This parser keeps values as plain strings and does not perform percent\n"
    " decoding. It is deterministic and suitable for typed server decoders.\n"
).
-spec parse_payload(binary()) -> form_data().
parse_payload(Payload) ->
    case Payload of
        <<""/utf8>> ->
            empty();

        _ ->
            _pipe = Payload,
            _pipe@1 = gleam@string:split(_pipe, <<"&"/utf8>>),
            _pipe@2 = parse_pairs(_pipe@1, [], []),
            {form_data, _pipe@2}
    end.

-file("src/lightspeed/form.gleam", 51).
?DOC(" Return all fields.\n").
-spec fields(form_data()) -> list(field()).
fields(Form) ->
    erlang:element(2, Form).

-file("src/lightspeed/form.gleam", 188).
-spec to_entries_loop(list(field()), list({binary(), binary()})) -> list({binary(),
    binary()}).
to_entries_loop(Fields, Entries_rev) ->
    case Fields of
        [] ->
            lists:reverse(Entries_rev);

        [{field, Name, Value} | Rest] ->
            to_entries_loop(Rest, [{Name, Value} | Entries_rev])
    end.

-file("src/lightspeed/form.gleam", 56).
?DOC(" Convert form data back into entries.\n").
-spec to_entries(form_data()) -> list({binary(), binary()}).
to_entries(Form) ->
    to_entries_loop(erlang:element(2, Form), []).

-file("src/lightspeed/form.gleam", 199).
-spec find_first(list(field()), binary()) -> gleam@option:option(binary()).
find_first(Fields, Name) ->
    case Fields of
        [] ->
            none;

        [{field, Field_name, Field_value} | Rest] ->
            case Field_name =:= Name of
                true ->
                    {some, Field_value};

                false ->
                    find_first(Rest, Name)
            end
    end.

-file("src/lightspeed/form.gleam", 61).
?DOC(" Return first value for a field.\n").
-spec value(form_data(), binary()) -> gleam@option:option(binary()).
value(Form, Name) ->
    find_first(erlang:element(2, Form), Name).

-file("src/lightspeed/form.gleam", 210).
-spec find_all(list(field()), binary(), list(binary())) -> list(binary()).
find_all(Fields, Name, Values_rev) ->
    case Fields of
        [] ->
            lists:reverse(Values_rev);

        [{field, Field_name, Field_value} | Rest] ->
            case Field_name =:= Name of
                true ->
                    find_all(Rest, Name, [Field_value | Values_rev]);

                false ->
                    find_all(Rest, Name, Values_rev)
            end
    end.

-file("src/lightspeed/form.gleam", 66).
?DOC(" Return all values for a field.\n").
-spec values(form_data(), binary()) -> list(binary()).
values(Form, Name) ->
    find_all(erlang:element(2, Form), Name, []).

-file("src/lightspeed/form.gleam", 71).
?DOC(" Return a required field or an explicit missing-field error.\n").
-spec require(form_data(), binary()) -> {ok, binary()} | {error, form_error()}.
require(Form, Name) ->
    case value(Form, Name) of
        {some, Field_value} ->
            {ok, Field_value};

        none ->
            {error, {missing_field, Name}}
    end.

-file("src/lightspeed/form.gleam", 79).
?DOC(" Parse a required integer field.\n").
-spec int(form_data(), binary()) -> {ok, integer()} | {error, form_error()}.
int(Form, Name) ->
    case require(Form, Name) of
        {error, Error} ->
            {error, Error};

        {ok, Field_value} ->
            case gleam_stdlib:parse_int(Field_value) of
                {ok, Parsed} ->
                    {ok, Parsed};

                {error, _} ->
                    {error, {invalid_integer, Name, Field_value}}
            end
    end.

-file("src/lightspeed/form.gleam", 91).
?DOC(" Parse a required boolean field.\n").
-spec bool(form_data(), binary()) -> {ok, boolean()} | {error, form_error()}.
bool(Form, Name) ->
    case require(Form, Name) of
        {error, Error} ->
            {error, Error};

        {ok, Field_value} ->
            case Field_value of
                <<"true"/utf8>> ->
                    {ok, true};

                <<"false"/utf8>> ->
                    {ok, false};

                <<"1"/utf8>> ->
                    {ok, true};

                <<"0"/utf8>> ->
                    {ok, false};

                <<"on"/utf8>> ->
                    {ok, true};

                <<"off"/utf8>> ->
                    {ok, false};

                _ ->
                    {error, {invalid_boolean, Name, Field_value}}
            end
    end.

-file("src/lightspeed/form.gleam", 108).
?DOC(" Presence helper for checkbox-style fields.\n").
-spec checked(form_data(), binary()) -> boolean().
checked(Form, Name) ->
    case value(Form, Name) of
        {some, _} ->
            true;

        none ->
            false
    end.

-file("src/lightspeed/form.gleam", 225).
-spec remove_name(list(field()), binary(), list(field())) -> list(field()).
remove_name(Fields, Name, Kept_rev) ->
    case Fields of
        [] ->
            lists:reverse(Kept_rev);

        [Field | Rest] ->
            case erlang:element(2, Field) =:= Name of
                true ->
                    remove_name(Rest, Name, Kept_rev);

                false ->
                    remove_name(Rest, Name, [Field | Kept_rev])
            end
    end.

-file("src/lightspeed/form.gleam", 116).
?DOC(" Upsert one form field value.\n").
-spec put(form_data(), binary(), binary()) -> form_data().
put(Form, Name, Value) ->
    Remaining = remove_name(erlang:element(2, Form), Name, []),
    {form_data, [{field, Name, Value} | Remaining]}.

-file("src/lightspeed/form.gleam", 122).
?DOC(" Render stable error string for logs or telemetry.\n").
-spec error_to_string(form_error()) -> binary().
error_to_string(Error) ->
    case Error of
        {missing_field, Name} ->
            <<"missing_field:"/utf8, Name/binary>>;

        {invalid_integer, Name@1, Value} ->
            <<<<<<"invalid_integer:"/utf8, Name@1/binary>>/binary, ":"/utf8>>/binary,
                Value/binary>>;

        {invalid_boolean, Name@2, Value@1} ->
            <<<<<<"invalid_boolean:"/utf8, Name@2/binary>>/binary, ":"/utf8>>/binary,
                Value@1/binary>>
    end.