src/lattice_presence@state_json.erl

-module(lattice_presence@state_json).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lattice_presence/state_json.gleam").
-export([to_json/1, to_json_string/1, decoder/0, from_json/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.

?MODULEDOC(
    " State JSON — serialization and deserialization for the presence CRDT\n"
    "\n"
    " Provides JSON encoding and decoding of the `State` type for cross-node\n"
    " replication.\n"
).

-file("src/lattice_presence/state_json.gleam", 114).
-spec encode_entry(lattice_presence@presence_state:entry()) -> gleam@json:json().
encode_entry(Entry) ->
    gleam@json:object(
        [{<<"topic"/utf8>>, gleam@json:string(erlang:element(2, Entry))},
            {<<"key"/utf8>>, gleam@json:string(erlang:element(3, Entry))},
            {<<"pid"/utf8>>, gleam@json:string(erlang:element(4, Entry))},
            {<<"meta"/utf8>>, erlang:element(5, Entry)}]
    ).

-file("src/lattice_presence/state_json.gleam", 97).
-spec encode_tag(lattice_presence@presence_state:tag()) -> gleam@json:json().
encode_tag(Tag) ->
    gleam@json:object(
        [{<<"replica"/utf8>>, gleam@json:string(erlang:element(2, Tag))},
            {<<"clock"/utf8>>, gleam@json:int(erlang:element(3, Tag))}]
    ).

-file("src/lattice_presence/state_json.gleam", 134).
-spec encode_values(
    gleam@dict:dict(lattice_presence@presence_state:tag(), lattice_presence@presence_state:entry())
) -> gleam@json:json().
encode_values(Values) ->
    _pipe = Values,
    _pipe@1 = maps:to_list(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Kv) ->
            gleam@json:object(
                [{<<"tag"/utf8>>, encode_tag(erlang:element(1, Kv))},
                    {<<"entry"/utf8>>, encode_entry(erlang:element(2, Kv))}]
            )
        end
    ),
    gleam@json:preprocessed_array(_pipe@2).

-file("src/lattice_presence/state_json.gleam", 75).
-spec encode_clouds(gleam@dict:dict(binary(), gleam@set:set(integer()))) -> gleam@json:json().
encode_clouds(Clouds) ->
    _pipe = Clouds,
    _pipe@1 = maps:to_list(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Kv) ->
            {erlang:element(1, Kv),
                gleam@json:array(
                    gleam@set:to_list(erlang:element(2, Kv)),
                    fun gleam@json:int/1
                )}
        end
    ),
    gleam@json:object(_pipe@2).

-file("src/lattice_presence/state_json.gleam", 56).
-spec encode_context(gleam@dict:dict(binary(), integer())) -> gleam@json:json().
encode_context(Context) ->
    _pipe = Context,
    _pipe@1 = maps:to_list(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Kv) ->
            {erlang:element(1, Kv), gleam@json:int(erlang:element(2, Kv))}
        end
    ),
    gleam@json:object(_pipe@2).

-file("src/lattice_presence/state_json.gleam", 19).
?DOC(" Encode a CRDT State to JSON\n").
-spec to_json(lattice_presence@presence_state:state()) -> gleam@json:json().
to_json(S) ->
    {Replica, Context, Clouds, Values} = lattice_presence@presence_state:replicated_parts(
        S
    ),
    gleam@json:object(
        [{<<"replica"/utf8>>, gleam@json:string(Replica)},
            {<<"context"/utf8>>, encode_context(Context)},
            {<<"clouds"/utf8>>, encode_clouds(Clouds)},
            {<<"values"/utf8>>, encode_values(Values)}]
    ).

-file("src/lattice_presence/state_json.gleam", 30).
?DOC(" Encode a State to a JSON string\n").
-spec to_json_string(lattice_presence@presence_state:state()) -> binary().
to_json_string(S) ->
    _pipe = to_json(S),
    gleam@json:to_string(_pipe).

-file("src/lattice_presence/state_json.gleam", 226).
-spec json_value_dict(
    list({binary(), gleam@dynamic:dynamic_()}),
    list({binary(), gleam@json:json()}),
    integer()
) -> gleam@dynamic@decode:decoder(gleam@json:json()).
json_value_dict(Pairs, Acc, Depth) ->
    case Pairs of
        [] ->
            gleam@dynamic@decode:success(gleam@json:object(lists:reverse(Acc)));

        [{Key, Value} | Rest] ->
            case gleam@dynamic@decode:run(Value, json_value_decoder_at(Depth)) of
                {ok, Val} ->
                    json_value_dict(Rest, [{Key, Val} | Acc], Depth);

                {error, _} ->
                    gleam@dynamic@decode:failure(
                        gleam@json:null(),
                        <<"valid JSON value in object"/utf8>>
                    )
            end
    end.

