Skip to main content

src/gleamson@decode.erl

-module(gleamson@decode).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleamson/decode.gleam").
-export([run/2, run_first/2, from_string/2, string/1, int/1, float/1, bool/1, json/1, success/1, failure/2, list/1, field/3, optional_field/3, at/2, dict/1, optional/1, map/2, one_of/2, then/2, index/2, enum/2]).
-export_type([decode_error/0, 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(
    " Combinator decoders that turn a `gleamson.Json` value into typed Gleam data.\n"
    "\n"
    " Decoders here *accumulate* errors: when one field fails, decoding keeps\n"
    " going so you get every problem at once, not just the first. A decoder\n"
    " always produces a best-effort value alongside a list of errors, where\n"
    " failed parts are filled with a zero value; that value is discarded by the\n"
    " runners unless the error list is empty.\n"
    "\n"
    " A `Decoder(t)` is just a function `fn(Json) -> #(t, List(DecodeError))`, so\n"
    " you can write your own as a plain function. Records are built with `use`.\n"
    "\n"
    " ```gleam\n"
    " import gleamson\n"
    " import gleamson/decode\n"
    "\n"
    " pub type Cat {\n"
    "   Cat(name: String, lives: Int, nicknames: List(String))\n"
    " }\n"
    "\n"
    " pub fn cat_from_json(text: String) -> Result(Cat, decode.Error) {\n"
    "   let cat = {\n"
    "     use name <- decode.field(\"name\", decode.string)\n"
    "     use lives <- decode.field(\"lives\", decode.int)\n"
    "     use nicknames <- decode.field(\"nicknames\", decode.list(decode.string))\n"
    "     decode.success(Cat(name:, lives:, nicknames:))\n"
    "   }\n"
    "   decode.from_string(text, cat)\n"
    " }\n"
    " ```\n"
).

-type decode_error() :: {decode_error, binary(), binary(), list(binary())}.

-type error() :: {could_not_parse, gleamson:parse_error()} |
    {could_not_decode, list(decode_error())}.

-file("src/gleamson/decode.gleam", 62).
?DOC(" Run a decoder, collecting *all* errors.\n").
-spec run(
    gleamson:json(),
    fun((gleamson:json()) -> {EBM, list(decode_error())})
) -> {ok, EBM} | {error, list(decode_error())}.
run(Json, Decoder) ->
    case Decoder(Json) of
        {Value, []} ->
            {ok, Value};

        {_, Errors} ->
            {error, Errors}
    end.

-file("src/gleamson/decode.gleam", 74).
?DOC(
    " Run a decoder but report only the first error. Handy when a single error\n"
    " is all you want to surface to the caller.\n"
).
-spec run_first(
    gleamson:json(),
    fun((gleamson:json()) -> {EBR, list(decode_error())})
) -> {ok, EBR} | {error, decode_error()}.
run_first(Json, Decoder) ->
    case Decoder(Json) of
        {Value, []} ->
            {ok, Value};

        {_, [First | _]} ->
            {error, First}
    end.

-file("src/gleamson/decode.gleam", 85).
?DOC(" Parse a string and decode it in one step, collecting all decode errors.\n").
-spec from_string(
    binary(),
    fun((gleamson:json()) -> {EBV, list(decode_error())})
) -> {ok, EBV} | {error, error()}.
from_string(Text, Decoder) ->
    case gleamson:parse(Text) of
        {ok, Json} ->
            case run(Json, Decoder) of
                {ok, Value} ->
                    {ok, Value};

                {error, Errors} ->
                    {error, {could_not_decode, Errors}}
            end;

        {error, Error} ->
            {error, {could_not_parse, Error}}
    end.

-file("src/gleamson/decode.gleam", 336).
-spec type_name(gleamson:json()) -> binary().
type_name(Json) ->
    case Json of
        null ->
            <<"Null"/utf8>>;

        {bool, _} ->
            <<"Bool"/utf8>>;

        {int, _} ->
            <<"Int"/utf8>>;

        {float, _} ->
            <<"Float"/utf8>>;

        {string, _} ->
            <<"String"/utf8>>;

        {array, _} ->
            <<"Array"/utf8>>;

        {object, _} ->
            <<"Object"/utf8>>
    end.

-file("src/gleamson/decode.gleam", 322).
-spec mismatch(binary(), gleamson:json()) -> decode_error().
mismatch(Expected, Found) ->
    {decode_error, Expected, type_name(Found), []}.

-file("src/gleamson/decode.gleam", 101).
-spec string(gleamson:json()) -> {binary(), list(decode_error())}.
string(Json) ->
    case Json of
        {string, Value} ->
            {Value, []};

        _ ->
            {<<""/utf8>>, [mismatch(<<"String"/utf8>>, Json)]}
    end.

-file("src/gleamson/decode.gleam", 108).
-spec int(gleamson:json()) -> {integer(), list(decode_error())}.
int(Json) ->
    case Json of
        {int, Value} ->
            {Value, []};

        _ ->
            {0, [mismatch(<<"Int"/utf8>>, Json)]}
    end.

-file("src/gleamson/decode.gleam", 116).
?DOC(" Decodes a JSON number as a float, accepting integer literals too.\n").
-spec float(gleamson:json()) -> {float(), list(decode_error())}.
float(Json) ->
    case Json of
        {float, Value} ->
            {Value, []};

        {int, Value@1} ->
            {erlang:float(Value@1), []};

        _ ->
            {+0.0, [mismatch(<<"Float"/utf8>>, Json)]}
    end.

-file("src/gleamson/decode.gleam", 124).
-spec bool(gleamson:json()) -> {boolean(), list(decode_error())}.
bool(Json) ->
    case Json of
        {bool, Value} ->
            {Value, []};

        _ ->
            {false, [mismatch(<<"Bool"/utf8>>, Json)]}
    end.

-file("src/gleamson/decode.gleam", 132).
?DOC(" A decoder that accepts anything and hands back the raw `Json`.\n").
-spec json(gleamson:json()) -> {gleamson:json(), list(decode_error())}.
json(Value) ->
    {Value, []}.

-file("src/gleamson/decode.gleam", 138).
?DOC(
    " A decoder that always succeeds with the given value. Used to finish a\n"
    " `use` chain.\n"
).
-spec success(ECE) -> fun((gleamson:json()) -> {ECE, list(decode_error())}).
success(Value) ->
    fun(_) -> {Value, []} end.

-file("src/gleamson/decode.gleam", 144).
?DOC(
    " A decoder that always fails, reporting `expected`. `zero` is the value used\n"
    " to keep accumulating in surrounding decoders.\n"
).
-spec failure(ECG, binary()) -> fun((gleamson:json()) -> {ECG,
    list(decode_error())}).
failure(Zero, Expected) ->
    fun(Json) -> {Zero, [mismatch(Expected, Json)]} end.

-file("src/gleamson/decode.gleam", 326).
-spec missing_field(binary()) -> decode_error().
missing_field(Name) ->
    {decode_error,
        <<<<"field \""/utf8, Name/binary>>/binary, "\""/utf8>>,
        <<"nothing"/utf8>>,
        [Name]}.

-file("src/gleamson/decode.gleam", 332).
-spec push(decode_error(), binary()) -> decode_error().
push(Error, Segment) ->
    {decode_error,
        erlang:element(2, Error),
        erlang:element(3, Error),
        [Segment | erlang:element(4, Error)]}.

-file("src/gleamson/decode.gleam", 246).
-spec decode_items(
    list(gleamson:json()),
    fun((gleamson:json()) -> {EDC, list(decode_error())}),
    integer(),
    list(EDC),
    list(decode_error())
) -> {list(EDC), list(decode_error())}.
decode_items(Items, Inner, Index, Values, Errors) ->
    case Items of
        [] ->
            {lists:reverse(Values), Errors};

        [First | Rest] ->
            {Value, Item_errors} = Inner(First),
            Item_errors@1 = gleam@list:map(
                Item_errors,
                fun(Error) -> push(Error, erlang:integer_to_binary(Index)) end
            ),
            decode_items(
                Rest,
                Inner,
                Index + 1,
                [Value | Values],
                lists:append(Errors, Item_errors@1)
            )
    end.

-file("src/gleamson/decode.gleam", 237).
?DOC(
    " Decode a JSON array, applying `inner` to every element and collecting every\n"
    " element's errors.\n"
).
-spec list(fun((gleamson:json()) -> {ECX, list(decode_error())})) -> fun((gleamson:json()) -> {list(ECX),
    list(decode_error())}).
list(Inner) ->
    fun(Json) -> case Json of
            {array, Items} ->
                decode_items(Items, Inner, 0, [], []);

            _ ->
                {[], [mismatch(<<"Array"/utf8>>, Json)]}
        end end.

-file("src/gleamson/decode.gleam", 152).
?DOC(
    " Decode a field of an object, then continue with the rest of the record.\n"
    " A failing field does not stop the others from being checked.\n"
).
-spec field(
    binary(),
    fun((gleamson:json()) -> {ECI, list(decode_error())}),
    fun((ECI) -> fun((gleamson:json()) -> {ECK, list(decode_error())}))
) -> fun((gleamson:json()) -> {ECK, list(decode_error())}).
field(Name, Field_decoder, Next) ->
    fun(Json) -> case Json of
            {object, Entries} ->
                case gleam@list:key_find(Entries, Name) of
                    {ok, Child} ->
                        {Value, Field_errors} = Field_decoder(Child),
                        Field_errors@1 = gleam@list:map(
                            Field_errors,
                            fun(Error) -> push(Error, Name) end
                        ),
                        {Rest, Rest_errors} = (Next(Value))(Json),
                        {Rest, lists:append(Field_errors@1, Rest_errors)};

                    {error, _} ->
                        {Zero, _} = Field_decoder(null),
                        {Rest@1, Rest_errors@1} = (Next(Zero))(Json),
                        {Rest@1, [missing_field(Name) | Rest_errors@1]}
                end;

            _ ->
                {Zero@1, _} = Field_decoder(null),
                {Rest@2, _} = (Next(Zero@1))(null),
                {Rest@2, [mismatch(<<"Object"/utf8>>, Json)]}
        end end.

-file("src/gleamson/decode.gleam", 186).
?DOC(
    " Like `field`, but a missing key or `null` value yields `None` instead of\n"
    " an error.\n"
).
-spec optional_field(
    binary(),
    fun((gleamson:json()) -> {ECN, list(decode_error())}),
    fun((gleam@option:option(ECN)) -> fun((gleamson:json()) -> {ECQ,
        list(decode_error())}))
) -> fun((gleamson:json()) -> {ECQ, list(decode_error())}).
optional_field(Name, Field_decoder, Next) ->
    fun(Json) -> case Json of
            {object, Entries} ->
                case gleam@list:key_find(Entries, Name) of
                    {ok, null} ->
                        (Next(none))(Json);

                    {ok, Child} ->
                        {Value, Field_errors} = Field_decoder(Child),
                        Field_errors@1 = gleam@list:map(
                            Field_errors,
                            fun(Error) -> push(Error, Name) end
                        ),
                        {Rest, Rest_errors} = (Next({some, Value}))(Json),
                        {Rest, lists:append(Field_errors@1, Rest_errors)};

                    {error, _} ->
                        (Next(none))(Json)
                end;

            _ ->
                {Rest@1, _} = (Next(none))(null),
                {Rest@1, [mismatch(<<"Object"/utf8>>, Json)]}
        end end.

-file("src/gleamson/decode.gleam", 214).
?DOC(" Decode a value found by following a path of object keys.\n").
-spec at(list(binary()), fun((gleamson:json()) -> {ECU, list(decode_error())})) -> fun((gleamson:json()) -> {ECU,
    list(decode_error())}).
at(Path, Inner) ->
    fun(Json) -> case gleamson:get(Json, Path) of
            {ok, Child} ->
                {Value, Errors} = Inner(Child),
                Errors@1 = gleam@list:map(
                    Errors,
                    fun(Error) ->
                        {decode_error,
                            erlang:element(2, Error),
                            erlang:element(3, Error),
                            lists:append(Path, erlang:element(4, Error))}
                    end
                ),
                {Value, Errors@1};

            {error, _} ->
                {Zero, _} = Inner(null),
                {Zero,
                    [{decode_error,
                            <<"value at "/utf8,
                                (gleam@string:join(Path, <<"."/utf8>>))/binary>>,
                            <<"nothing"/utf8>>,
                            Path}]}
        end end.

-file("src/gleamson/decode.gleam", 277).
-spec decode_entries(
    list({binary(), gleamson:json()}),
    fun((gleamson:json()) -> {EDO, list(decode_error())}),
    gleam@dict:dict(binary(), EDO),
    list(decode_error())
) -> {gleam@dict:dict(binary(), EDO), list(decode_error())}.
decode_entries(Entries, Value_decoder, Acc, Errors) ->
    case Entries of
        [] ->
            {Acc, Errors};

        [{Key, Value} | Rest] ->
            {Decoded, Entry_errors} = Value_decoder(Value),
            Entry_errors@1 = gleam@list:map(
                Entry_errors,
                fun(Error) -> push(Error, Key) end
            ),
            decode_entries(
                Rest,
                Value_decoder,
                gleam@dict:insert(Acc, Key, Decoded),
                lists:append(Errors, Entry_errors@1)
            )
    end.

-file("src/gleamson/decode.gleam", 268).
?DOC(" Decode a JSON object into a `Dict` keyed by its string keys.\n").
-spec dict(fun((gleamson:json()) -> {EDI, list(decode_error())})) -> fun((gleamson:json()) -> {gleam@dict:dict(binary(), EDI),
    list(decode_error())}).
dict(Value_decoder) ->
    fun(Json) -> case Json of
            {object, Entries} ->
                decode_entries(Entries, Value_decoder, maps:new(), []);

            _ ->
                {maps:new(), [mismatch(<<"Object"/utf8>>, Json)]}
        end end.

-file("src/gleamson/decode.gleam", 300).
?DOC(" Wrap a decoder so that `null` becomes `None`.\n").
-spec optional(fun((gleamson:json()) -> {EDW, list(decode_error())})) -> fun((gleamson:json()) -> {gleam@option:option(EDW),
    list(decode_error())}).
optional(Inner) ->
    fun(Json) -> case Json of
            null ->
                {none, []};

            _ ->
                {Value, Errors} = Inner(Json),
                {{some, Value}, Errors}
        end end.

-file("src/gleamson/decode.gleam", 313).
?DOC(" Transform a decoder's value. Errors are carried through unchanged.\n").
-spec map(
    fun((gleamson:json()) -> {EEA, list(decode_error())}),
    fun((EEA) -> EEC)
) -> fun((gleamson:json()) -> {EEC, list(decode_error())}).
map(Decoder, Transform) ->
    fun(Json) ->
        {Value, Errors} = Decoder(Json),
        {Transform(Value), Errors}
    end.

-file("src/gleamson/decode.gleam", 366).
-spec first_success(
    list(fun((gleamson:json()) -> {EEJ, list(decode_error())})),
    gleamson:json(),
    EEJ,
    list(decode_error())
) -> {EEJ, list(decode_error())}.
first_success(Decoders, Json, Zero, Errors) ->
    case Decoders of
        [] ->
            {Zero, Errors};

        [Decoder | Rest] ->
            case Decoder(Json) of
                {Value, []} ->
                    {Value, []};

                {_, More} ->
                    first_success(Rest, Json, Zero, lists:append(Errors, More))
            end
    end.

-file("src/gleamson/decode.gleam", 357).
?DOC(
    " Try `first`; if it fails, try each decoder in `others` in turn, returning\n"
    " the first that succeeds. If none match, every branch's errors are reported.\n"
    "\n"
    " ```gleam\n"
    " // a field that may arrive as an int or as a bool\n"
    " one_of(int, [map(bool, fn(b) { case b { True -> 1 False -> 0 } })])\n"
    " ```\n"
).
-spec one_of(
    fun((gleamson:json()) -> {EEE, list(decode_error())}),
    list(fun((gleamson:json()) -> {EEE, list(decode_error())}))
) -> fun((gleamson:json()) -> {EEE, list(decode_error())}).
one_of(First, Others) ->
    fun(Json) -> case First(Json) of
            {Value, []} ->
                {Value, []};

            {Zero, Errors} ->
                first_success(Others, Json, Zero, Errors)
        end end.

-file("src/gleamson/decode.gleam", 394).
?DOC(
    " Decode a value, then use it to choose the next decoder. Useful for\n"
    " validation, or for discriminated unions (read a \"type\" field, then decode\n"
    " the matching shape). This short-circuits: if the first decoder fails, the\n"
    " chosen one is not run.\n"
    "\n"
    " ```gleam\n"
    " use n <- then(int)\n"
    " case n >= 0 {\n"
    "   True -> success(n)\n"
    "   False -> failure(0, \"a non-negative int\")\n"
    " }\n"
    " ```\n"
).
-spec then(
    fun((gleamson:json()) -> {EEO, list(decode_error())}),
    fun((EEO) -> fun((gleamson:json()) -> {EEQ, list(decode_error())}))
) -> fun((gleamson:json()) -> {EEQ, list(decode_error())}).
then(Decoder, Next) ->
    fun(Json) -> case Decoder(Json) of
            {Value, []} ->
                Chosen = Next(Value),
                Chosen(Json);

            {Value@1, Errors} ->
                Chosen@1 = Next(Value@1),
                {Zero, _} = Chosen@1(Json),
                {Zero, Errors}
        end end.

-file("src/gleamson/decode.gleam", 411).
?DOC(" Decode the element at a given array index.\n").
-spec index(integer(), fun((gleamson:json()) -> {EET, list(decode_error())})) -> fun((gleamson:json()) -> {EET,
    list(decode_error())}).
index(Position, Inner) ->
    fun(Json) -> case Json of
            {array, _} ->
                case gleamson:index(Json, Position) of
                    {ok, Child} ->
                        {Value, Errors} = Inner(Child),
                        {Value,
                            gleam@list:map(
                                Errors,
                                fun(Error) ->
                                    push(
                                        Error,
                                        erlang:integer_to_binary(Position)
                                    )
                                end
                            )};

                    {error, _} ->
                        {Zero, _} = Inner(null),
                        {Zero,
                            [{decode_error,
                                    <<"element at index "/utf8,
                                        (erlang:integer_to_binary(Position))/binary>>,
                                    <<"nothing"/utf8>>,
                                    [erlang:integer_to_binary(Position)]}]}
                end;

            _ ->
                {Zero@1, _} = Inner(null),
                {Zero@1, [mismatch(<<"Array"/utf8>>, Json)]}
        end end.

-file("src/gleamson/decode.gleam", 478).
-spec allowed(list({binary(), any()})) -> binary().
allowed(Variants) ->
    _pipe = Variants,
    _pipe@1 = gleam@list:map(
        _pipe,
        fun(Pair) ->
            <<<<"\""/utf8, (erlang:element(1, Pair))/binary>>/binary,
                "\""/utf8>>
        end
    ),
    gleam@string:join(_pipe@1, <<", "/utf8>>).

-file("src/gleamson/decode.gleam", 453).
?DOC(
    " Decode a JSON string by mapping it to a value from a fixed set, the way\n"
    " you'd decode an enum-like custom type. The first pair's value doubles as the\n"
    " fallback used while accumulating errors.\n"
    "\n"
    " ```gleam\n"
    " pub type Side {\n"
    "   Buy\n"
    "   Sell\n"
    " }\n"
    "\n"
    " let side = enum(#(\"buy\", Buy), or: [#(\"sell\", Sell)])\n"
    " ```\n"
).
-spec enum({binary(), EEW}, list({binary(), EEW})) -> fun((gleamson:json()) -> {EEW,
    list(decode_error())}).
enum(First, Others) ->
    Variants = [First | Others],
    Fallback = erlang:element(2, First),
    fun(Json) -> case Json of
            {string, Text} ->
                case gleam@list:key_find(Variants, Text) of
                    {ok, Value} ->
                        {Value, []};

                    {error, _} ->
                        {Fallback,
                            [{decode_error,
                                    <<"one of: "/utf8,
                                        (allowed(Variants))/binary>>,
                                    <<<<"\""/utf8, Text/binary>>/binary,
                                        "\""/utf8>>,
                                    []}]}
                end;

            _ ->
                {Fallback, [mismatch(<<"String"/utf8>>, Json)]}
        end end.