Edit a value¶
Two routes lead to the same engine. Document.Set is a complete transaction
that applies one change; File.Edit is a callback that applies several as one.
Both take the same Go values and give the same result for a single change.
Change an existing scalar¶
The scalar is edited where it sits, so its inline comment, its column and its quoting style survive:
A value that was quoted stays quoted; one that was bare stays bare, unless the new value needs quoting to keep its meaning:
doc.Set("name", "after") // quoted: "before" → quoted: "after"
doc.Set("kind", "after") // kind: before → kind: after
doc.Set("kind", "true") // kind: before → kind: "true" (a string, not a boolean)
doc.Set("note", "a # b") // note: before → note: "a # b" (not a comment)
That last pair is the type contract: the value you write is the value that
reads back, under the document's own YAML version. A Go string never becomes a
boolean, a number or a comment; a float64(1) is written as 1.0, not 1;
float64(0.1) is written as 0.1. Strings that contain characters a reader
cannot see, such as a zero-width space or a bidi override, are written
double-quoted with escapes.
A string with line breaks is written as a literal block, the way a person writes one, with the chomping indicator the text's trailing breaks call for:
"a\nb" gives |-, "a\n\n" gives |+. The block goes below the key at the
document's own indentation step. It stays double-quoted where a block cannot
go, and a single-quoted scalar keeps its quotes; the complete rules for every
kind of value and every kind of existing scalar are in
How a written value is spelt.
Set accepts strings, booleans, every integer and float width, *big.Int,
time.Time and []byte (each written with an explicit timestamp or binary
tag, so it reads back as what it was), nil, and maps, slices and arrays of
those for whole subtrees. A Go map has no order, so its keys are written
sorted; use MappingInput to write entries in an order of your choosing.
Create a key¶
Setting a path that does not exist creates it, appended to the end of the deepest existing ancestor's entries. Appending rather than inserting is deliberate: existing keys keep their comments and their order.
Missing intermediate mappings are created on the way, in block style at the document's own indentation step:
Replace a subtree¶
Passing a map or a MappingInput replaces the addressed mapping: keys absent
from the input are removed, keys present are updated, and the input's order
wins.
err := doc.Set("avatars.matt", yamldoc.MappingInput{
{Key: likeness, Value: "a bearded developer"},
{Key: style, Value: "flat"},
})
An entry whose key matches one already there keeps its comments and, for a scalar that has not changed, its exact bytes:
avatars:
matt:
# who he is
likeness: old # inline
style: flat
extra: 1
# becomes
avatars:
matt:
# who he is
likeness: a bearded developer # inline
style: flat
Keys are matched by YAML value, so 8080 and 0x1f90 are the same key, and a
replacement that could match one input key to two existing entries is refused
as ambiguous rather than guessed at.
A sequence is replaced whole
A sequence has no keys to match on, so replacing one replaces every item: the old items' comments go with them, whatever the new values happen to be. To change one item, address it with an index instead.
Change one item in a list¶
An index in square brackets reaches into a sequence from a dotted path:
_ = doc.Set("servers[0].port", 9090) // replace a value inside the first item
_ = doc.Set("servers[2]", newServer) // append: 2 is the sequence's length
_ = doc.Remove("servers[1]") // the survivors keep their comments
Inside Edit, the same reach is a Step:
err := f.Edit(func(tx *yamldoc.Transaction) error {
s, _ := tx.Snapshot()
d, _ := s.Document(0)
servers, err := d.Get("servers")
if err != nil {
return err
}
port, err := servers.Get(yamldoc.IndexStep(0), yamldoc.StringStep("port"))
if err != nil {
return err
}
return tx.Replace(port, 9090)
})
tx.Insert(servers, yamldoc.IndexStep(i), value) adds an item before index
i (or at the end when i is the length), and tx.Remove(servers,
yamldoc.IndexStep(i)) removes one. Surviving items keep their comments.
An index that names a gap (servers[5] on a two-item sequence) is
ErrNotFound rather than a guess at padding; the rules are in
Paths and steps.
Change a value's kind¶
A scalar can become a mapping or a sequence and the other way round. The enclosing entry's comments stay with it; the old value's own comments go.
Where an inline comment follows the scalar, the new collection is written in flow style on the same line so the comment keeps its place.
What cannot be created¶
A path that runs through a scalar is refused rather than silently rewriting it:
Creating it would mean destroying a, which you did not ask for.
A key cannot be created in a document whose root is a sequence or a scalar. In
an empty stream, tx.CreateDocument(value) creates the first document; a
stream that already has one is edited, not extended.
Writes through aliases and anchors¶
An edit under an anchor works and keeps the anchor, so every alias sees the new value. An edit over an alias is refused: changing the shared value and detaching this one reference are different intentions, and the library will not guess. A removal that would leave an alias dangling is refused too.
doc.Set("defaults.retries", 5) // fine: edits under &defaults
doc.Set("prod.settings", value) // ErrAlias: prod.settings is *defaults
doc.Remove("defaults") // ErrReference: *defaults still needs it
Errors worth branching on¶
switch {
case errors.Is(err, yamldoc.ErrNotFound): // the path does not resolve
case errors.Is(err, yamldoc.ErrKind): // a step runs through the wrong kind of node
case errors.Is(err, yamldoc.ErrPath): // malformed dotted path
case errors.Is(err, yamldoc.ErrAlias): // the target is an alias
case errors.Is(err, yamldoc.ErrReference): // the edit would strand an alias
case errors.Is(err, yamldoc.ErrSemantic): // the document has invalid content the edit depends on
}
Each sentinel is in Errors.