-file("src/lattice_presence/state_json.gleam", 211).
-spec json_value_list(
    list(gleam@dynamic:dynamic_()),
    list(gleam@json:json()),
    integer()
) -> gleam@dynamic@decode:decoder(gleam@json:json()).
json_value_list(Items, Acc, Depth) ->
    case Items of
        [] ->
            gleam@dynamic@decode:success(
                gleam@json:preprocessed_array(lists:reverse(Acc))
            );

        [Item | Rest] ->
            case gleam@dynamic@decode:run(Item, json_value_decoder_at(Depth)) of
                {ok, Val} ->
                    json_value_list(Rest, [Val | Acc], Depth);

                {error, _} ->
                    gleam@dynamic@decode:failure(
                        gleam@json:null(),
                        <<"valid JSON value in array"/utf8>>
                    )
            end
    end.

-file("src/lattice_presence/state_json.gleam", 184).
-spec json_value_decoder_within_limit(integer()) -> gleam@dynamic@decode:decoder(gleam@json:json()).
json_value_decoder_within_limit(Depth) ->
    gleam@dynamic@decode:one_of(
        begin
            _pipe = {decoder, fun gleam@dynamic@decode:decode_string/1},
            gleam@dynamic@decode:map(_pipe, fun gleam@json:string/1)
        end,
        [begin
                _pipe@1 = {decoder, fun gleam@dynamic@decode:decode_int/1},
                gleam@dynamic@decode:map(_pipe@1, fun gleam@json:int/1)
            end,
            begin
                _pipe@2 = {decoder, fun gleam@dynamic@decode:decode_float/1},
                gleam@dynamic@decode:map(_pipe@2, fun gleam@json:float/1)
            end,
            begin
                _pipe@3 = {decoder, fun gleam@dynamic@decode:decode_bool/1},
                gleam@dynamic@decode:map(_pipe@3, fun gleam@json:bool/1)
            end,
            begin
                _pipe@4 = gleam@dynamic@decode:optional(
                    {decoder, fun gleam@dynamic@decode:decode_string/1}
                ),
                gleam@dynamic@decode:then(_pipe@4, fun(Opt) -> case Opt of
                            none ->
                                gleam@dynamic@decode:success(gleam@json:null());

                            {some, _} ->
                                gleam@dynamic@decode:failure(
                                    gleam@json:null(),
                                    <<"null"/utf8>>
                                )
                        end end)
            end,
            begin
                _pipe@5 = gleam@dynamic@decode:list(
                    {decoder, fun gleam@dynamic@decode:decode_dynamic/1}
                ),
                gleam@dynamic@decode:then(
                    _pipe@5,
                    fun(Items) -> json_value_list(Items, [], Depth + 1) end
                )
            end,
            begin
                _pipe@6 = gleam@dynamic@decode:dict(
                    {decoder, fun gleam@dynamic@decode:decode_string/1},
                    {decoder, fun gleam@dynamic@decode:decode_dynamic/1}
                ),
                gleam@dynamic@decode:then(
                    _pipe@6,
                    fun(D) ->
                        Pairs = maps:to_list(D),
                        json_value_dict(Pairs, [], Depth + 1)
                    end
                )
            end]
    ).

-file("src/lattice_presence/state_json.gleam", 177).
-spec json_value_decoder_at(integer()) -> gleam@dynamic@decode:decoder(gleam@json:json()).
json_value_decoder_at(Depth) ->
    case Depth > 64 of
        true ->
            gleam@dynamic@decode:failure(
                gleam@json:null(),
                <<"metadata depth within limit"/utf8>>
            );

        false ->
            json_value_decoder_within_limit(Depth)
    end.

-file("src/lattice_presence/state_json.gleam", 173).
?DOC(
    " Decoder that reconstructs a json.Json value from parsed JSON. Uses\n"
    " standard decoder combinators instead of BEAM-specific dynamic.classify\n"
    " so the same code works on both the Erlang and JavaScript targets.\n"
).
-spec json_value_decoder() -> gleam@dynamic@decode:decoder(gleam@json:json()).
json_value_decoder() ->
    json_value_decoder_at(0).

