Skip to content

Reading a document

A Node answers more questions than "what is the value". It knows how the value was spelt, what type the schema made of it, which line it is on, who its comments belong to, and, after a projection, where every value inside it came from. This page is the reading surface in one place, worked against a single file. Every example is pinned by reading_docs_test.go; if the engine and this page disagree, the test fails.

The file:

%YAML 1.1
---
# Defaults shared by every environment.
defaults: &defaults
  timeout: 30   # seconds
  retries: 3

# The production listener.
server:
  <<: *defaults
  retries: 5
  host: 'localhost'
  port: 0x1F90
  ratio: 0.1
  id: 123456789012345678901234567890
  since: 2001-01-02
  flag: yes
  note: !!str 42
  # end of the listener
ports:
  0x1F90: public
f, _ := yamldoc.Parse(src, yamldoc.Options{})
d, _ := f.Snapshot().Document(0)

The value, the text, and the spelling

Three different questions, three different answers, all from the same node. Value is what the document's schema makes of the text; LiteralText is the text after quoting and escapes are undone but before any schema is applied; Raw is the bytes exactly as they sit in the file. TagIdentity names the type the schema settled on, and Syntax reports how it was written.

d.Get("server.…") Value() LiteralText() TagIdentity() Raw() Syntax().Style
host "localhost" localhost …:str 'localhost' single-quoted
port int64(8080) 0x1F90 …:int 0x1F90 plain
ratio float64(0.1) 0.1 …:float 0.1 plain
id *big.Int 123456789012345678901234567890 the digits …:int the digits plain
since time.Time 2001-01-02 UTC 2001-01-02 …:timestamp 2001-01-02 plain
flag true yes …:bool yes plain
note "42" 42 …:str !!str 42 plain

is tag:yaml.org,2002. flag is a boolean and since a timestamp because the document says %YAML 1.1; Document.Version reports YAML11, and under 1.2 the same text would be two strings. Syntax also carries Kind, Tag as authored (!!str), Anchor, Alias and a Location with byte span and one-based line and column; port is at line 13.

The typed accessors are strict: port.String() is ErrKind, not "8080". For numbers there are three exact forms beyond Int64 and Float64: BigInt for an integer past 64 bits, Rat for a decimal as a fraction (ratio.Rat() is 1/10), and Number, the engine's own exact value.

A key is matched by value under the same schema, so ports.Get(yamldoc.KeyStep(yamldoc.IntKey(8080))) finds 0x1F90: public. Paths and steps has the step types.

Where a value came from

Project turns a subtree into map[string]any and []any all the way down, and returns beside the value an Origin for every node it visited:

p, _ := defaults.Project(false)
p.Value                       // map[string]any{"timeout": int64(30), "retries": int64(3)}
o := p.Origins[1]             // the entry for timeout
o.Path                        // [{Key: "timeout"}]
o.Location.Start.Line         // 5
o.KeyLocation.Start.Column    // 3
o.Aliases                     // the alias hops taken to reach it, if any

That is how a tool reports server.timeout at config.yaml:5 after the value has become a plain Go map, or says that a value arrived through *defaults rather than being written where it appears. Pass expandAliases as true to follow aliases; each origin under one then lists the hop as an AliasHopLocation, the alias's location and its target's. Origin, ProjectionStep and MergeHop carry locations rather than node handles, so they outlive the revision they were read from.

Project refuses a mapping with a << key, because a merge key is not a string key. That mapping is read through EvaluateMerge.

Merge keys, with their inheritance

EvaluateMerge gives the mapping as YAML 1.1 merge semantics define it: explicit entries first, then inherited ones, each inherited entry saying which << edge supplied it.

limits := yamldoc.MergeLimits{Mappings: 100, Entries: 1000, Dependencies: 1000, Depth: 100, Work: 100000}
m, _ := server.EvaluateMerge(limits)
entries, _ := m.Entries()
Entry Via
retries none: the explicit retries: 5 wins over the inherited 3
host none
timeout one hop: Key is the << at line 10, Target the anchored mapping at line 4, Aliases the *defaults that joined them

