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
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)
})
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.