Skip to main content

src/molt@internal@document@structure.erl

-module(molt@internal@document@structure).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/molt/internal/document/structure.gleam").
-export([ensure_exists/3, merge_values/4]).

-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(false).

-file("src/molt/internal/document/structure.gleam", 97).
?DOC(false).
-spec do_build(
    molt@types:document(),
    list(molt@types:path_segment()),
    molt@types:toml_kind()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
do_build(Doc, Segments, Kind) ->
    Key_only = molt@internal@path:path_to_table_header(Segments),
    Header = case Kind of
        array_of_tables ->
            molt@internal@cst@builder:build_empty_array_of_tables(Key_only);

        _ ->
            molt@internal@cst@builder:build_empty_table(Key_only)
    end,
    _pipe = molt@cst:insert_table_node(erlang:element(4, Doc), Header),
    gleam@result:map(
        _pipe,
        fun(_capture) ->
            molt@internal@document@primitives:rebuild(Doc, _capture)
        end
    ).

-file("src/molt/internal/document/structure.gleam", 317).
?DOC(false).
-spec rehome_scope(
    gleam@dict:dict(molt@types:index_key(), molt@types:index_entry()),
    list(list(molt@types:path_segment()))
) -> list(molt@types:path_segment()).
rehome_scope(Idx, Paths) ->
    case Paths of
        [] ->
            [];

        [First | _] ->
            case molt@internal@document@index:get_path(Idx, First) of
                {ok, {index_scalar_value, Container}} ->
                    Container;

                {ok, {index_array_value, Container}} ->
                    Container;

                {ok, {index_inline_table_value, Container}} ->
                    Container;

                _ ->
                    []
            end
    end.

-file("src/molt/internal/document/structure.gleam", 298).
?DOC(false).
-spec order_by_source(
    list(list(molt@types:path_segment())),
    gleam@dict:dict(molt@types:index_key(), molt@types:index_entry()),
    molt@types:document()
) -> list(list(molt@types:path_segment())).
order_by_source(Paths, Idx, Doc) ->
    Scope = rehome_scope(Idx, Paths),
    Ordered = case molt@cst:get(erlang:element(4, Doc), Scope) of
        {ok, Scope_node} ->
            _pipe = molt@internal@cst@elements:child_key_paths(Scope_node),
            _pipe@1 = gleam@list:map(
                _pipe,
                fun(Segs) ->
                    lists:append(
                        Scope,
                        gleam@list:map(
                            Segs,
                            fun(Field@0) -> {key_segment, Field@0} end
                        )
                    )
                end
            ),
            gleam@list:filter(
                _pipe@1,
                fun(_capture) -> gleam@list:contains(Paths, _capture) end
            );

        _ ->
            []
    end,
    Leftover = gleam@list:filter(
        Paths,
        fun(P) -> not gleam@list:contains(Ordered, P) end
    ),
    lists:append(Ordered, Leftover).

-file("src/molt/internal/document/structure.gleam", 354).
?DOC(false).
-spec path_starts_with(
    list(molt@types:path_segment()),
    list(molt@types:path_segment())
) -> boolean().
path_starts_with(Full, Prefix) ->
    case {Prefix, Full} of
        {[], _} ->
            true;

        {[_ | _], []} ->
            false;

        {[P | Ps], [F | Fs]} when P =:= F ->
            path_starts_with(Fs, Ps);

        {_, _} ->
            false
    end.

-file("src/molt/internal/document/structure.gleam", 330).
?DOC(false).
-spec collect_value_descendants_outside(
    gleam@dict:dict(molt@types:index_key(), molt@types:index_entry()),
    list(molt@types:path_segment())
) -> list(list(molt@types:path_segment())).
collect_value_descendants_outside(Idx, Prefix) ->
    Prefix_len = erlang:length(Prefix),
    gleam@dict:fold(
        Idx,
        [],
        fun(Acc, Key, Entry) ->
            Full_path = molt@internal@document@index:key_to_path(Key),
            case path_starts_with(Full_path, Prefix) of
                false ->
                    Acc;

                true ->
                    case Entry of
                        {index_scalar_value, Container} ->
                            case erlang:length(Container) < Prefix_len of
                                true ->
                                    [Full_path | Acc];

                                false ->
                                    Acc
                            end;

                        {index_array_value, Container} ->
                            case erlang:length(Container) < Prefix_len of
                                true ->
                                    [Full_path | Acc];

                                false ->
                                    Acc
                            end;

                        {index_inline_table_value, Container} ->
                            case erlang:length(Container) < Prefix_len of
                                true ->
                                    [Full_path | Acc];

                                false ->
                                    Acc
                            end;

                        _ ->
                            Acc
                    end
            end
        end
    ).

-file("src/molt/internal/document/structure.gleam", 234).
?DOC(false).
-spec do_promote_implicit(
    molt@types:document(),
    gleam@dict:dict(molt@types:index_key(), molt@types:index_entry()),
    list(molt@types:path_segment())
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
do_promote_implicit(Doc, Idx, Segments) ->
    To_rehome = begin
        _pipe = collect_value_descendants_outside(Idx, Segments),
        order_by_source(_pipe, Idx, Doc)
    end,
    gleam@result:'try'(
        gleam@list:try_map(
            To_rehome,
            fun(Full_path) ->
                gleam@result:'try'(
                    molt@cst:get(erlang:element(4, Doc), Full_path),
                    fun(Old_kv) ->
                        New_key_segments = gleam@list:drop(
                            Full_path,
                            erlang:length(Segments)
                        ),
                        New_key_names = gleam@list:filter_map(
                            New_key_segments,
                            fun(Seg) -> case Seg of
                                    {key_segment, Name} ->
                                        {ok,
                                            molt@internal@utils:quote_key(Name)};

                                    {index_segment, _} ->
                                        {error, nil}
                                end end
                        ),
                        New_kv = molt@internal@cst@elements:rewrite_kv_key_in_place(
                            Old_kv,
                            New_key_names
                        ),
                        {ok, {Full_path, New_kv}}
                    end
                )
            end
        ),
        fun(Staged) ->
            gleam@result:'try'(
                gleam@list:try_fold(
                    Staged,
                    erlang:element(4, Doc),
                    fun(Tree, Item) ->
                        {Full_path@1, _} = Item,
                        molt@cst:delete(Tree, Full_path@1)
                    end
                ),
                fun(Deleted_tree) ->
                    Key_only = molt@internal@path:path_to_table_header(Segments),
                    Header = molt@internal@cst@builder:build_empty_table(
                        Key_only
                    ),
                    gleam@result:'try'(
                        molt@cst:insert_table_node(Deleted_tree, Header),
                        fun(With_header) ->
                            Section_cst_path = gleam@list:map(
                                Key_only,
                                fun(Field@0) -> {key_segment, Field@0} end
                            ),
                            gleam@result:'try'(
                                gleam@list:try_fold(
                                    Staged,
                                    With_header,
                                    fun(Tree@1, Item@1) ->
                                        {_, New_kv@1} = Item@1,
                                        molt@cst:insert_kv(
                                            Tree@1,
                                            Section_cst_path,
                                            New_kv@1,
                                            kv_at_end
                                        )
                                    end
                                ),
                                fun(New_tree) ->
                                    {ok,
                                        molt@internal@document@primitives:rebuild(
                                            Doc,
                                            New_tree
                                        )}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/molt/internal/document/structure.gleam", 215).
?DOC(false).
-spec promote_implicit_to_concrete(
    molt@types:document(),
    list(molt@types:path_segment())
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
promote_implicit_to_concrete(Doc, Segments) ->
    molt@internal@document@index:with_index(
        Doc,
        fun(Idx) -> case molt@internal@document@index:resolve(Idx, Segments) of
                {hit, _, {index_implicit_table, _}} ->
                    do_promote_implicit(Doc, Idx, Segments);

                {hit, _, {index_table, _}} ->
                    {ok, Doc};

                {hit, _, Entry} ->
                    {error,
                        {type_mismatch,
                            {some, molt@internal@path:to_string(Segments)},
                            <<"implicit table"/utf8>>,
                            molt@internal@utils:index_entry_to_string(Entry)}};

                {miss, _, _, _} ->
                    {error, molt@error:not_found_path(Segments)};

                {fresh, _} ->
                    {error, molt@error:not_found_path(Segments)}
            end end
    ).

-file("src/molt/internal/document/structure.gleam", 31).
?DOC(false).
-spec ensure_exists(molt@types:document(), binary(), molt@types:toml_kind()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
ensure_exists(Doc, P, Kind) ->
    gleam@bool:guard(
        (Kind /= table) andalso (Kind /= array_of_tables),
        {error,
            {type_mismatch,
                {some, P},
                <<"Table or ArrayOfTables kind"/utf8>>,
                molt@internal@utils:toml_kind(Kind)}},
        fun() ->
            gleam@result:'try'(
                molt@internal@path:parse(P),
                fun(Segments) ->
                    molt@internal@document@index:with_index(
                        Doc,
                        fun(Idx) ->
                            Mismatch = fun(Got) ->
                                {error,
                                    {type_mismatch,
                                        {some, P},
                                        molt@internal@utils:toml_kind(Kind),
                                        Got}}
                            end,
                            case {molt@internal@document@index:resolve(
                                    Idx,
                                    Segments
                                ),
                                Kind} of
                                {{hit, _, {index_implicit_table, _}}, table} ->
                                    promote_implicit_to_concrete(Doc, Segments);

                                {{hit, _, {index_table, _}}, table} ->
                                    {ok, Doc};

                                {{hit,
                                        _,
                                        {index_array_of_tables_entry, _, _, _}},
                                    table} ->
                                    {ok, Doc};

                                {{hit, _, {index_array_of_tables, _, _}},
                                    array_of_tables} ->
                                    {ok, Doc};

                                {{hit, _, Entry}, _} ->
                                    Mismatch(
                                        molt@internal@utils:index_entry_to_string(
                                            Entry
                                        )
                                    );

                                {{miss, _, Ancestor, Tail}, _} ->
                                    case Ancestor of
                                        {index_scalar_value, _} ->
                                            Mismatch(
                                                molt@internal@utils:index_entry_to_string(
                                                    Ancestor
                                                )
                                            );

                                        {index_array_value, _} ->
                                            Mismatch(
                                                molt@internal@utils:index_entry_to_string(
                                                    Ancestor
                                                )
                                            );

                                        {index_inline_table_value, _} ->
                                            Mismatch(
                                                molt@internal@utils:index_entry_to_string(
                                                    Ancestor
                                                )
                                            );

                                        _ ->
                                            case molt@internal@path:contains_index(
                                                Tail
                                            ) of
                                                true ->
                                                    {error,
                                                        molt@error:not_found(P)};

                                                false ->
                                                    do_build(
                                                        Doc,
                                                        Segments,
                                                        Kind
                                                    )
                                            end
                                    end;

                                {{fresh, _}, _} ->
                                    case molt@internal@path:contains_index(
                                        Segments
                                    ) of
                                        true ->
                                            {error, molt@error:not_found(P)};

                                        false ->
                                            do_build(Doc, Segments, Kind)
                                    end
                            end
                        end
                    )
                end
            )
        end
    ).

-file("src/molt/internal/document/structure.gleam", 194).
?DOC(false).
-spec overwrite_leaf(
    molt@types:document(),
    list(molt@types:path_segment()),
    molt@value:value()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
overwrite_leaf(Doc, Full, V) ->
    gleam@result:'try'(
        begin
            _pipe = molt@internal@cst@query:get_cursor(
                erlang:element(4, Doc),
                Full
            ),
            gleam@result:replace_error(_pipe, molt@error:not_found_path(Full))
        end,
        fun(Cursor) ->
            New_node = molt@internal@document@primitives:rebuild_kv_value(
                erlang:element(2, Cursor),
                molt@value:to_cst(V)
            ),
            _pipe@1 = greenwood@zipper:set_focus(Cursor, New_node),
            _pipe@2 = greenwood@zipper:unzip(_pipe@1),
            _pipe@3 = molt@internal@document@primitives:rebuild(Doc, _pipe@2),
            {ok, _pipe@3}
        end
    ).

-file("src/molt/internal/document/structure.gleam", 155).
?DOC(false).
-spec merge_one_entry(
    molt@types:document(),
    list(molt@types:path_segment()),
    {binary(), molt@value:value()},
    molt@ops:conflict_strategy()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
merge_one_entry(Doc, Base, Entry, On_conflict) ->
    {Rel_key, V} = Entry,
    gleam@result:'try'(
        molt@internal@path:parse(Rel_key),
        fun(Rel_segments) ->
            gleam@bool:guard(
                molt@internal@path:contains_index(Rel_segments),
                {error,
                    {invalid_path,
                        <<"merge_values entry key must not contain index segments: "/utf8,
                            Rel_key/binary>>}},
                fun() ->
                    molt@internal@document@index:with_index(
                        Doc,
                        fun(Idx) ->
                            Full = lists:append(Base, Rel_segments),
                            case molt@internal@document@index:resolve(Idx, Full) of
                                {hit, _, Existing} ->
                                    case On_conflict of
                                        on_conflict_error ->
                                            {error,
                                                {already_exists,
                                                    molt@internal@path:to_string(
                                                        Full
                                                    ),
                                                    Existing}};

                                        on_conflict_skip ->
                                            {ok, Doc};

                                        on_conflict_overwrite ->
                                            overwrite_leaf(Doc, Full, V)
                                    end;

                                {miss, _, _, _} ->
                                    Kv = molt@internal@cst@builder:build_kv_from_path(
                                        molt@internal@path:path_to_table_header(
                                            Rel_segments
                                        ),
                                        molt@value:to_cst(V)
                                    ),
                                    _pipe = molt@cst:insert_kv(
                                        erlang:element(4, Doc),
                                        Base,
                                        Kv,
                                        kv_at_end
                                    ),
                                    gleam@result:map(
                                        _pipe,
                                        fun(_capture) ->
                                            molt@internal@document@primitives:rebuild(
                                                Doc,
                                                _capture
                                            )
                                        end
                                    );

                                {fresh, _} ->
                                    Kv = molt@internal@cst@builder:build_kv_from_path(
                                        molt@internal@path:path_to_table_header(
                                            Rel_segments
                                        ),
                                        molt@value:to_cst(V)
                                    ),
                                    _pipe = molt@cst:insert_kv(
                                        erlang:element(4, Doc),
                                        Base,
                                        Kv,
                                        kv_at_end
                                    ),
                                    gleam@result:map(
                                        _pipe,
                                        fun(_capture) ->
                                            molt@internal@document@primitives:rebuild(
                                                Doc,
                                                _capture
                                            )
                                        end
                                    )
                            end
                        end
                    )
                end
            )
        end
    ).

-file("src/molt/internal/document/structure.gleam", 114).
?DOC(false).
-spec merge_values(
    molt@types:document(),
    binary(),
    list({binary(), molt@value:value()}),
    molt@ops:conflict_strategy()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
merge_values(Doc, P, Entries, On_conflict) ->
    gleam@result:'try'(
        molt@internal@path:parse(P),
        fun(Segments) ->
            molt@internal@document@index:with_index(
                Doc,
                fun(Idx) ->
                    gleam@result:'try'(
                        case molt@internal@document@index:resolve(Idx, Segments) of
                            {hit, _, {index_table, _}} ->
                                {ok, nil};

                            {hit, _, {index_array_of_tables_entry, _, _, _}} ->
                                {ok, nil};

                            {hit, _, Entry} ->
                                {error,
                                    {type_mismatch,
                                        {some, P},
                                        <<"concrete table"/utf8>>,
                                        molt@internal@utils:index_entry_to_string(
                                            Entry
                                        )}};

                            {miss, _, _, _} ->
                                {error, molt@error:not_found(P)};

                            {fresh, _} ->
                                {error, molt@error:not_found(P)}
                        end,
                        fun(_) ->
                            gleam@result:'try'(
                                gleam@list:try_fold(
                                    Entries,
                                    Doc,
                                    fun(D, Entry@1) ->
                                        merge_one_entry(
                                            D,
                                            Segments,
                                            Entry@1,
                                            On_conflict
                                        )
                                    end
                                ),
                                fun(Merged) ->
                                    case molt@internal@validate:count(
                                        erlang:element(4, Merged)
                                    ) of
                                        0 ->
                                            {ok, Merged};

                                        _ ->
                                            {error,
                                                {invalid_operation,
                                                    <<"merge_values"/utf8>>,
                                                    {some,
                                                        <<<<"an entry would redefine an existing table or value at \""/utf8,
                                                                P/binary>>/binary,
                                                            "\""/utf8>>}}}
                                    end
                                end
                            )
                        end
                    )
                end
            )
        end
    ).