Skip to main content

src/gleamson.erl

-module(gleamson).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleamson.gleam").
-export([parse_bits/1, parse/1, to_string_tree/1, to_string/1, array/2, nullable/2, from_dict/3, field/2, get/2, index/2, to_dict/1, as_string/1, as_int/1, as_float/1, as_bool/1, to_string_pretty_with/2, to_string_pretty/1, merge/2, semantically_equal/2, pointer/2]).
-export_type([json/0, parse_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(
    " gleamson — a pure-Gleam JSON library.\n"
    "\n"
    " Unlike libraries that delegate to a platform's native JSON facilities,\n"
    " `gleamson` is written entirely in Gleam. That means:\n"
    "\n"
    "   * It runs identically on the Erlang and JavaScript targets.\n"
    "   * It has no Erlang/OTP version requirement.\n"
    "   * Parse errors carry a precise byte position, on every runtime.\n"
    "   * The `Json` value is a transparent type you can pattern match on,\n"
    "     transform, and build directly — no opaque box.\n"
    "\n"
    " Parsing is a single pass over a `BitArray` using Gleam's bit-array\n"
    " pattern matching, which compiles to fast binary matching on the BEAM.\n"
).

-type json() :: null |
    {bool, boolean()} |
    {int, integer()} |
    {float, float()} |
    {string, binary()} |
    {array, list(json())} |
    {object, list({binary(), json()})}.

-type parse_error() :: unexpected_end |
    {unexpected_byte, binary(), integer()} |
    {unexpected_token, binary(), integer()}.

-file("src/gleamson.gleam", 374).
-spec position(bitstring(), integer()) -> integer().
position(Bits, Len) ->
    Len - erlang:byte_size(Bits).

-file("src/gleamson.gleam", 424).
-spec hex_char(integer()) -> binary().
hex_char(Digit) ->
    case Digit of
        10 ->
            <<"a"/utf8>>;

        11 ->
            <<"b"/utf8>>;

        12 ->
            <<"c"/utf8>>;

        13 ->
            <<"d"/utf8>>;

        14 ->
            <<"e"/utf8>>;

        15 ->
            <<"f"/utf8>>;

        _ ->
            erlang:integer_to_binary(Digit)
    end.

-file("src/gleamson.gleam", 417).
-spec to_hex_loop(integer(), binary()) -> binary().
to_hex_loop(N, Acc) ->
    case N of
        0 ->
            Acc;

        _ ->
            to_hex_loop(N div 16, <<(hex_char(N rem 16))/binary, Acc/binary>>)
    end.

-file("src/gleamson.gleam", 410).
-spec to_hex(integer()) -> binary().
to_hex(N) ->
    case N of
        0 ->
            <<"0"/utf8>>;

        _ ->
            to_hex_loop(N, <<""/utf8>>)
    end.

-file("src/gleamson.gleam", 406).
-spec byte_hex(integer()) -> binary().
byte_hex(Byte) ->
    <<"0x"/utf8, (string:uppercase(to_hex(Byte)))/binary>>.

-file("src/gleamson.gleam", 395).
-spec describe_byte(integer()) -> binary().
describe_byte(Byte) ->
    case (Byte >= 16#20) andalso (Byte =< 16#7E) of
        true ->
            case gleam@string:utf_codepoint(Byte) of
                {ok, Cp} ->
                    gleam_stdlib:utf_codepoint_list_to_string([Cp]);

                {error, _} ->
                    byte_hex(Byte)
            end;

        false ->
            byte_hex(Byte)
    end.

-file("src/gleamson.gleam", 364).
-spec skip_whitespace(bitstring()) -> bitstring().
skip_whitespace(Bits) ->
    case Bits of
        <<16#20, Rest/bitstring>> ->
            skip_whitespace(Rest);

        <<16#09, Rest@1/bitstring>> ->
            skip_whitespace(Rest@1);

        <<16#0A, Rest@2/bitstring>> ->
            skip_whitespace(Rest@2);

        <<16#0D, Rest@3/bitstring>> ->
            skip_whitespace(Rest@3);

        _ ->
            Bits
    end.

-file("src/gleamson.gleam", 348).
?DOC(
    " `float.parse` is backed by Erlang's `binary_to_float`, which insists on a\n"
    " decimal point. JSON allows `1e9`, so we make sure the mantissa has one and\n"
    " normalise the exponent letter to lowercase. The result is then parsed\n"
    " identically on both targets.\n"
).
-spec normalize_float(binary()) -> binary().
normalize_float(Lexeme) ->
    Lexeme@1 = gleam@string:replace(Lexeme, <<"E"/utf8>>, <<"e"/utf8>>),
    case gleam@string:split_once(Lexeme@1, <<"e"/utf8>>) of
        {ok, {Mantissa, Exponent}} ->
            Mantissa@1 = case gleam_stdlib:contains_string(
                Mantissa,
                <<"."/utf8>>
            ) of
                true ->
                    Mantissa;

                false ->
                    <<Mantissa/binary, ".0"/utf8>>
            end,
            <<<<Mantissa@1/binary, "e"/utf8>>/binary, Exponent/binary>>;

        {error, _} ->
            Lexeme@1
    end.

-file("src/gleamson.gleam", 333).
-spec is_integer_lexeme(binary()) -> boolean().
is_integer_lexeme(Lexeme) ->
    case {gleam_stdlib:contains_string(Lexeme, <<"."/utf8>>),
        gleam_stdlib:contains_string(Lexeme, <<"e"/utf8>>),
        gleam_stdlib:contains_string(Lexeme, <<"E"/utf8>>)} of
        {false, false, false} ->
            true;

        {_, _, _} ->
            false
    end.

-file("src/gleamson.gleam", 324).
-spec is_number_char(integer()) -> boolean().
is_number_char(Byte) ->
    ((((((Byte >= 16#30) andalso (Byte =< 16#39)) orelse (Byte =:= 16#2D))
    orelse (Byte =:= 16#2B))
    orelse (Byte =:= 16#2E))
    orelse (Byte =:= 16#65))
    orelse (Byte =:= 16#45).

-file("src/gleamson.gleam", 313).
-spec take_number(bitstring(), bitstring()) -> {bitstring(), bitstring()}.
take_number(Bits, Acc) ->
    case Bits of
        <<Byte, Rest/bitstring>> ->
            case is_number_char(Byte) of
                true ->
                    take_number(Rest, <<Acc/bitstring, Byte>>);

                false ->
                    {Acc, Bits}
            end;

        _ ->
            {Acc, Bits}
    end.

-file("src/gleamson.gleam", 293).
-spec parse_number(bitstring(), integer()) -> {ok, {json(), bitstring()}} |
    {error, parse_error()}.
parse_number(Bits, Len) ->
    {Lexeme_bits, Rest} = take_number(Bits, <<>>),
    case gleam@bit_array:to_string(Lexeme_bits) of
        {ok, Lexeme} ->
            case is_integer_lexeme(Lexeme) of
                true ->
                    case gleam_stdlib:parse_int(Lexeme) of
                        {ok, Value} ->
                            {ok, {{int, Value}, Rest}};

                        {error, _} ->
                            {error,
                                {unexpected_token, Lexeme, position(Bits, Len)}}
                    end;

                false ->
                    case gleam_stdlib:parse_float(normalize_float(Lexeme)) of
                        {ok, Value@1} ->
                            {ok, {{float, Value@1}, Rest}};

                        {error, _} ->
                            {error,
                                {unexpected_token, Lexeme, position(Bits, Len)}}
                    end
            end;

        {error, _} ->
            {error,
                {unexpected_byte, <<"invalid UTF-8"/utf8>>, position(Bits, Len)}}
    end.

-file("src/gleamson.gleam", 386).
-spec hex_digit(integer()) -> {ok, integer()} | {error, nil}.
hex_digit(Byte) ->
    case Byte of
        _ when (Byte >= 16#30) andalso (Byte =< 16#39) ->
            {ok, Byte - 16#30};

        _ when (Byte >= 16#41) andalso (Byte =< 16#46) ->
            {ok, (Byte - 16#41) + 10};

        _ when (Byte >= 16#61) andalso (Byte =< 16#66) ->
            {ok, (Byte - 16#61) + 10};

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 378).
-spec hex4(integer(), integer(), integer(), integer()) -> {ok, integer()} |
    {error, nil}.
hex4(A, B, C, D) ->
    gleam@result:'try'(
        hex_digit(A),
        fun(A@1) ->
            gleam@result:'try'(
                hex_digit(B),
                fun(B@1) ->
                    gleam@result:'try'(
                        hex_digit(C),
                        fun(C@1) ->
                            gleam@result:'try'(
                                hex_digit(D),
                                fun(D@1) ->
                                    {ok,
                                        (((((A@1 * 16) + B@1) * 16) + C@1) * 16)
                                        + D@1}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/gleamson.gleam", 278).
-spec append_codepoint(integer(), bitstring(), integer(), bitstring()) -> {ok,
        {binary(), bitstring()}} |
    {error, parse_error()}.
append_codepoint(Code, Rest, Len, Acc) ->
    case gleam@string:utf_codepoint(Code) of
        {ok, Cp} ->
            parse_string_loop(Rest, Len, <<Acc/bitstring, Cp/utf8>>);

        {error, _} ->
            {error,
                {unexpected_byte,
                    <<"invalid code point"/utf8>>,
                    position(Rest, Len)}}
    end.

-file("src/gleamson.gleam", 252).
-spec parse_low_surrogate(bitstring(), integer(), bitstring(), integer()) -> {ok,
        {binary(), bitstring()}} |
    {error, parse_error()}.
parse_low_surrogate(Bits, Len, Acc, High) ->
    case Bits of
        <<"\\u"/utf8, A, B, C, D, Rest/bitstring>> ->
            case hex4(A, B, C, D) of
                {ok, Low} ->
                    case (Low >= 16#DC00) andalso (Low =< 16#DFFF) of
                        true ->
                            Code = (16#10000 + ((High - 16#D800) * 16#400)) + (Low
                            - 16#DC00),
                            append_codepoint(Code, Rest, Len, Acc);

                        false ->
                            {error,
                                {unexpected_byte,
                                    <<"invalid low surrogate"/utf8>>,
                                    position(Bits, Len)}}
                    end;

                {error, _} ->
                    {error,
                        {unexpected_byte,
                            <<"invalid \\u escape"/utf8>>,
                            position(Bits, Len)}}
            end;

        _ ->
            {error,
                {unexpected_byte,
                    <<"unpaired surrogate"/utf8>>,
                    position(Bits, Len)}}
    end.

-file("src/gleamson.gleam", 232).
-spec parse_unicode_escape(bitstring(), integer(), bitstring()) -> {ok,
        {binary(), bitstring()}} |
    {error, parse_error()}.
parse_unicode_escape(Bits, Len, Acc) ->
    case Bits of
        <<A, B, C, D, Rest/bitstring>> ->
            case hex4(A, B, C, D) of
                {ok, Code} ->
                    case (Code >= 16#D800) andalso (Code =< 16#DBFF) of
                        true ->
                            parse_low_surrogate(Rest, Len, Acc, Code);

                        false ->
                            append_codepoint(Code, Rest, Len, Acc)
                    end;

                {error, _} ->
                    {error,
                        {unexpected_byte,
                            <<"invalid \\u escape"/utf8>>,
                            position(Bits, Len)}}
            end;

        _ ->
            {error, unexpected_end}
    end.

-file("src/gleamson.gleam", 215).
-spec parse_escape(bitstring(), integer(), bitstring()) -> {ok,
        {binary(), bitstring()}} |
    {error, parse_error()}.
parse_escape(Bits, Len, Acc) ->
    case Bits of
        <<"\""/utf8, Rest/bitstring>> ->
            parse_string_loop(Rest, Len, <<Acc/bitstring, 16#22>>);

        <<"\\"/utf8, Rest@1/bitstring>> ->
            parse_string_loop(Rest@1, Len, <<Acc/bitstring, 16#5C>>);

        <<"/"/utf8, Rest@2/bitstring>> ->
            parse_string_loop(Rest@2, Len, <<Acc/bitstring, 16#2F>>);

        <<"b"/utf8, Rest@3/bitstring>> ->
            parse_string_loop(Rest@3, Len, <<Acc/bitstring, 16#08>>);

        <<"f"/utf8, Rest@4/bitstring>> ->
            parse_string_loop(Rest@4, Len, <<Acc/bitstring, 16#0C>>);

        <<"n"/utf8, Rest@5/bitstring>> ->
            parse_string_loop(Rest@5, Len, <<Acc/bitstring, 16#0A>>);

        <<"r"/utf8, Rest@6/bitstring>> ->
            parse_string_loop(Rest@6, Len, <<Acc/bitstring, 16#0D>>);

        <<"t"/utf8, Rest@7/bitstring>> ->
            parse_string_loop(Rest@7, Len, <<Acc/bitstring, 16#09>>);

        <<"u"/utf8, Rest@8/bitstring>> ->
            parse_unicode_escape(Rest@8, Len, Acc);

        <<Byte, _/bitstring>> ->
            {error, {unexpected_byte, describe_byte(Byte), position(Bits, Len)}};

        _ ->
            {error, unexpected_end}
    end.

-file("src/gleamson.gleam", 196).
-spec parse_string_loop(bitstring(), integer(), bitstring()) -> {ok,
        {binary(), bitstring()}} |
    {error, parse_error()}.
parse_string_loop(Bits, Len, Acc) ->
    case Bits of
        <<"\""/utf8, Rest/bitstring>> ->
            case gleam@bit_array:to_string(Acc) of
                {ok, Value} ->
                    {ok, {Value, Rest}};

                {error, _} ->
                    {error,
                        {unexpected_byte,
                            <<"invalid UTF-8"/utf8>>,
                            position(Bits, Len)}}
            end;

        <<"\\"/utf8, Rest@1/bitstring>> ->
            parse_escape(Rest@1, Len, Acc);

        <<Byte, Rest@2/bitstring>> ->
            case Byte < 16#20 of
                true ->
                    {error,
                        {unexpected_byte,
                            describe_byte(Byte),
                            position(Bits, Len)}};

                false ->
                    parse_string_loop(Rest@2, Len, <<Acc/bitstring, Byte>>)
            end;

        _ ->
            {error, unexpected_end}
    end.

-file("src/gleamson.gleam", 192).
-spec parse_string(bitstring(), integer()) -> {ok, {binary(), bitstring()}} |
    {error, parse_error()}.
parse_string(Bits, Len) ->
    parse_string_loop(Bits, Len, <<>>).

-file("src/gleamson.gleam", 126).
-spec parse_array_element(bitstring(), integer(), list(json())) -> {ok,
        {json(), bitstring()}} |
    {error, parse_error()}.
parse_array_element(Bits, Len, Acc) ->
    gleam@result:'try'(
        parse_value(Bits, Len),
        fun(_use0) ->
            {Value, Rest} = _use0,
            Acc@1 = [Value | Acc],
            Rest@1 = skip_whitespace(Rest),
            case Rest@1 of
                <<","/utf8, After/bitstring>> ->
                    parse_array_element(skip_whitespace(After), Len, Acc@1);

                <<"]"/utf8, After@1/bitstring>> ->
                    {ok, {{array, lists:reverse(Acc@1)}, After@1}};

                <<Byte, _/bitstring>> ->
                    {error,
                        {unexpected_byte,
                            describe_byte(Byte),
                            position(Rest@1, Len)}};

                _ ->
                    {error, unexpected_end}
            end
        end
    ).

-file("src/gleamson.gleam", 118).
-spec parse_array(bitstring(), integer(), list(json())) -> {ok,
        {json(), bitstring()}} |
    {error, parse_error()}.
parse_array(Bits, Len, Acc) ->
    case Bits of
        <<"]"/utf8, Rest/bitstring>> ->
            {ok, {{array, lists:reverse(Acc)}, Rest}};

        _ ->
            parse_array_element(Bits, Len, Acc)
    end.

-file("src/gleamson.gleam", 153).
-spec parse_object_member(bitstring(), integer(), list({binary(), json()})) -> {ok,
        {json(), bitstring()}} |
    {error, parse_error()}.
parse_object_member(Bits, Len, Acc) ->
    case Bits of
        <<"\""/utf8, Rest/bitstring>> ->
            gleam@result:'try'(
                parse_string(Rest, Len),
                fun(_use0) ->
                    {Key, Rest@1} = _use0,
                    Rest@2 = skip_whitespace(Rest@1),
                    case Rest@2 of
                        <<":"/utf8, After/bitstring>> ->
                            gleam@result:'try'(
                                parse_value(skip_whitespace(After), Len),
                                fun(_use0@1) ->
                                    {Value, Rest@3} = _use0@1,
                                    Acc@1 = [{Key, Value} | Acc],
                                    Rest@4 = skip_whitespace(Rest@3),
                                    case Rest@4 of
                                        <<","/utf8, More/bitstring>> ->
                                            parse_object_member(
                                                skip_whitespace(More),
                                                Len,
                                                Acc@1
                                            );

                                        <<"}"/utf8, More@1/bitstring>> ->
                                            {ok,
                                                {{object, lists:reverse(Acc@1)},
                                                    More@1}};

                                        <<Byte, _/bitstring>> ->
                                            {error,
                                                {unexpected_byte,
                                                    describe_byte(Byte),
                                                    position(Rest@4, Len)}};

                                        _ ->
                                            {error, unexpected_end}
                                    end
                                end
                            );

                        <<Byte@1, _/bitstring>> ->
                            {error,
                                {unexpected_byte,
                                    describe_byte(Byte@1),
                                    position(Rest@2, Len)}};

                        _ ->
                            {error, unexpected_end}
                    end
                end
            );

        <<Byte@2, _/bitstring>> ->
            {error,
                {unexpected_byte, describe_byte(Byte@2), position(Bits, Len)}};

        _ ->
            {error, unexpected_end}
    end.

-file("src/gleamson.gleam", 141).
-spec parse_object(bitstring(), integer(), list({binary(), json()})) -> {ok,
        {json(), bitstring()}} |
    {error, parse_error()}.
parse_object(Bits, Len, Acc) ->
    case Bits of
        <<"}"/utf8, Rest/bitstring>> ->
            {ok, {{object, lists:reverse(Acc)}, Rest}};

        _ ->
            parse_object_member(Bits, Len, Acc)
    end.

-file("src/gleamson.gleam", 98).
-spec parse_value(bitstring(), integer()) -> {ok, {json(), bitstring()}} |
    {error, parse_error()}.
parse_value(Bits, Len) ->
    case Bits of
        <<"null"/utf8, Rest/bitstring>> ->
            {ok, {null, Rest}};

        <<"true"/utf8, Rest@1/bitstring>> ->
            {ok, {{bool, true}, Rest@1}};

        <<"false"/utf8, Rest@2/bitstring>> ->
            {ok, {{bool, false}, Rest@2}};

        <<"\""/utf8, Rest@3/bitstring>> ->
            gleam@result:'try'(
                parse_string(Rest@3, Len),
                fun(_use0) ->
                    {Value, Remainder} = _use0,
                    {ok, {{string, Value}, Remainder}}
                end
            );

        <<"{"/utf8, Rest@4/bitstring>> ->
            parse_object(skip_whitespace(Rest@4), Len, []);

        <<"["/utf8, Rest@5/bitstring>> ->
            parse_array(skip_whitespace(Rest@5), Len, []);

        <<Byte, _/bitstring>> ->
            case (Byte =:= 16#2D) orelse ((Byte >= 16#30) andalso (Byte =< 16#39)) of
                true ->
                    parse_number(Bits, Len);

                false ->
                    {error,
                        {unexpected_byte,
                            describe_byte(Byte),
                            position(Bits, Len)}}
            end;

        _ ->
            {error, unexpected_end}
    end.

-file("src/gleamson.gleam", 83).
?DOC(
    " Parse JSON from a `BitArray`. Useful when the bytes come straight off the\n"
    " wire and you would rather not allocate an intermediate `String`.\n"
).
-spec parse_bits(bitstring()) -> {ok, json()} | {error, parse_error()}.
parse_bits(Json) ->
    Len = erlang:byte_size(Json),
    gleam@result:'try'(
        parse_value(skip_whitespace(Json), Len),
        fun(_use0) ->
            {Value, Rest} = _use0,
            Trailing = skip_whitespace(Rest),
            case Trailing of
                <<>> ->
                    {ok, Value};

                <<Byte, _/bitstring>> ->
                    {error,
                        {unexpected_byte,
                            describe_byte(Byte),
                            position(Trailing, Len)}};

                _ ->
                    {error, unexpected_end}
            end
        end
    ).

-file("src/gleamson.gleam", 77).
?DOC(
    " Parse a JSON string into a `Json` value.\n"
    "\n"
    " ```gleam\n"
    " parse(\"[1, 2, 3]\")\n"
    " // -> Ok(Array([Int(1), Int(2), Int(3)]))\n"
    "\n"
    " parse(\"[\")\n"
    " // -> Error(UnexpectedEnd)\n"
    " ```\n"
).
-spec parse(binary()) -> {ok, json()} | {error, parse_error()}.
parse(Json) ->
    parse_bits(gleam_stdlib:identity(Json)).

-file("src/gleamson.gleam", 536).
-spec pad_hex4(integer()) -> binary().
pad_hex4(Code) ->
    Hex = to_hex(Code),
    case string:length(Hex) of
        1 ->
            <<"000"/utf8, Hex/binary>>;

        2 ->
            <<"00"/utf8, Hex/binary>>;

        3 ->
            <<"0"/utf8, Hex/binary>>;

        _ ->
            Hex
    end.

-file("src/gleamson.gleam", 522).
-spec escape_codepoint(integer()) -> binary().
escape_codepoint(Cp) ->
    case gleam_stdlib:identity(Cp) of
        16#22 ->
            <<"\\\""/utf8>>;

        16#5C ->
            <<"\\\\"/utf8>>;

        16#08 ->
            <<"\\b"/utf8>>;

        16#0C ->
            <<"\\f"/utf8>>;

        16#0A ->
            <<"\\n"/utf8>>;

        16#0D ->
            <<"\\r"/utf8>>;

        16#09 ->
            <<"\\t"/utf8>>;

        Code when Code < 16#20 ->
            <<"\\u"/utf8, (pad_hex4(Code))/binary>>;

        _ ->
            gleam_stdlib:utf_codepoint_list_to_string([Cp])
    end.

-file("src/gleamson.gleam", 514).
-spec escape_string(binary()) -> gleam@string_tree:string_tree().
escape_string(Input) ->
    _pipe = Input,
    _pipe@1 = gleam@string:to_utf_codepoints(_pipe),
    gleam@list:fold(
        _pipe@1,
        gleam@string_tree:new(),
        fun(Tree, Cp) ->
            gleam@string_tree:append(Tree, escape_codepoint(Cp))
        end
    ).

-file("src/gleamson.gleam", 506).
-spec encode_pair({binary(), json()}) -> gleam@string_tree:string_tree().
encode_pair(Pair) ->
    {Key, Value} = Pair,
    _pipe = gleam_stdlib:identity(<<"\""/utf8>>),
    _pipe@1 = gleam_stdlib:iodata_append(_pipe, escape_string(Key)),
    _pipe@2 = gleam@string_tree:append(_pipe@1, <<"\":"/utf8>>),
    gleam_stdlib:iodata_append(_pipe@2, to_string_tree(Value)).

-file("src/gleamson.gleam", 489).
-spec encode_object(list({binary(), json()})) -> gleam@string_tree:string_tree().
encode_object(Entries) ->
    case Entries of
        [] ->
            gleam_stdlib:identity(<<"{}"/utf8>>);

        [First | Rest] ->
            Body = gleam@list:fold(
                Rest,
                encode_pair(First),
                fun(Acc, Pair) -> _pipe = Acc,
                    _pipe@1 = gleam@string_tree:append(_pipe, <<","/utf8>>),
                    gleam_stdlib:iodata_append(_pipe@1, encode_pair(Pair)) end
            ),
            _pipe@2 = gleam_stdlib:identity(<<"{"/utf8>>),
            _pipe@3 = gleam_stdlib:iodata_append(_pipe@2, Body),
            gleam@string_tree:append(_pipe@3, <<"}"/utf8>>)
    end.

-file("src/gleamson.gleam", 472).
-spec encode_array(list(json())) -> gleam@string_tree:string_tree().
encode_array(Items) ->
    case Items of
        [] ->
            gleam_stdlib:identity(<<"[]"/utf8>>);

        [First | Rest] ->
            Body = gleam@list:fold(
                Rest,
                to_string_tree(First),
                fun(Acc, Item) -> _pipe = Acc,
                    _pipe@1 = gleam@string_tree:append(_pipe, <<","/utf8>>),
                    gleam_stdlib:iodata_append(_pipe@1, to_string_tree(Item)) end
            ),
            _pipe@2 = gleam_stdlib:identity(<<"["/utf8>>),
            _pipe@3 = gleam_stdlib:iodata_append(_pipe@2, Body),
            gleam@string_tree:append(_pipe@3, <<"]"/utf8>>)
    end.

-file("src/gleamson.gleam", 456).
?DOC(" Render a `Json` value to a `StringTree` (iodata).\n").
-spec to_string_tree(json()) -> gleam@string_tree:string_tree().
to_string_tree(Json) ->
    case Json of
        null ->
            gleam_stdlib:identity(<<"null"/utf8>>);

        {bool, true} ->
            gleam_stdlib:identity(<<"true"/utf8>>);

        {bool, false} ->
            gleam_stdlib:identity(<<"false"/utf8>>);

        {int, Value} ->
            gleam_stdlib:identity(erlang:integer_to_binary(Value));

        {float, Value@1} ->
            gleam_stdlib:identity(gleam_stdlib:float_to_string(Value@1));

        {string, Value@2} ->
            _pipe = gleam_stdlib:identity(<<"\""/utf8>>),
            _pipe@1 = gleam_stdlib:iodata_append(_pipe, escape_string(Value@2)),
            gleam@string_tree:append(_pipe@1, <<"\""/utf8>>);

        {array, Items} ->
            encode_array(Items);

        {object, Entries} ->
            encode_object(Entries)
    end.

-file("src/gleamson.gleam", 449).
?DOC(
    " Render a `Json` value to a compact string.\n"
    "\n"
    " Prefer `to_string_tree` when feeding the BEAM's IO, which is optimised for\n"
    " iodata.\n"
    "\n"
    " ```gleam\n"
    " to_string(Array([Int(1), Int(2), Int(3)]))\n"
    " // -> \"[1,2,3]\"\n"
    " ```\n"
).
-spec to_string(json()) -> binary().
to_string(Json) ->
    _pipe = Json,
    _pipe@1 = to_string_tree(_pipe),
    unicode:characters_to_binary(_pipe@1).

-file("src/gleamson.gleam", 556).
?DOC(
    " Encode a list as a JSON array using a per-item encoder.\n"
    "\n"
    " ```gleam\n"
    " array([\"a\", \"b\"], of: String)\n"
    " // -> Array([String(\"a\"), String(\"b\")])\n"
    " ```\n"
).
-spec array(list(DLW), fun((DLW) -> json())) -> json().
array(Items, Encode) ->
    {array, gleam@list:map(Items, Encode)}.

-file("src/gleamson.gleam", 561).
?DOC(" Encode an `Option`, using `Null` for `None`.\n").
-spec nullable(gleam@option:option(DLY), fun((DLY) -> json())) -> json().
nullable(Value, Encode) ->
    case Value of
        {some, Inner} ->
            Encode(Inner);

        none ->
            null
    end.

-file("src/gleamson.gleam", 569).
?DOC(" Build an `Object` from a `Dict`.\n").
-spec from_dict(
    gleam@dict:dict(DMA, DMB),
    fun((DMA) -> binary()),
    fun((DMB) -> json())
) -> json().
from_dict(Values, Keys, Encode) ->
    {object,
        gleam@dict:fold(
            Values,
            [],
            fun(Acc, Key, Value) -> [{Keys(Key), Encode(Value)} | Acc] end
        )}.

-file("src/gleamson.gleam", 586).
?DOC(" Look up a key in an object.\n").
-spec field(json(), binary()) -> {ok, json()} | {error, nil}.
field(Json, Name) ->
    case Json of
        {object, Entries} ->
            gleam@list:key_find(Entries, Name);

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 598).
?DOC(
    " Follow a path of object keys.\n"
    "\n"
    " ```gleam\n"
    " get(value, at: [\"user\", \"name\"])\n"
    " ```\n"
).
-spec get(json(), list(binary())) -> {ok, json()} | {error, nil}.
get(Json, Path) ->
    gleam@list:try_fold(
        Path,
        Json,
        fun(Current, Key) -> field(Current, Key) end
    ).

-file("src/gleamson.gleam", 603).
?DOC(" Index into an array.\n").
-spec index(json(), integer()) -> {ok, json()} | {error, nil}.
index(Json, I) ->
    case Json of
        {array, Items} when I >= 0 ->
            gleam@list:first(gleam@list:drop(Items, I));

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 612).
?DOC(
    " Convert an object to a `Dict` for repeated `O(1)` lookups. Later keys win\n"
    " on duplicates.\n"
).
-spec to_dict(json()) -> {ok, gleam@dict:dict(binary(), json())} | {error, nil}.
to_dict(Json) ->
    case Json of
        {object, Entries} ->
            {ok, maps:from_list(Entries)};

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 619).
-spec as_string(json()) -> {ok, binary()} | {error, nil}.
as_string(Json) ->
    case Json of
        {string, Value} ->
            {ok, Value};

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 626).
-spec as_int(json()) -> {ok, integer()} | {error, nil}.
as_int(Json) ->
    case Json of
        {int, Value} ->
            {ok, Value};

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 633).
-spec as_float(json()) -> {ok, float()} | {error, nil}.
as_float(Json) ->
    case Json of
        {float, Value} ->
            {ok, Value};

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

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 641).
-spec as_bool(json()) -> {ok, boolean()} | {error, nil}.
as_bool(Json) ->
    case Json of
        {bool, Value} ->
            {ok, Value};

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 723).
-spec join_trees(list(gleam@string_tree:string_tree()), binary()) -> gleam@string_tree:string_tree().
join_trees(Trees, Separator) ->
    case Trees of
        [] ->
            gleam@string_tree:new();

        [First | Rest] ->
            gleam@list:fold(Rest, First, fun(Acc, Tree) -> _pipe = Acc,
                    _pipe@1 = gleam@string_tree:append(_pipe, Separator),
                    gleam_stdlib:iodata_append(_pipe@1, Tree) end)
    end.

-file("src/gleamson.gleam", 698).
-spec pretty_object(list({binary(), json()}), integer(), integer()) -> gleam@string_tree:string_tree().
pretty_object(Entries, Depth, Spaces) ->
    Item_indent = gleam@string:repeat(<<" "/utf8>>, (Depth + 1) * Spaces),
    Close_indent = gleam@string:repeat(<<" "/utf8>>, Depth * Spaces),
    Body = begin
        _pipe = Entries,
        _pipe@5 = gleam@list:map(
            _pipe,
            fun(Entry) ->
                {Key, Value} = Entry,
                _pipe@1 = gleam_stdlib:identity(Item_indent),
                _pipe@2 = gleam@string_tree:append(_pipe@1, <<"\""/utf8>>),
                _pipe@3 = gleam_stdlib:iodata_append(
                    _pipe@2,
                    escape_string(Key)
                ),
                _pipe@4 = gleam@string_tree:append(_pipe@3, <<"\": "/utf8>>),
                gleam_stdlib:iodata_append(
                    _pipe@4,
                    pretty(Value, Depth + 1, Spaces)
                )
            end
        ),
        join_trees(_pipe@5, <<",\n"/utf8>>)
    end,
    _pipe@6 = gleam_stdlib:identity(<<"{\n"/utf8>>),
    _pipe@7 = gleam_stdlib:iodata_append(_pipe@6, Body),
    _pipe@8 = gleam@string_tree:append(_pipe@7, <<"\n"/utf8>>),
    _pipe@9 = gleam@string_tree:append(_pipe@8, Close_indent),
    gleam@string_tree:append(_pipe@9, <<"}"/utf8>>).

-file("src/gleamson.gleam", 681).
-spec pretty_array(list(json()), integer(), integer()) -> gleam@string_tree:string_tree().
pretty_array(Items, Depth, Spaces) ->
    Item_indent = gleam@string:repeat(<<" "/utf8>>, (Depth + 1) * Spaces),
    Close_indent = gleam@string:repeat(<<" "/utf8>>, Depth * Spaces),
    Body = begin
        _pipe = Items,
        _pipe@2 = gleam@list:map(
            _pipe,
            fun(Item) -> _pipe@1 = gleam_stdlib:identity(Item_indent),
                gleam_stdlib:iodata_append(
                    _pipe@1,
                    pretty(Item, Depth + 1, Spaces)
                ) end
        ),
        join_trees(_pipe@2, <<",\n"/utf8>>)
    end,
    _pipe@3 = gleam_stdlib:identity(<<"[\n"/utf8>>),
    _pipe@4 = gleam_stdlib:iodata_append(_pipe@3, Body),
    _pipe@5 = gleam@string_tree:append(_pipe@4, <<"\n"/utf8>>),
    _pipe@6 = gleam@string_tree:append(_pipe@5, Close_indent),
    gleam@string_tree:append(_pipe@6, <<"]"/utf8>>).

-file("src/gleamson.gleam", 670).
-spec pretty(json(), integer(), integer()) -> gleam@string_tree:string_tree().
pretty(Json, Depth, Spaces) ->
    case Json of
        {array, []} ->
            gleam_stdlib:identity(<<"[]"/utf8>>);

        {object, []} ->
            gleam_stdlib:identity(<<"{}"/utf8>>);

        {array, Items} ->
            pretty_array(Items, Depth, Spaces);

        {object, Entries} ->
            pretty_object(Entries, Depth, Spaces);

        _ ->
            to_string_tree(Json)
    end.

-file("src/gleamson.gleam", 664).
?DOC(" Like `to_string_pretty`, but with a configurable number of spaces per level.\n").
-spec to_string_pretty_with(json(), integer()) -> binary().
to_string_pretty_with(Json, Spaces) ->
    _pipe = Json,
    _pipe@1 = pretty(_pipe, 0, Spaces),
    unicode:characters_to_binary(_pipe@1).

-file("src/gleamson.gleam", 659).
?DOC(
    " Render a `Json` value as indented, human-readable text using two spaces per\n"
    " nesting level.\n"
    "\n"
    " ```gleam\n"
    " to_string_pretty(Object([#(\"a\", Int(1))]))\n"
    " // -> \"{\\n  \\\"a\\\": 1\\n}\"\n"
    " ```\n"
).
-spec to_string_pretty(json()) -> binary().
to_string_pretty(Json) ->
    to_string_pretty_with(Json, 2).

-file("src/gleamson.gleam", 768).
-spec value_for(list({binary(), json()}), binary()) -> json().
value_for(Entries, Key) ->
    case gleam@list:key_find(Entries, Key) of
        {ok, Value} ->
            Value;

        {error, _} ->
            null
    end.

-file("src/gleamson.gleam", 775).
-spec upsert(list({binary(), json()}), binary(), json()) -> list({binary(),
    json()}).
upsert(Entries, Key, Value) ->
    case gleam@list:any(Entries, fun(Kv) -> erlang:element(1, Kv) =:= Key end) of
        true ->
            gleam@list:map(
                Entries,
                fun(Kv@1) -> case erlang:element(1, Kv@1) =:= Key of
                        true ->
                            {Key, Value};

                        false ->
                            Kv@1
                    end end
            );

        false ->
            lists:append(Entries, [{Key, Value}])
    end.

-file("src/gleamson.gleam", 755).
-spec merge_entries(list({binary(), json()}), list({binary(), json()})) -> list({binary(),
    json()}).
merge_entries(Base, Patch) ->
    gleam@list:fold(
        Patch,
        Base,
        fun(Acc, Entry) ->
            {Key, Value} = Entry,
            case Value of
                null ->
                    gleam@list:filter(
                        Acc,
                        fun(Kv) -> erlang:element(1, Kv) /= Key end
                    );

                _ ->
                    upsert(Acc, Key, merge(value_for(Acc, Key), Value))
            end
        end
    ).

-file("src/gleamson.gleam", 747).
?DOC(
    " Merge `patch` into `base`. Objects are merged recursively, a `Null` in the\n"
    " patch removes that key, and any non-object patch replaces the base value.\n"
    " Handy for layering configuration or applying partial updates.\n"
    "\n"
    " ```gleam\n"
    " merge(into: Object([#(\"a\", Int(1)), #(\"b\", Int(2))]), patch: Object([#(\"b\", Null)]))\n"
    " // -> Object([#(\"a\", Int(1))])\n"
    " ```\n"
).
-spec merge(json(), json()) -> json().
merge(Base, Patch) ->
    case {Base, Patch} of
        {{object, Base_entries}, {object, Patch_entries}} ->
            {object, merge_entries(Base_entries, Patch_entries)};

        {_, _} ->
            Patch
    end.

-file("src/gleamson.gleam", 814).
-spec elements_equal(list(json()), list(json())) -> boolean().
elements_equal(A, B) ->
    case {A, B} of
        {[], []} ->
            true;

        {[X | Xs], [Y | Ys]} ->
            semantically_equal(X, Y) andalso elements_equal(Xs, Ys);

        {_, _} ->
            false
    end.

-file("src/gleamson.gleam", 799).
?DOC(
    " Compare two values for equality while ignoring the order of object keys.\n"
    " Arrays stay order-sensitive, since JSON arrays are ordered. Great for tests\n"
    " where `==` would be too strict about key order.\n"
).
-spec semantically_equal(json(), json()) -> boolean().
semantically_equal(A, B) ->
    case {A, B} of
        {{object, Entries_a}, {object, Entries_b}} ->
            (erlang:length(Entries_a) =:= erlang:length(Entries_b)) andalso gleam@list:all(
                Entries_a,
                fun(Entry) ->
                    case gleam@list:key_find(
                        Entries_b,
                        erlang:element(1, Entry)
                    ) of
                        {ok, Value_b} ->
                            semantically_equal(
                                erlang:element(2, Entry),
                                Value_b
                            );

                        {error, _} ->
                            false
                    end
                end
            );

        {{array, Items_a}, {array, Items_b}} ->
            elements_equal(Items_a, Items_b);

        {_, _} ->
            A =:= B
    end.

-file("src/gleamson.gleam", 871).
-spec unescape_token(binary()) -> binary().
unescape_token(Token) ->
    _pipe = Token,
    _pipe@1 = gleam@string:replace(_pipe, <<"~1"/utf8>>, <<"/"/utf8>>),
    gleam@string:replace(_pipe@1, <<"~0"/utf8>>, <<"~"/utf8>>).

-file("src/gleamson.gleam", 859).
-spec resolve_token(json(), binary()) -> {ok, json()} | {error, nil}.
resolve_token(Json, Token) ->
    case Json of
        {object, _} ->
            field(Json, Token);

        {array, _} ->
            case gleam_stdlib:parse_int(Token) of
                {ok, I} ->
                    index(Json, I);

                {error, _} ->
                    {error, nil}
            end;

        _ ->
            {error, nil}
    end.

-file("src/gleamson.gleam", 849).
-spec follow_tokens(json(), list(binary())) -> {ok, json()} | {error, nil}.
follow_tokens(Json, Tokens) ->
    case Tokens of
        [] ->
            {ok, Json};

        [Token | Rest] ->
            gleam@result:'try'(
                resolve_token(Json, Token),
                fun(Child) -> follow_tokens(Child, Rest) end
            )
    end.

-file("src/gleamson.gleam", 836).
?DOC(
    " Look up a value by a JSON Pointer string, e.g. `\"/user/items/0/id\"`.\n"
    "\n"
    " An empty string returns the whole document. Object keys containing `/` or\n"
    " `~` are escaped as `~1` and `~0` respectively, per RFC 6901.\n"
    "\n"
    " ```gleam\n"
    " pointer(value, \"/a/b/1\")   // 2nd element of a.b\n"
    " pointer(value, \"\")         // the whole value\n"
    " pointer(value, \"/a~1b\")    // the key \"a/b\"\n"
    " ```\n"
).
-spec pointer(json(), binary()) -> {ok, json()} | {error, nil}.
pointer(Json, Path) ->
    case Path of
        <<""/utf8>> ->
            {ok, Json};

        _ ->
            case gleam@string:split(Path, <<"/"/utf8>>) of
                [<<""/utf8>> | Tokens] ->
                    follow_tokens(
                        Json,
                        gleam@list:map(Tokens, fun unescape_token/1)
                    );

                _ ->
                    {error, nil}
            end
    end.