The limits are explicit and a zero MergeLimits is refused, because merge evaluation can expand a small file into a large amount of work and the bound belongs to the caller. MergedMapping is a view of the committed revision; it does not name an edit destination, so writes still address the literal entries.

Comments as data

Every comment has one owner, and the owner can be asked for its comments. Entry comments come from Entry.Comments, sequence item comments from Sequence.ItemComments, and a container's own (its section, interior and tail comments) from Node.Comments; Document.Comments and Snapshot.Comments cover the two levels above.

root, _ := d.Root()
entries, _ := root.Entries()
for _, e := range entries {
    comments, _ := e.Comments()
    for _, c := range comments {
        text, _ := c.Content()   // " Defaults shared by every environment."
        owner, _ := c.Owner()
        kind, _ := owner.Kind()  // CommentOnMappingEntry
        where, _ := c.Location()
    }
}
Comment Reached through Owner kind
# Defaults shared by every environment. the defaults entry's Comments CommentOnMappingEntry
# seconds the timeout entry's Comments CommentOnMappingEntry
# The production listener. the server entry's Comments CommentOnMappingEntry
# end of the listener server.Comments(), at line 19 CommentOnContainer

Content is the text after the #, spacing kept, so a renderer can decide what to trim. The rules that decide ownership are in Comment ownership; this is the API that lets a linter demand a comment on every key, or a docs generator lift the comments out of a config file.

Copying a subtree

Detach closes a subtree into a Graph: aliases resolved, values exact, nothing bound to the source revision. A Graph is a value any write accepts, so a subtree moves between documents without passing through Go maps:

g, _ := defaults.Detach()
err := target.Edit(func(tx *yamldoc.Transaction) error {
    s, _ := tx.Snapshot()
    d, _ := s.Document(0)
    root, _ := d.Root()
    return tx.Set(root, yamldoc.StringStep("limits"), g)
})
service: api
limits:
  timeout: 30
  retries: 3

The copy is semantic: the destination spells it under its own version and in its own style, as How a written value is spelt describes, so a yes copied from a 1.1 document into a 1.2 one is written true.

Validating what can be validated

Validate composes the document and reports every diagnostic with a location. A file that uses a tag the profile does not know, say password: !secret vault/db, fails full validation at that node. Partial validation reports the same diagnostic, does not fail, and lists what it could not prove:

r, err := f.Snapshot().Validate(yamldoc.ValidationOptions{Mode: yamldoc.PartialValidation})
err                   // nil
r.Complete            // true: composition finished
r.Diagnostics         // one: "profile does not support this tag", line 1
r.UnavailableChecks   // the root's collection-content and the node's tag-content

An UnavailableCheck names the check by kind (CheckTagContent, CheckKeyUniqueness, CheckCollectionContent, CheckAliasContent) and the nodes it was waiting on. A diagnostic that is independently established, a dangling alias or a duplicate key, fails both modes. Edits under an unresolved tag are accepted; port: 8080 in that file can still be set.

Inspecting a document that does not compose

Literal structure does not depend on semantics. A document with a dangling alias and a duplicate key still has entries, and each has a Syntax:

f, _ := yamldoc.Parse([]byte("a: *missing\nb: 1\nb: 2\n"), yamldoc.Options{})
r, err := f.Snapshot().Validate(yamldoc.ValidationOptions{})
// err: two diagnostics; the duplicate at line 3 relates to line 2
root, _ := doc.Root()
entries, _ := root.Entries()          // three
s, _ := entries[0].Value.Syntax()     // Kind: KindAlias, Alias: "missing"
_, err = entries[0].Value.Value()     // an error: no target to resolve

So a tool can point at the offending line, name both halves of a duplicate, and walk the rest of the file. What it cannot do is edit it: a write to an invalid document is refused, because the expectation check needs a document that composes. Fix the diagnostic first.