-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.