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.

OperationFunction formSummary
Appendmolt.appendAppend one value to an array / array of tables
Concatmolt.concatAppend several values at once
EnsureExistsmolt.ensure_existsEnsure a table / array of tables exists
Insertmolt.insertInsert into an array before an index
InsertKeymolt.insert_keyInsert a key/value before an existing key
MergeValuesmolt.merge_valuesWrite key/value entries into a table
Movemolt.moveRelocate a key or table
MoveCommentsmolt.move_commentsMove comments between nodes
MoveKeysmolt.move_keysMove a subset of keys between tables
Placemolt.placeUnconditionally write any value or structural value
Removemolt.removeDelete the node at a path
Renamemolt.renameRename the last path segment
Representationmolt.representationConvert a table between block and inline form
Setmolt.setCreate or overwrite a scalar/array/inline value
SetCommentsmolt.set_commentsReplace comments on a node
Transfermolt.transferMove all keys, then delete the source table
Updatemolt.updateTransform 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:

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:

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
Search Document