-file("src/lattice_presence/state_json.gleam", 126).
-spec entry_decoder() -> gleam@dynamic@decode:decoder(lattice_presence@presence_state:entry()).
entry_decoder() ->
    gleam@dynamic@decode:field(
        <<"topic"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Topic) ->
            gleam@dynamic@decode:field(
                <<"key"/utf8>>,
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Key) ->
                    gleam@dynamic@decode:field(
                        <<"pid"/utf8>>,
                        {decoder, fun gleam@dynamic@decode:decode_string/1},
                        fun(Pid) ->
                            gleam@dynamic@decode:field(
                                <<"meta"/utf8>>,
                                json_value_decoder(),
                                fun(Meta) ->
                                    gleam@dynamic@decode:success(
                                        {entry, Topic, Key, Pid, Meta}
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/lattice_presence/state_json.gleam", 104).
-spec tag_decoder() -> gleam@dynamic@decode:decoder(lattice_presence@presence_state:tag()).
tag_decoder() ->
    gleam@dynamic@decode:field(
        <<"replica"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Replica) ->
            gleam@dynamic@decode:field(
                <<"clock"/utf8>>,
                {decoder, fun gleam@dynamic@decode:decode_int/1},
                fun(Clock) -> case Clock > 0 of
                        true ->
                            gleam@dynamic@decode:success({tag, Replica, Clock});

                        false ->
                            gleam@dynamic@decode:failure(
                                {tag, Replica, Clock},
                                <<"positive tag clock"/utf8>>
                            )
                    end end
            )
        end
    ).

-file("src/lattice_presence/state_json.gleam", 146).
-spec values_decoder() -> gleam@dynamic@decode:decoder(gleam@dict:dict(lattice_presence@presence_state:tag(), lattice_presence@presence_state:entry())).
values_decoder() ->
    _pipe = gleam@dynamic@decode:list(
        begin
            gleam@dynamic@decode:field(
                <<"tag"/utf8>>,
                tag_decoder(),
                fun(Tag) ->
                    gleam@dynamic@decode:field(
                        <<"entry"/utf8>>,
                        entry_decoder(),
                        fun(Entry) ->
                            gleam@dynamic@decode:success({Tag, Entry})
                        end
                    )
                end
            )
        end
    ),
    gleam@dynamic@decode:map(_pipe, fun maps:from_list/1).

-file("src/lattice_presence/state_json.gleam", 164).
-spec all_cloud_clocks_positive(gleam@dict:dict(binary(), list(integer()))) -> boolean().
all_cloud_clocks_positive(Values) ->
    gleam@dict:fold(
        Values,
        true,
        fun(Valid, _, Clocks) ->
            Valid andalso gleam@list:all(Clocks, fun(Clock) -> Clock > 0 end)
        end
    ).

-file("src/lattice_presence/state_json.gleam", 82).
-spec clouds_decoder() -> gleam@dynamic@decode:decoder(gleam@dict:dict(binary(), gleam@set:set(integer()))).
clouds_decoder() ->
    _pipe = gleam@dynamic@decode:dict(
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        gleam@dynamic@decode:list(
            {decoder, fun gleam@dynamic@decode:decode_int/1}
        )
    ),
    gleam@dynamic@decode:then(
        _pipe,
        fun(D) -> case all_cloud_clocks_positive(D) of
                true ->
                    gleam@dynamic@decode:success(
                        gleam@dict:map_values(
                            D,
                            fun(_, Clocks) -> gleam@set:from_list(Clocks) end
                        )
                    );

                false ->
                    gleam@dynamic@decode:failure(
                        maps:new(),
                        <<"positive cloud clocks"/utf8>>
                    )
            end end
    ).

-file("src/lattice_presence/state_json.gleam", 157).
-spec all_dict_values(
    gleam@dict:dict(binary(), integer()),
    fun((integer()) -> boolean())
) -> boolean().
all_dict_values(Values, Predicate) ->
    gleam@dict:fold(
        Values,
        true,
        fun(Valid, _, Value) -> Valid andalso Predicate(Value) end
    ).

-file("src/lattice_presence/state_json.gleam", 63).
-spec context_decoder() -> gleam@dynamic@decode:decoder(gleam@dict:dict(binary(), integer())).
context_decoder() ->
    _pipe = gleam@dynamic@decode:dict(
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        {decoder, fun gleam@dynamic@decode:decode_int/1}
    ),
    gleam@dynamic@decode:then(
        _pipe,
        fun(Context) ->
            case all_dict_values(Context, fun(Clock) -> Clock >= 0 end) of
                true ->
                    gleam@dynamic@decode:success(Context);

                false ->
                    gleam@dynamic@decode:failure(
                        Context,
                        <<"non-negative context clocks"/utf8>>
                    )
            end
        end
    ).

-file("src/lattice_presence/state_json.gleam", 41).
?DOC(
    " Decoder for the CRDT State type. Used by `from_json` and\n"
    " available for embedding in larger decoders (e.g. sync envelope parsing).\n"
).
-spec decoder() -> gleam@dynamic@decode:decoder(lattice_presence@presence_state:state()).
decoder() ->
    gleam@dynamic@decode:field(
        <<"replica"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Replica) ->
            gleam@dynamic@decode:field(
                <<"context"/utf8>>,
                context_decoder(),
                fun(Context) ->
                    gleam@dynamic@decode:field(
                        <<"clouds"/utf8>>,
                        clouds_decoder(),
                        fun(Clouds) ->
                            gleam@dynamic@decode:field(
                                <<"values"/utf8>>,
                                values_decoder(),
                                fun(Values) ->
                                    gleam@dynamic@decode:success(
                                        lattice_presence@presence_state:from_replicated_parts(
                                            Replica,
                                            Context,
                                            Clouds,
                                            Values
                                        )
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/lattice_presence/state_json.gleam", 35).
?DOC(" Decode a JSON string into a State\n").
-spec from_json(binary()) -> {ok, lattice_presence@presence_state:state()} |
    {error, gleam@json:decode_error()}.
from_json(Json_string) ->
    gleam@json:parse(Json_string, decoder()).