molt Operations
Every edit in molt is an ops.Operation. The high-level functions on molt
(e.g. molt.set) are each single-operation sugar over molt.run, which applies
a list of molt/ops operations to a document.
import molt
import molt/ops
import molt/value
molt.run folds operations over the document, short-circuiting on the first
error: either the document is fully transformed, or you get the Error from the
operation that failed and your original doc binding is left untouched. It
refuses to run against a document that still has validation errors (see the
usage guide). Every example below shows the
single-operation function form and its molt.run batch equivalent; the two
are exactly interchangeable.
Operations are grouped in this document by purpose: writing values, relocating nodes, array and table editing, representation, and comments.
| Operation | Function form | Summary |
|---|---|---|
Append | molt.append | Append one value to an array / array of tables |
Concat | molt.concat | Append several values at once |
EnsureExists | molt.ensure_exists | Ensure a table / array of tables exists |
Insert | molt.insert | Insert into an array before an index |
InsertKey | molt.insert_key | Insert a key/value before an existing key |
MergeValues | molt.merge_values | Write key/value entries into a table |
Move | molt.move | Relocate a key or table |
MoveComments | molt.move_comments | Move comments between nodes |
MoveKeys | molt.move_keys | Move a subset of keys between tables |
Place | molt.place | Unconditionally write any value or structural value |
Remove | molt.remove | Delete the node at a path |
Rename | molt.rename | Rename the last path segment |
Representation | molt.representation | Convert a table between block and inline form |
Set | molt.set | Create or overwrite a scalar/array/inline value |
SetComments | molt.set_comments | Replace comments on a node |
Transfer | molt.transfer | Move all keys, then delete the source table |
Update | molt.update | Transform a value via a callback |
Examples below use small, focused documents so each input and output is exact. Molt preserves the formatting, comments, and whitespace of every node an operation does not touch; newly created nodes get uniform formatting.
Set
Set(path: String, value: Value)
molt.set(doc: Document, path: String, value: Value)
Creates or overwrites a scalar, array, or inline-table value at path. If
path does not exist it is created, with implicit ancestors as needed; if it
resolves to an existing value node, the value is replaced in place, preserving
its surrounding formatting and comments.
Set deals in value nodes only — on both the path and the value side — and
returns TypeMismatch when either would be structural:
-
If
pathresolves to a structural node (a section table, array of tables, or implicit table). UsePlaceorRepresentationto overwrite structure. -
If
valuewould render a structural node (a section table or array of tables): the intent of thoseValueshapes is a header, not a value. To write a[header]/[[header]], usePlace; to write the same data inline, coerce first withvalue.as_inline_tableorvalue.as_array.
Input
gleam = 1
|> Transform
molt.set(doc, "a.b", value.int(42))
molt.run(doc, [
ops.Set(path: "a.b", value: value.int(42)),
])
⇒ Output
gleam = 1
a.b = 42
Place
Place(path: String, value: Value)
molt.place(doc: Document, path: String, value: Value)
Writes value at path, removing whatever is already there first. Unlike
Set, Place accepts structural values, so it can replace a value with a table
or a table with a value.
Input
server = "old"
|> Transform
molt.place(
doc,
"server",
value.table([#("host", value.string("localhost"))]),
)
molt.run(doc, [
ops.Place(
path: "server",
value: value.table([#("host", value.string("localhost"))]),
),
])
⇒ Output
server = { host = "localhost" }
Remove
Remove(path: String)
molt.remove(doc: Document, path: String)
Deletes the node at path. If the path resolves to an implicit table, the
implicit table and all concrete nodes beneath it are removed.
In the example below a.b is an implicit table (implied by a.b.q and the
[a.b.c] header), so removing it takes a.b.q and the whole [a.b.c] subtree
with it. To delete a single leaf, target it directly with
molt.remove(doc, "a.b.q").
Input
gleam = 1
a.b.q = 42
[a.b.c]
d = 67
|> Transform
molt.remove(doc, "a.b")
molt.run(doc, [ops.Remove(path: "a.b")])
⇒ Output
gleam = 1
Move
Move(from: String, to: String)
molt.move(doc: Document, from: String, to: String)
Relocates a node from from to to. The destination must not already exist and
its last segment must be a key (not an index). Works for keys, tables, and array
of tables. Moving a table rewrites its header: molt.move(doc, "a.b", "c")
turns [a.b] into [c], carrying its keys and comments along.
Input
[a]
x = 10
y = 20
[b]
z = 30
|> Transform
molt.move(doc, "a.y", "b.y")
molt.run(doc, [ops.Move(from: "a.y", to: "b.y")])
⇒ Output
[a]
x = 10
[b]
z = 30
y = 20
Rename
Rename(path: String, to: String)
molt.rename(doc: Document, path: String, to: String)
Renames the last segment of path to to. The new name must not already exist
as a sibling. Renaming an implicit table updates every concrete descendant that
references it. For a table, molt.rename(doc, "a.b", "config") renames the last
segment only: [a.b] becomes [a.config].
Input
rating = 4.5
|> Transform
molt.rename(doc, "rating", "score")
molt.run(doc, [ops.Rename(path: "rating", to: "score")])
⇒ Output
score = 4.5
MoveKeys
MoveKeys(
from: String,
to: String,
keys: List(String),
on_conflict: ConflictStrategy
)
molt.move_keys(
doc: Document,
from: String,
to: String,
keys: List(String),
on_conflict: ConflictStrategy,
)
Moves the named keys from the table at from into the table at to. Keys not
present in from are ignored. If to does not exist (or is implicit) a
concrete table is created. on_conflict follows the
conflict strategies.
Input
[source]
a = 1
b = 2
c = 3
[target]
z = 99
|> Transform
molt.move_keys(
doc,
from: "source",
to: "target",
keys: ["a", "b"],
on_conflict: ops.OnConflictError,
)
molt.run(doc, [
ops.MoveKeys(
from: "source",
to: "target",
keys: ["a", "b"],
on_conflict: ops.OnConflictError,
),
])
⇒ Output
[source]
c = 3
[target]
z = 99
a = 1
b = 2
Transfer
Transfer(from: String, to: String, on_conflict: ConflictStrategy)
molt.transfer(
doc: Document,
from: String,
to: String,
on_conflict: ConflictStrategy,
)
Moves all keys from from into to, then removes the now-empty from table.
to is created if it does not exist. on_conflict follows the
conflict strategies.
Input
[old]
a = 1
b = 2
[new]
z = 9
|> Transform
molt.transfer(
doc,
from: "old",
to: "new",
on_conflict: ops.OnConflictError,
)
molt.run(doc, [
ops.Transfer(
from: "old",
to: "new",
on_conflict: ops.OnConflictError
),
])
⇒ Output
[new]
z = 9
a = 1
b = 2
MergeValues
MergeValues(
path: String,
entries: List(#(String, Value)),
on_conflict: ConflictStrategy,
)
molt.merge_values(
doc: Document,
path: String,
entries: List(#(String, Value)),
on_conflict: ConflictStrategy,
)
Writes a list of #(key, value) entries into the concrete table (or array of
tables entry) at path. Each entry key is parsed as a path relative to path,
so dotted keys nest. on_conflict follows the
conflict strategies, applied per existing leaf key.
Input
[server]
host = "localhost"
|> Transform
molt.merge_values(
doc,
"server",
[#("port", value.int(8080)), #("timeout", value.int(30))],
ops.OnConflictOverwrite,
)
molt.run(doc, [
ops.MergeValues(
path: "server",
entries: [
#("port", value.int(8080)),
#("timeout", value.int(30))
],
on_conflict: ops.OnConflictOverwrite,
),
])
⇒ Output
[server]
host = "localhost"
port = 8080
timeout = 30
Append
Append(path: String, value: Value)
molt.append(doc: Document, path: String, value: Value)
Appends one value to the array (or array of tables) at path. For an array of
tables the value must be table-like; appending one adds a new [[…]] entry. The
first example below appends to a plain array, the second to an array of tables.
Input
tags = ["a", "b"]
|> Transform
molt.append(doc, "tags", value.string("c"))
molt.run(doc, [
ops.Append(path: "tags", value: value.string("c")),
])
⇒ Output
tags = ["a", "b", "c"]
Input
[[plugins]]
name = "formatter"
|> Transform
molt.append(
doc,
"plugins",
value.table([#("name", value.string("linter"))]),
)
molt.run(doc, [
ops.Append(
path: "plugins",
value: value.table([#("name", value.string("linter"))]),
),
])
⇒ Output
[[plugins]]
name = "formatter"
[[plugins]]
name = "linter"
Concat
Concat(path: String, values: List(Value))
molt.concat(doc: Document, path: String, values: List(Value))
Like Append, but adds several values in one operation.
Input
tags = ["a"]
|> Transform
molt.concat(doc, "tags", [value.string("b"), value.string("c")])
molt.run(doc, [
ops.Concat(
path: "tags",
values: [value.string("b"), value.string("c")]
),
])
⇒ Output
tags = ["a", "b", "c"]
Insert
Insert(path: String, before: Int, value: Value)
molt.insert(doc: Document, path: String, before: Int, value: Value)
Inserts value before index before in the array at path. Negative indexes
count from the end: before: -1 inserts before the last element, before: 0
inserts at the front.
Input
tags = ["a", "c"]
|> Transform
molt.insert(doc, "tags", before: 1, value: value.string("b"))
molt.run(doc, [
ops.Insert(path: "tags", before: 1, value: value.string("b")),
])
⇒ Output
tags = ["a", "b", "c"]
InsertKey
InsertKey(path: String, before: String, key: String, value: Value)
molt.insert_key(
doc: Document,
path: String,
before: String,
key: String,
value: Value,
)
Inserts a key/value pair before an existing key in the table at path,
preserving order. If before is not found, the new entry is appended.
Input
[server]
host = "localhost"
port = 8080
|> Transform
molt.insert_key(
doc,
"server",
before: "port",
key: "timeout",
value: value.int(30),
)
molt.run(doc, [
ops.InsertKey(
path: "server",
before: "port",
key: "timeout",
value: value.int(30),
),
])
⇒ Output
[server]
host = "localhost"
timeout = 30
port = 8080
EnsureExists
EnsureExists(path: String, kind: TomlKind)
molt.ensure_exists(doc: Document, path: String, kind: TomlKind)
Creates a table or array of tables at path. If the structure already exists,
nothing changes; if path is an implicit table and kind is types.Table, the
implicit table is promoted into an explicit header. The kind parameter must be
types.Table or types.ArrayOfTables.
Input
[a]
x = 1
|> Transform
import molt/types
molt.ensure_exists(doc, "b", types.Table)
molt.run(doc, [
ops.EnsureExists(path: "b", kind: types.Table),
])
⇒ Output
[a]
x = 1
[b]
Representation
Representation(path: String, form: Form)
molt.representation(doc: Document, path: String, form: Form)
Converts the table or array of tables at path between block form ([table]
headers) and inline form ({ … } / [{ … }]). Data is preserved; only the
representation changes. Conversions that would produce invalid TOML (e.g.
inlining a table with sub-table descendants) are rejected. The example below
converts to inline form with ops.Inline, then feeds that result back
through the reverse, ops.Block, to recover the block form.
Input
[server]
host = "localhost"
port = 8080
|> Transform
molt.representation(doc, "server", ops.Inline)
molt.run(doc, [
ops.Representation(path: "server", form: ops.Inline),
])
⇒ Output
server = { host = "localhost", port = 8080 }
|> Transform
molt.representation(doc, "server", ops.Block)
molt.run(doc, [
ops.Representation(path: "server", form: ops.Block),
])
⇒ Output
[server]
host = "localhost"
port = 8080
Update
Update(path: String, with: fn(Value) -> Result(Value, MoltError))
molt.update(
doc: Document,
path: String,
with: fn(Value) -> Result(Value, MoltError)
)
Transforms the value at path through a callback returning
Result(Value, MoltError). Only scalar, array, and inline-table values are
permitted; structural types are rejected. Round-tripping an array or inline
table through Value drops interior comments and multiline formatting.
To fail the update with a custom message, return
Error(molt.update_error("reason")) from the callback. molt.run then
short-circuits and returns that UpdateError.
Input
[server]
port = 8080
|> Transform
molt.update(doc, "server.port", fn(v) {
case value.unwrap_int(v) {
Ok(n) -> Ok(value.int(n * 2))
Error(e) -> Error(e)
}
})
molt.run(doc, [
ops.Update(path: "server.port", with: fn(v) {
value.unwrap_int(v)
|> result.map(fn(n) { value.int(n * 2 )})
}),
])
⇒ Output
[server]
port = 16160
SetComments
SetComments(path: String, comments: Comments)
molt.set_comments(doc: Document, path: String, comments: Comments)
Replaces the comments on the node at path with an ops.Comments
value: leading lines above the node, and an optional trailing comment on its
line. The path must resolve to a concrete node (not an implicit table) or the
root of the document (""). To read comments back, see molt.get_comments in
the usage guide.
Input
[server]
port = 8080
|> Transform
import gleam/option.{Some}
molt.set_comments(
doc,
"server.port",
ops.Comments(
leading: ["Listen port"],
trailing: Some("default")
),
)
molt.run(doc, [
ops.SetComments(
path: "server.port",
comments: ops.Comments(
leading: ["Listen port"],
trailing: Some("default")
),
),
])
⇒ Output
[server]
# Listen port
port = 8080 # default
MoveComments
MoveComments(from: String, to: String)
molt.move_comments(doc: Document, from: String, to: String)
Moves the comments from the node at from to the node at to. Both must be
concrete nodes or the root of the document ("").
Input
# keep me
host = "localhost"
port = 8080
|> Transform
molt.move_comments(doc, "host", "port")
molt.run(doc, [ops.MoveComments(from: "host", to: "port")])
⇒ Output
host = "localhost"
# keep me
port = 8080
Parameter Types
A few operations take a dedicated molt/ops type as an argument. They are
collected here and linked from the operations that use them.
Conflict Strategies
MoveKeys, Transfer, and MergeValues take an on_conflict argument
(ops.ConflictStrategy) that decides what happens when a key being written
already exists in the destination:
ops.OnConflictError: abort the whole operation with an error, changing nothing.ops.OnConflictOverwrite: replace the existing destination value with the incoming one.ops.OnConflictSkip: keep the existing destination value and drop the incoming one.
Comments
SetComments takes an ops.Comments(leading:, trailing:): leading is the
list of comment lines above the node, and trailing is an optional inline
comment on the node’s own line. A leading # is added automatically if you omit
it.
Representation Form
Representation takes a Form: ops.Inline converts a table to inline form
({ … } / [{ … }]), and ops.Block converts it back to block form ([table]
headers).
Batch Execution
molt.run applies a list of operations as one atomic batch: it folds them over
the document in order and short-circuits on the first error. Either every
operation applies and you get the transformed document, or one fails and you get
its Error. In the example below, if rating does not exist, molt.run
returns that operation’s Error.
let assert Ok(doc) =
molt.run(doc, [
ops.Set(path: "name", value: value.string("my_action")),
ops.Rename(path: "rating", to: "score"),
ops.MoveKeys(
from: "build.bundle",
to: "build",
keys: ["minify"],
on_conflict: ops.OnConflictError,
),
ops.Representation(path: "repository", form: ops.Inline),
])
Recipes
The operations and the high-level functions in molt provide a rich vocabulary for document migrations, but some complex edits require using these in interesting ways. This is a collection of useful recipes from these functions and operations.
Rename a Key in All Array of Tables Entries
If you need to rename all instances of a key in an array of tables, it’s
necessary to loop over each entry to perform the rename. This recipe shows
renaming the srv array of tables to server and in each entry, the required
key addr to host, and the optional key prt to port.
Input
[[srv]]
addr = "a"
prt = 22
[[srv]]
addr = "b"
|> Transform
import gleam/bool
import gleam/int
import gleam/list
let assert Ok(indices) =
molt.length(doc, "srv")
|> result.map(fn(n) {
int.range(from: 0, to: n, with: [], run: fn(acc, i) {
["srv[" <> int.to_string(i) <> "]", ..acc]
})
})
let assert Ok(renamed) =
list.try_fold(indices, doc, fn(doc, entry) {
use doc <- result.try(molt.rename(doc,entry <> ".addr", "host"))
let port = entry <> ".prt"
use <- bool.guard(!molt.has(doc, port), return: Ok(doc))
molt.rename(doc, port, "port")
})
|> result.try(molt.rename("srv", "server"))
⇒ Output
[[server]]
host = "a"
port = 22
[[server]]
host = "b"
Copy a Table
There is no Copy operation, but with molt.get and either molt.place or
molt.append you can copy the contents of tables to new locations.
Input
[[item]]
name = "x"
qty = 1
|> Transform
import gleam/result
let assert Ok(value) = molt.get(doc, "item[0]")
let assert Ok(duplicated) =
molt.append(doc, "item", value)
|> result.try(molt.place(_, "default_item", value))
⇒ Output
[[item]]
name = "x"
qty = 1
[[item]]
name = "x"
qty = 1
[default_item]
name = "x"
qty = 1