Skip to main content

src/molt.erl

-module(molt).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/molt.gleam").
-export([new/0, parse/1, parse_bits/1, document_errors/1, has_errors/1, error_count/1, to_string/1, normalize/1, to_normalized_string/1, run/2, append/3, concat/3, ensure_exists/3, get/2, get_comments/2, get_document_comments/2, set_document_comments/3, has/2, insert/4, insert_key/5, keys/2, length/2, merge_values/4, move/3, move_comments/3, move_keys/5, place/3, remove/2, rename/3, representation/3, set/3, set_comments/3, set_version/2, transfer/4, update/3, update_error/1]).
-export_type([document_comment_position/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(
    " molt: TOML manipulation kit\n"
    "\n"
    " Molt parses a TOML document into a concrete syntax representation that\n"
    " can be transformed and rewritten while preserving comments associated with\n"
    " nodes.\n"
    "\n"
    " ## Path Syntax\n"
    "\n"
    " Molt functions and operations accept a path string made up of key\n"
    " segments separated by `.` (like `a.b`) and array indexes in brackets\n"
    " (like `[0]` or `[-1]`).\n"
    "\n"
    " Bare key segments may contain ASCII letters, digits, underscores, and\n"
    " dashes. Segments requiring other characters (such as spaces or dots) must\n"
    " be quoted with single quotes (no escape processing) or double quotes (with\n"
    " escape processing, e.g. `\\\"`).\n"
    "\n"
    " ```\n"
    " a.b.c.d                   // [\"a\", \"b\", \"c\", \"d\"]\n"
    " a.b.-1.d                  // [\"a\", \"b\", \"-1\", \"d\"]\n"
    " a.b.\"with space\".'[3]'    // [\"a\", \"b\", \"with space\", \"[3]\"]\n"
    " a.b.\"\\e\".'\\e'             // [\"a\", \"b\", \"\\u{001b}\", \"\\\\e\"]\n"
    " ```\n"
    "\n"
    " Array indexes must be integer values within brackets and negative indexing\n"
    " is supported. `0` is the first value in an array or array of tables, `-1` is\n"
    " the last value in an array or array of tables.\n"
    "\n"
    " ```\n"
    " a.b[-1].c                 // [\"a\", \"b\", -1, \"c\"]\n"
    " a.b[3].c                  // [\"a\", \"b\", 3, \"c\"]\n"
    " ```\n"
    "\n"
    " Path resolution behaviour with key or index values that do not resolve to\n"
    " a logical entry in the document depends on the operation performed.\n"
    "\n"
    " When operations are required against the document root, use an empty path\n"
    " string (`\"\"`).\n"
    "\n"
    " The `InvalidPath` variant of `MoltError` will be returned from\n"
    " `molt` functions if the path syntax is invalid.\n"
    "\n"
    " In some contexts, _only_ key paths may be provided.\n"
).

-type document_comment_position() :: header | trailer.

-file("src/molt.gleam", 76).
?DOC(" Create an empty TOML document.\n").
-spec new() -> molt@types:document().
new() ->
    {document,
        {toml_version, <<"1.1"/utf8>>},
        0,
        greenwood:node(root, []),
        {some, maps:new()}}.

-file("src/molt.gleam", 96).
?DOC(
    " Parse a TOML source string into a `Document`.\n"
    "\n"
    " Returns an error if the the document _cannot_ be parsed. Note that most\n"
    " documents can be parsed, but not all documents with errors produce usable or\n"
    " easily recoverable syntax trees.\n"
    "\n"
    " The document's `error_count` records how many validation errors were found;\n"
    " retrieve the full positioned list with `document_errors`.\n"
    "\n"
    " The parsed document defaults to TOML 1.1 format, even if the source file was\n"
    " TOML 1.0.\n"
).
-spec parse(binary()) -> {ok, molt@types:document()} |
    {error, molt@error:molt_error()}.
parse(Source) ->
    gleam@result:'try'(
        molt@internal@parser:parse(Source),
        fun(Tree) ->
            Error_count = molt@internal@validate:count(Tree),
            {ok,
                {document,
                    {toml_version, <<"1.1"/utf8>>},
                    Error_count,
                    Tree,
                    none}}
        end
    ).

-file("src/molt.gleam", 742).
-spec safe_bits_to_string(bitstring()) -> {ok, binary()} |
    {error, molt@error:molt_error()}.
safe_bits_to_string(Source) ->
    {Bom_prefix, Source@1} = case Source of
        <<239, 187, 191, 239, 187, 191, Rest/binary>> ->
            {<<"\x{FEFF}\x{FEFF}"/utf8>>, Rest};

        <<239, 187, 191, Rest@1/binary>> ->
            {<<"\x{FEFF}"/utf8>>, Rest@1};

        _ ->
            {<<""/utf8>>, Source}
    end,
    case gleam@bit_array:to_string(Source@1) of
        {ok, Source@2} ->
            {ok, <<Bom_prefix/binary, Source@2/binary>>};

        {error, nil} ->
            {error, invalid_source_encoding}
    end.

-file("src/molt.gleam", 108).
?DOC(
    " Parses a TOML source `BitArray` into a `Document`.\n"
    "\n"
    " Returns an error if the source is not valid UTF-8 data or if the transformed\n"
    " UTF-8 data fails on `parse`.\n"
).
-spec parse_bits(bitstring()) -> {ok, molt@types:document()} |
    {error, molt@error:molt_error()}.
parse_bits(Source) ->
    gleam@result:'try'(
        safe_bits_to_string(Source),
        fun(Source@1) -> parse(Source@1) end
    ).

-file("src/molt.gleam", 118).
?DOC(
    " Every validation error in the document as a fully-positioned `SyntaxError`,\n"
    " computed on demand from the current tree. Returns `[]` for a valid document.\n"
    "\n"
    " This is the only path that builds error spans; parsing and construction only\n"
    " count, so positions are paid for solely when you ask for them here.\n"
).
-spec document_errors(molt@types:document()) -> list(molt@types:syntax_error()).
document_errors(Doc) ->
    molt@internal@validate:enrich(erlang:element(4, Doc)).

-file("src/molt.gleam", 124).
?DOC(
    " Whether the document has any validation errors. Most document-level\n"
    " operations refuse to run while this is `True`.\n"
).
-spec has_errors(molt@types:document()) -> boolean().
has_errors(Doc) ->
    erlang:element(3, Doc) > 0.

-file("src/molt.gleam", 129).
?DOC(" The number of validation errors found in the document.\n").
-spec error_count(molt@types:document()) -> integer().
error_count(Doc) ->
    erlang:element(3, Doc).

-file("src/molt.gleam", 137).
?DOC(
    " Generate a TOML string from the document.\n"
    "\n"
    " If the document version is the same as the original version and no changes\n"
    " have been made, the original document will be reproduced exactly.\n"
).
-spec to_string(molt@types:document()) -> binary().
to_string(Doc) ->
    molt@internal@emitter:emit_versioned(
        erlang:element(4, Doc),
        erlang:element(2, Doc)
    ).

-file("src/molt.gleam", 161).
?DOC(
    " Returns a copy of the document with its tree normalized:\n"
    "\n"
    " - Unix newlines (LF) throughout.\n"
    " - Table header declarations have excess leading and interior space removed.\n"
    " - Key/value pairs have excess leading space removed, a single space after\n"
    "   the key and a single space before the value, resulting in `key = value`.\n"
    " - A single blank line separates table and array of tables headers.\n"
    " - Leading comments are preserved immediately before their node.\n"
    " - Trailing (inline) comments on key-value and header lines are preserved.\n"
    " - Inline arrays and tables without comments are collapsed to a single-line\n"
    "   form.\n"
    " - The document ends with a single trailing newline.\n"
    "\n"
    " The returned document is valid for further operations or for piping into\n"
    " `to_string`.\n"
).
-spec normalize(molt@types:document()) -> molt@types:document().
normalize(Doc) ->
    {document,
        erlang:element(2, Doc),
        erlang:element(3, Doc),
        molt@internal@emitter:normalize(erlang:element(4, Doc)),
        erlang:element(5, Doc)}.

-file("src/molt.gleam", 142).
?DOC(" Outputs a `normalize`d version of the document as a string.\n").
-spec to_normalized_string(molt@types:document()) -> binary().
to_normalized_string(Doc) ->
    _pipe = Doc,
    _pipe@1 = normalize(_pipe),
    to_string(_pipe@1).

-file("src/molt.gleam", 169).
?DOC(
    " Execute a batch of `Operation`s over a `Document` where all operations must\n"
    " succeed for an update.\n"
    "\n"
    " `Operation`s will not run over a `Document` with errors.\n"
).
-spec run(molt@types:document(), list(molt@ops:operation())) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
run(Doc, Ops) ->
    gleam@list:try_fold(Ops, Doc, fun molt@internal@document:run/2).

-file("src/molt.gleam", 181).
?DOC(
    " Appends to an array at `path`.\n"
    "\n"
    " `path` must resolve to either a value node containing an array or an array\n"
    " of tables. When `path` resolves to an array of tables, the `value`\n"
    " provided _must_ be table-like.\n"
).
-spec append(molt@types:document(), binary(), molt@value:value()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
append(Doc, Path, Value) ->
    run(Doc, [{append, Path, Value}]).

-file("src/molt.gleam", 194).
?DOC(
    " Concatenate multiple values to an array at `path`.\n"
    "\n"
    " `path` must resolve to either a value node containing an array or an array\n"
    " of tables. When `path` resolves to an array of tables, all entries in\n"
    " `values` _must_ be table-like.\n"
).
-spec concat(molt@types:document(), binary(), list(molt@value:value())) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
concat(Doc, Path, Values) ->
    run(Doc, [{concat, Path, Values}]).

-file("src/molt.gleam", 216).
?DOC(
    " Ensures that a table or array of tables exists at `path`.\n"
    "\n"
    " `kind` must be `types.Table` or `types.ArrayOfTables`; other kinds are\n"
    " rejected.\n"
    "\n"
    " If the matching structure already exists, nothing is done. If `path` does\n"
    " not resolve, an empty entry is created with implicit table ancestors as\n"
    " required. If `path` resolves to an implicit table and `kind` is\n"
    " `types.Table`, the implicit table is concretized into an emitted header\n"
    " using the key segments of `path`.\n"
    "\n"
    " A `TypeMismatch` error is returned if `path` resolves to any other value\n"
    " shape, or if any ancestor in the `path` is anything other than an implicit\n"
    " table, concrete table, or array of tables entry.\n"
).
-spec ensure_exists(molt@types:document(), binary(), molt@types:toml_kind()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
ensure_exists(Doc, Path, Kind) ->
    run(Doc, [{ensure_exists, Path, Kind}]).

-file("src/molt.gleam", 680).
-spec get_array_of_tables_entry_value(
    molt@types:document(),
    list(molt@types:path_segment()),
    integer()
) -> {ok, molt@value:value()} | {error, molt@error:molt_error()}.
get_array_of_tables_entry_value(Doc, Segments, Index) ->
    Entry_path = lists:append(Segments, [{index_segment, Index}]),
    case molt@cst:get(erlang:element(4, Doc), Entry_path) of
        {ok, Entry_node} ->
            Entries = begin
                _pipe = molt@cst:list_keys(Entry_node),
                gleam@list:filter_map(
                    _pipe,
                    fun(K) ->
                        case molt@cst:get(Entry_node, [{key_segment, K}]) of
                            {ok, Kv} ->
                                {ok, {K, molt@value:from_cst(Kv)}};

                            {error, _} ->
                                {error, nil}
                        end
                    end
                )
            end,
            {ok, molt@value:from_table_entries(Entries)};

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

-file("src/molt.gleam", 711).
-spec get_array_of_table_entry(
    molt@types:document(),
    list(molt@types:path_segment()),
    integer(),
    integer(),
    list(molt@value:value())
) -> list(molt@value:value()).
get_array_of_table_entry(Doc, Segments, Index, Count, Acc) ->
    case Index >= Count of
        true ->
            lists:reverse(Acc);

        false ->
            case get_array_of_tables_entry_value(Doc, Segments, Index) of
                {ok, V} ->
                    get_array_of_table_entry(
                        Doc,
                        Segments,
                        Index + 1,
                        Count,
                        [V | Acc]
                    );

                _ ->
                    get_array_of_table_entry(
                        Doc,
                        Segments,
                        Index + 1,
                        Count,
                        Acc
                    )
            end
    end.

-file("src/molt.gleam", 702).
-spec get_array_of_tables_as_value(
    molt@types:document(),
    list(molt@types:path_segment()),
    integer()
) -> {ok, molt@value:value()} | {error, molt@error:molt_error()}.
get_array_of_tables_as_value(Doc, Segments, Count) ->
    _pipe = get_array_of_table_entry(Doc, Segments, 0, Count, []),
    molt@value:from_array_of_tables(_pipe).

-file("src/molt.gleam", 759).
-spec get_value_by_index(
    molt@types:document(),
    gleam@dict:dict(molt@types:index_key(), molt@types:index_entry()),
    list(molt@types:path_segment())
) -> {ok, molt@value:value()} | {error, molt@error:molt_error()}.
get_value_by_index(Doc, Idx, Segments) ->
    Lookup = molt@internal@document@index:resolve_negative_indices(
        Idx,
        Segments
    ),
    Key = molt@internal@document@index:path_to_index_key(Lookup),
    case molt@internal@document@index:get(Idx, Key) of
        {ok, {index_scalar_value, Container}} ->
            Kv_key = gleam@list:drop(Lookup, erlang:length(Container)),
            case Kv_key of
                [{key_segment, _} | _] ->
                    molt@internal@document:get_value_at(Doc, Container, Kv_key);

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

        {ok, {index_array_value, Container}} ->
            Kv_key = gleam@list:drop(Lookup, erlang:length(Container)),
            case Kv_key of
                [{key_segment, _} | _] ->
                    molt@internal@document:get_value_at(Doc, Container, Kv_key);

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

        {ok, {index_inline_table_value, Container}} ->
            Kv_key = gleam@list:drop(Lookup, erlang:length(Container)),
            case Kv_key of
                [{key_segment, _} | _] ->
                    molt@internal@document:get_value_at(Doc, Container, Kv_key);

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

        {ok, {index_implicit_table, Children}} ->
            Entries = gleam@list:filter_map(
                Children,
                fun(K) ->
                    _pipe = get_value_by_index(
                        Doc,
                        Idx,
                        lists:append(Lookup, [{key_segment, K}])
                    ),
                    _pipe@1 = gleam@result:map(_pipe, fun(V) -> {K, V} end),
                    gleam@result:replace_error(_pipe@1, nil)
                end
            ),
            {ok, molt@value:from_table_entries(Entries)};

        {ok, {index_table, _}} ->
            gleam@result:'try'(
                begin
                    _pipe@2 = molt@cst:get(erlang:element(4, Doc), Lookup),
                    gleam@result:replace_error(
                        _pipe@2,
                        molt@error:not_found_path(Segments)
                    )
                end,
                fun(Node) ->
                    Entries@1 = begin
                        _pipe@3 = molt@cst:list_keys(Node),
                        gleam@list:filter_map(
                            _pipe@3,
                            fun(K@1) ->
                                _pipe@4 = molt@cst:get(
                                    Node,
                                    [{key_segment, K@1}]
                                ),
                                _pipe@5 = gleam@result:map(
                                    _pipe@4,
                                    fun(Kv) ->
                                        {K@1, molt@value:from_cst(Kv)}
                                    end
                                ),
                                gleam@result:replace_error(_pipe@5, nil)
                            end
                        )
                    end,
                    {ok, molt@value:from_table_entries(Entries@1)}
                end
            );

        {ok, {index_array_of_tables_entry, _, _, _}} ->
            gleam@result:'try'(
                begin
                    _pipe@2 = molt@cst:get(erlang:element(4, Doc), Lookup),
                    gleam@result:replace_error(
                        _pipe@2,
                        molt@error:not_found_path(Segments)
                    )
                end,
                fun(Node) ->
                    Entries@1 = begin
                        _pipe@3 = molt@cst:list_keys(Node),
                        gleam@list:filter_map(
                            _pipe@3,
                            fun(K@1) ->
                                _pipe@4 = molt@cst:get(
                                    Node,
                                    [{key_segment, K@1}]
                                ),
                                _pipe@5 = gleam@result:map(
                                    _pipe@4,
                                    fun(Kv) ->
                                        {K@1, molt@value:from_cst(Kv)}
                                    end
                                ),
                                gleam@result:replace_error(_pipe@5, nil)
                            end
                        )
                    end,
                    {ok, molt@value:from_table_entries(Entries@1)}
                end
            );

        {ok, {index_array_of_tables, Count, _}} ->
            get_array_of_tables_as_value(Doc, Lookup, Count);

        {error, nil} ->
            case molt@internal@document@index:find_deepest_ancestor_entry(
                Idx,
                Key
            ) of
                {ok, {index_scalar_value, _}} ->
                    _pipe@6 = molt@cst:get(erlang:element(4, Doc), Lookup),
                    _pipe@7 = gleam@result:map(
                        _pipe@6,
                        fun molt@value:from_cst/1
                    ),
                    gleam@result:replace_error(
                        _pipe@7,
                        molt@error:not_found_path(Segments)
                    );

                {ok, {index_array_value, _}} ->
                    _pipe@6 = molt@cst:get(erlang:element(4, Doc), Lookup),
                    _pipe@7 = gleam@result:map(
                        _pipe@6,
                        fun molt@value:from_cst/1
                    ),
                    gleam@result:replace_error(
                        _pipe@7,
                        molt@error:not_found_path(Segments)
                    );

                {ok, {index_inline_table_value, _}} ->
                    _pipe@6 = molt@cst:get(erlang:element(4, Doc), Lookup),
                    _pipe@7 = gleam@result:map(
                        _pipe@6,
                        fun molt@value:from_cst/1
                    ),
                    gleam@result:replace_error(
                        _pipe@7,
                        molt@error:not_found_path(Segments)
                    );

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

-file("src/molt.gleam", 666).
-spec get_root_as_value(molt@types:document()) -> {ok, molt@value:value()} |
    {error, molt@error:molt_error()}.
get_root_as_value(Doc) ->
    _pipe = molt@internal@document:list_keys(Doc, []),
    _pipe@3 = gleam@result:map(
        _pipe,
        fun(_capture) ->
            gleam@list:filter_map(
                _capture,
                fun(K) ->
                    _pipe@1 = molt@internal@document:get_value(Doc, [], K),
                    _pipe@2 = gleam@result:map(_pipe@1, fun(V) -> {K, V} end),
                    gleam@result:replace_error(_pipe@2, {error, nil})
                end
            )
        end
    ),
    _pipe@4 = gleam@result:unwrap(_pipe@3, []),
    _pipe@5 = molt@value:from_table_entries(_pipe@4),
    {ok, _pipe@5}.

-file("src/molt.gleam", 227).
?DOC(
    " Get the value or structure at `path` as a molt Value.\n"
    "\n"
    " Implicit tables are returned as table values.\n"
).
-spec get(molt@types:document(), binary()) -> {ok, molt@value:value()} |
    {error, molt@error:molt_error()}.
get(Doc, Path) ->
    gleam@result:'try'(
        molt@internal@path:parse(Path),
        fun(Path@1) -> case Path@1 of
                [] ->
                    get_root_as_value(Doc);

                _ ->
                    molt@internal@document@index:with_index(
                        Doc,
                        fun(Idx) -> get_value_by_index(Doc, Idx, Path@1) end
                    )
            end end
    ).

-file("src/molt.gleam", 248).
?DOC(
    " Reads the comments attached to the node at `path`.\n"
    "\n"
    " `path` must resolve to a concrete node (not an implicit table) or the root\n"
    " of the document (`\"\"`); other paths return an error.\n"
    "\n"
    " The result mirrors `set_comments`: leading comment lines and an optional\n"
    " trailing (inline) comment. Comment text is returned verbatim, including the\n"
    " leading `#`, so a value read here round-trips back through `set_comments`\n"
    " unchanged.\n"
).
-spec get_comments(molt@types:document(), binary()) -> {ok, molt@ops:comments()} |
    {error, molt@error:molt_error()}.
get_comments(Doc, Path) ->
    molt@internal@document@comments:get_comments(Doc, Path).

-file("src/molt.gleam", 270).
?DOC(
    " Reads `Header` or `Trailer` document comments.\n"
    "\n"
    " Returns the comment lines verbatim (including the leading `#` but without\n"
    " newlines) or `[]` when there are no comments.\n"
).
-spec get_document_comments(molt@types:document(), document_comment_position()) -> list(binary()).
get_document_comments(Doc, At) ->
    case At of
        header ->
            molt@cst:document_head_comments(erlang:element(4, Doc));

        trailer ->
            molt@cst:document_tail_comments(erlang:element(4, Doc))
    end.

-file("src/molt.gleam", 286).
?DOC(
    " Replaces `Header` or `Trailer` document comments, returning the updated\n"
    " document. If the comment text does not include `#`, the comment text will\n"
    " have `# ` prepended. Comments must not include newlines; they will be added\n"
    " appropriately on emit.\n"
    "\n"
    " Passing `[]` clears the comments.\n"
).
-spec set_document_comments(
    molt@types:document(),
    document_comment_position(),
    list(binary())
) -> molt@types:document().
set_document_comments(Doc, At, Comments) ->
    Tree = case At of
        header ->
            molt@cst:set_document_head_comments(
                erlang:element(4, Doc),
                Comments
            );

        trailer ->
            molt@cst:set_document_tail_comments(
                erlang:element(4, Doc),
                Comments
            )
    end,
    {document, erlang:element(2, Doc), erlang:element(3, Doc), Tree, none}.

-file("src/molt.gleam", 301).
?DOC(" Check if `path` exists in the document.\n").
-spec has(molt@types:document(), binary()) -> boolean().
has(Doc, Path) ->
    case {molt@internal@path:parse(Path),
        molt@internal@document@index:get_index(Doc)} of
        {{ok, []}, _} ->
            true;

        {{ok, Path@1}, {ok, Idx}} ->
            Lookup = molt@internal@document@index:resolve_negative_indices(
                Idx,
                Path@1
            ),
            Key = molt@internal@document@index:path_to_index_key(Lookup),
            gleam@bool:guard(
                molt@internal@document@index:has_key(Idx, Key),
                true,
                fun() ->
                    case molt@internal@document@index:find_deepest_ancestor_entry(
                        Idx,
                        Key
                    ) of
                        {ok, {index_scalar_value, _}} ->
                            _pipe = molt@cst:get(erlang:element(4, Doc), Lookup),
                            gleam@result:is_ok(_pipe);

                        {ok, {index_array_value, _}} ->
                            _pipe = molt@cst:get(erlang:element(4, Doc), Lookup),
                            gleam@result:is_ok(_pipe);

                        {ok, {index_inline_table_value, _}} ->
                            _pipe = molt@cst:get(erlang:element(4, Doc), Lookup),
                            gleam@result:is_ok(_pipe);

                        _ ->
                            false
                    end
                end
            );

        {_, _} ->
            false
    end.

-file("src/molt.gleam", 327).
?DOC(
    " Inserts a value before index `before` in an array at `path`.\n"
    "\n"
    " `path` must resolve to a value node containing an array or an array of\n"
    " tables. When `path` resolves to an array of tables, `value` must be\n"
    " table-like.\n"
).
-spec insert(molt@types:document(), binary(), integer(), molt@value:value()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
insert(Doc, Path, Before, Value) ->
    run(Doc, [{insert, Path, Before, Value}]).

-file("src/molt.gleam", 344).
?DOC(
    " Inserts a key/value pair before an existing key in the table at `path`.\n"
    "\n"
    " `path` must resolve to a concrete table, array of tables entry, or\n"
    " implicit table. Both `before` and `key` are literal key names (immediate\n"
    " children of `path`). If `before` is not found, the entry is appended.\n"
    "\n"
    " When `path` is an implicit table, the new entry is emitted as a\n"
    " root-level dotted key.\n"
).
-spec insert_key(
    molt@types:document(),
    binary(),
    binary(),
    binary(),
    molt@value:value()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
insert_key(Doc, Path, Before, Key, Value) ->
    run(Doc, [{insert_key, Path, Before, Key, Value}]).

-file("src/molt.gleam", 355).
?DOC(" Return the list of keys in a table-like value at the provided `path`.\n").
-spec keys(molt@types:document(), binary()) -> {ok, list(binary())} |
    {error, molt@error:molt_error()}.
keys(Doc, P) ->
    gleam@result:'try'(
        molt@internal@path:parse(P),
        fun(Segments) ->
            molt@internal@document@index:with_index(
                Doc,
                fun(Idx) -> case Segments of
                        [] ->
                            {ok,
                                molt@internal@document@index:root_children(Idx)};

                        _ ->
                            case molt@internal@document@index:get_path(
                                Idx,
                                Segments
                            ) of
                                {ok, {index_table, Children}} ->
                                    {ok, Children};

                                {ok, {index_implicit_table, Children}} ->
                                    {ok, Children};

                                {ok,
                                    {index_array_of_tables_entry,
                                        _,
                                        _,
                                        Children}} ->
                                    {ok, Children};

                                {ok, _} ->
                                    {error,
                                        {type_mismatch,
                                            {some,
                                                molt@internal@path:to_string(
                                                    Segments
                                                )},
                                            <<"table, implicit table, or array of tables entry"/utf8>>,
                                            <<"value"/utf8>>}};

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

-file("src/molt.gleam", 383).
?DOC(" Return the number of entries in an array or array of tables at `path`.\n").
-spec length(molt@types:document(), binary()) -> {ok, integer()} |
    {error, molt@error:molt_error()}.
length(Doc, P) ->
    gleam@result:'try'(
        molt@internal@path:parse(P),
        fun(Segments) ->
            gleam@bool:guard(
                Segments =:= [],
                {error, {invalid_operation, <<"length"/utf8>>, none}},
                fun() ->
                    molt@internal@document@index:with_index(
                        Doc,
                        fun(Idx) ->
                            case molt@internal@document@index:get_path(
                                Idx,
                                Segments
                            ) of
                                {ok, {index_array_of_tables, Count, _}} ->
                                    {ok, Count};

                                {ok, {index_array_value, _}} ->
                                    {Table_segs, Key} = molt@internal@path:split_last_segment(
                                        Segments
                                    ),
                                    molt@internal@path:with_segment_key_name(
                                        Key,
                                        {error,
                                            molt@error:not_found_path(Segments)},
                                        fun(Key_name) ->
                                            gleam@result:'try'(
                                                molt@internal@document:get_value(
                                                    Doc,
                                                    Table_segs,
                                                    Key_name
                                                ),
                                                fun(Val) ->
                                                    _pipe = molt@value:array_length(
                                                        Val
                                                    ),
                                                    gleam@option:to_result(
                                                        _pipe,
                                                        {type_mismatch,
                                                            {some,
                                                                molt@internal@path:to_string(
                                                                    Segments
                                                                )},
                                                            <<"array"/utf8>>,
                                                            molt@value:type_of(
                                                                Val
                                                            )}
                                                    )
                                                end
                                            )
                                        end
                                    );

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

                                {error, nil} ->
                                    case molt@internal@path:split_last_segment(
                                        Segments
                                    ) of
                                        {Parent, {index_segment, I}} ->
                                            case molt@internal@document@index:get_path(
                                                Idx,
                                                Parent
                                            ) of
                                                {ok, {index_array_value, _}} ->
                                                    {Table_segs@1, Key@1} = molt@internal@path:split_last_segment(
                                                        Parent
                                                    ),
                                                    molt@internal@path:with_segment_key_name(
                                                        Key@1,
                                                        {error,
                                                            molt@error:not_found_path(
                                                                Segments
                                                            )},
                                                        fun(Key_name@1) ->
                                                            gleam@result:'try'(
                                                                molt@internal@document:get_value(
                                                                    Doc,
                                                                    Table_segs@1,
                                                                    Key_name@1
                                                                ),
                                                                fun(Arr) ->
                                                                    gleam@result:'try'(
                                                                        begin
                                                                            _pipe@1 = molt@value:array_to_list(
                                                                                Arr
                                                                            ),
                                                                            gleam@result:replace_error(
                                                                                _pipe@1,
                                                                                molt@error:not_found_path(
                                                                                    Segments
                                                                                )
                                                                            )
                                                                        end,
                                                                        fun(
                                                                            Items
                                                                        ) ->
                                                                            Len = erlang:length(
                                                                                Items
                                                                            ),
                                                                            Resolved = molt@internal@utils:resolve_index(
                                                                                I,
                                                                                Len
                                                                            ),
                                                                            gleam@result:'try'(
                                                                                begin
                                                                                    _pipe@2 = molt@internal@utils:list_at(
                                                                                        Items,
                                                                                        Resolved
                                                                                    ),
                                                                                    gleam@result:replace_error(
                                                                                        _pipe@2,
                                                                                        {index_out_of_range,
                                                                                            molt@internal@path:to_string(
                                                                                                Parent
                                                                                            ),
                                                                                            I,
                                                                                            Len}
                                                                                    )
                                                                                end,
                                                                                fun(
                                                                                    Entry@1
                                                                                ) ->
                                                                                    _pipe@3 = molt@value:array_length(
                                                                                        Entry@1
                                                                                    ),
                                                                                    gleam@option:to_result(
                                                                                        _pipe@3,
                                                                                        {type_mismatch,
                                                                                            {some,
                                                                                                molt@internal@path:to_string(
                                                                                                    Segments
                                                                                                )},
                                                                                            <<"array"/utf8>>,
                                                                                            molt@value:type_of(
                                                                                                Entry@1
                                                                                            )}
                                                                                    )
                                                                                end
                                                                            )
                                                                        end
                                                                    )
                                                                end
                                                            )
                                                        end
                                                    );

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

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

-file("src/molt.gleam", 479).
?DOC(
    " Merge key/value entries into the resolved table at `path`.\n"
    "\n"
    " `path` must resolve to a concrete table or an array of tables entry;\n"
    " implicit tables and other shapes are rejected with a `TypeMismatch` error.\n"
    "\n"
    " Each key in `entries` is parsed as a path value relative to the table at\n"
    " `path`. Index segments in entry keys will be rejected with an\n"
    " `InvalidPath` error, and entry keys redefining any existing concrete table\n"
    " or value are rejected.\n"
    "\n"
    " The `on_conflict` parameter controls how collisions with existing leaf\n"
    " keys are resolved.\n"
).
-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, Path, Entries, On_conflict) ->
    run(Doc, [{merge_values, Path, Entries, On_conflict}]).

-file("src/molt.gleam", 494).
?DOC(
    " Moves the node at `from` to `to`.\n"
    "\n"
    " `from` must resolve to an existing node of any kind except the root. `to`\n"
    " must not already exist, and its last segment must be a key (not an index).\n"
    " The node is removed from `from` and re-inserted at `to`, preserving its\n"
    " structure.\n"
).
-spec move(molt@types:document(), binary(), binary()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
move(Doc, From, To) ->
    run(Doc, [{move, From, To}]).

-file("src/molt.gleam", 506).
?DOC(
    " Moves comments from the node at `from` to the node at `to`.\n"
    "\n"
    " Both `from` and `to` must resolve to concrete nodes or the root of the\n"
    " document (`\"\"`).\n"
).
-spec move_comments(molt@types:document(), binary(), binary()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
move_comments(Doc, From, To) ->
    run(Doc, [{move_comments, From, To}]).

-file("src/molt.gleam", 523).
?DOC(
    " Moves a subset of keys from the table at `from` into the table at `to`.\n"
    "\n"
    " `from` must resolve to a concrete table, implicit table, or array of\n"
    " tables entry. `keys` are literal key names naming the immediate children\n"
    " of the `from` table. Keys not present in the `from` table are ignored.\n"
    "\n"
    " If `to` does not exist or is an implicit table, a concrete table header\n"
    " will be created. The `on_conflict` parameter controls how collisions\n"
    " with existing keys in the `to` table are resolved.\n"
).
-spec move_keys(
    molt@types:document(),
    binary(),
    binary(),
    list(binary()),
    molt@ops:conflict_strategy()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
move_keys(Doc, From, To, Keys, On_conflict) ->
    run(Doc, [{move_keys, From, To, Keys, On_conflict}]).

-file("src/molt.gleam", 537).
?DOC(
    " Unconditionally places `value` at `path`.\n"
    "\n"
    " If `path` already exists, it is removed before writing the `value`.\n"
    " Structural `Value`s (table, array of tables, etc.) are permitted.\n"
).
-spec place(molt@types:document(), binary(), molt@value:value()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
place(Doc, Path, Value) ->
    run(Doc, [{place, Path, Value}]).

-file("src/molt.gleam", 549).
?DOC(
    " Removes the node at `path` from the document.\n"
    "\n"
    " If `path` resolves to an implicit table, the implicit table and _all_\n"
    " concrete nodes beneath it are removed.\n"
).
-spec remove(molt@types:document(), binary()) -> {ok, molt@types:document()} |
    {error, molt@error:molt_error()}.
remove(Doc, Path) ->
    run(Doc, [{remove, Path}]).

-file("src/molt.gleam", 563).
?DOC(
    " Renames the last segment of `path` to `to`.\n"
    "\n"
    " The last segment of `path` must be a key. `to` is a literal key name and\n"
    " must not already exist as a sibling.\n"
    "\n"
    " Renaming an implicit table renames all concrete descendants that reference\n"
    " it.\n"
).
-spec rename(molt@types:document(), binary(), binary()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
rename(Doc, Path, To) ->
    run(Doc, [{rename, Path, To}]).

-file("src/molt.gleam", 580).
?DOC(
    " Converts the structure at `path` between inline and block forms.\n"
    "\n"
    " `path` must resolve to a table or array of tables. The data is preserved;\n"
    " only the representation changes (inline table ↔ table section, array of\n"
    " inline tables ↔ array of tables entries).\n"
    "\n"
    " A `path` that does not reference a convertible structure is\n"
    " a `TypeMismatch`. Conversions that would produce invalid TOML (e.g.,\n"
    " inlining a table with sub-table descendants) are rejected.\n"
).
-spec representation(molt@types:document(), binary(), molt@ops:form()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
representation(Doc, Path, Form) ->
    run(Doc, [{representation, Path, Form}]).

-file("src/molt.gleam", 596).
?DOC(
    " Sets a value at `path` in the document.\n"
    "\n"
    " Creates or overwrites a key/value node. If `path` does not exist, it is\n"
    " created (with implicit ancestors as needed). If `path` resolves to an\n"
    " existing value node, the value is replaced.\n"
    "\n"
    " If `path` resolves to a structural node (table sections, array of tables,\n"
    " implicit tables), `Set` will return a `TypeMismatch` error.\n"
).
-spec set(molt@types:document(), binary(), molt@value:value()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
set(Doc, Path, Value) ->
    run(Doc, [{set, Path, Value}]).

-file("src/molt.gleam", 609).
?DOC(
    " Sets comments on the node at `path`.\n"
    "\n"
    " `path` must resolve to a concrete node (not an implicit table) or the root\n"
    " of the document (`\"\"`). Replaces any existing comments on the node with the\n"
    " provided `comments`.\n"
).
-spec set_comments(molt@types:document(), binary(), molt@ops:comments()) -> {ok,
        molt@types:document()} |
    {error, molt@error:molt_error()}.
set_comments(Doc, Path, Comments) ->
    run(Doc, [{set_comments, Path, Comments}]).

-file("src/molt.gleam", 620).
?DOC(
    " Set the target TOML version for output.\n"
    "\n"
    " Parsed documents default to TOML 1.1 (`molt.v1_1`).\n"
).
-spec set_version(molt@types:document(), molt@types:toml_version()) -> molt@types:document().
set_version(Doc, Version) ->
    {document,
        Version,
        erlang:element(3, Doc),
        erlang:element(4, Doc),
        erlang:element(5, Doc)}.

-file("src/molt.gleam", 633).
?DOC(
    " Transfers all keys from `from` to `to`, then removes `from`.\n"
    "\n"
    " `from` must resolve to a concrete or implicit table. `to` will be\n"
    " created as a concrete table if it does not exist. The `on_conflict`\n"
    " parameter controls how collisions with existing keys in `to` are\n"
    " resolved.\n"
).
-spec transfer(
    molt@types:document(),
    binary(),
    binary(),
    molt@ops:conflict_strategy()
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
transfer(Doc, From, To, On_conflict) ->
    run(Doc, [{transfer, From, To, On_conflict}]).

-file("src/molt.gleam", 650).
?DOC(
    " Transforms a value in place via callback.\n"
    "\n"
    " `path` must resolve to a scalar, array, or inline table value node.\n"
    " Structural types (concrete tables, implicit tables, array of tables) are\n"
    " rejected with `TypeMismatch`.\n"
    "\n"
    " Transforming inline tables or arrays round-trips through `Value`, which\n"
    " loses internal comments and multiline formatting.\n"
).
-spec update(
    molt@types:document(),
    binary(),
    fun((molt@value:value()) -> {ok, molt@value:value()} |
        {error, molt@error:molt_error()})
) -> {ok, molt@types:document()} | {error, molt@error:molt_error()}.
update(Doc, Path, With) ->
    run(Doc, [{update, Path, With}]).

-file("src/molt.gleam", 662).
?DOC(
    " Creates a `MoltError` for returning from `Update` callbacks.\n"
    "\n"
    " `Update` callbacks must return `Result(Value, MoltError)`. Use this\n"
    " function to signal a failure with a descriptive message.\n"
).
-spec update_error(binary()) -> molt@error:molt_error().
update_error(Message) ->
    {update_error, Message}.