Documentation
¶
Overview ¶
Package nodeview reads a YAML mapping the way the resolver will: through aliases, through `<<` merge keys, and with duplicate keys resolved the way the parser resolves them.
It is separate from the scans that first needed it because the schema lowering needs the same view — a `$dynamicAnchor` lookup and an anchor walk both read mappings the raw yaml.Node tree does not present directly. A view over source text is neither a scan nor a lowering, so it sits below both.
Index ¶
- Constants
- func Deref(n *yaml.Node) *yaml.Node
- func DocumentRoot(n *yaml.Node) *yaml.Node
- func InternalPointer(ref string) (jsontext.Pointer, bool)
- func IsMergeKey(n *yaml.Node) bool
- func PureRefTargetOf(pairs []Pair) (jsontext.Pointer, bool)
- type Pair
- type View
- func (v *View) ChildByToken(n *yaml.Node, token string) *yaml.Node
- func (v *View) DocumentPath(root *yaml.Node, pointer jsontext.Pointer) (path []*yaml.Node, complete bool)
- func (v *View) Exhausted() bool
- func (v *View) MappingPairs(n *yaml.Node) []Pair
- func (v *View) PointerPath(root *yaml.Node, pointer jsontext.Pointer) (path []*yaml.Node, complete bool)
- func (v *View) PureRefTarget(n *yaml.Node) (jsontext.Pointer, bool)
Constants ¶
const MergeDepthLimit = 64
MergeDepthLimit bounds how deep a chain of `<<` merge keys the mapping view expands. It is far tighter than maxAliasChain: each merge level re-materializes every pair beneath it, so expanding a chain of depth d costs O(d²), and real specs nest merge keys one or two levels deep. A chain that hits the bound is reported via a diag.CycleScanFailed warning (see View.expand and refCycles), not silently truncated.
Variables ¶
This section is empty.
Functions ¶
func Deref ¶
Deref follows AliasNode links to the anchored node, bounded against an alias chain that loops (the anchor-cycle detector reports those separately).
func DocumentRoot ¶
DocumentRoot returns the effective root node to scan: the content of a document node, or the node itself otherwise. It returns nil for an empty document.
func InternalPointer ¶
InternalPointer reports the JSON pointer a $ref value names inside this document, and whether the resolver walks it as a pointer into this document at all.
It mirrors the resolver exactly: speakeasy splits a $ref on '#', treats what precedes it as a URI and what follows as the pointer, trims whitespace from both, and percent-decodes the pointer (references/reference.go GetURI and GetJSONPointer, v1.24.0). A ref whose URI half is empty names this document.
Reading the raw value instead is not a near-enough approximation, it is a hole in the cycle scan: a pointer this package calls dangling but the resolver resolves is a reference the scan cannot see, and '#/paths/~1a ' — one trailing space — is enough to be one. A dependency bump should re-check those two methods, as MergeDepthLimit's comment does for the behavior it tracks.
The mirror holds the other way too. A fragment that does not start with '/' is refused: the resolver never walks one as a pointer — its pointer walk rejects it, and `#name` is a $anchor it looks up by name if at all — and walking the name from the root as a key refused a document as a cycle the resolver never enters (GitHub #523). A chain the resolver does follow through such a lookup is one the scan cannot see (GitHub #526). A bare '#' stays: it names the root, where the resolver lands it.
A fragment that decodes to bytes that are not UTF-8 stays as well, although resolve.FragmentPointer refuses it (GitHub #520). No document key spells those bytes, but the resolver walks the pointer anyway, through every token before the one it cannot find, and a walk through a reference already on the chain is the re-entrant hop the scan refuses whether or not the pointer then resolves.
func IsMergeKey ¶
IsMergeKey reports whether a raw mapping key node is a `<<` merge key, applying the same test speakeasy does: yml.IsMergeKey (yml/yml.go), run over every mapping via yml.ResolveMergeKeys. The key is checked undereferenced (an alias standing in for the key is not a scalar) and by resolved tag (a quoted '<<' resolves to !!str) — speakeasy treats both as ordinary keys, and expanding them would invent pairs it never sees.
yaml.v3's own decoder (isMerge in decode.go) is the wrong model to copy: it's laxer about the tag (also accepts an empty or non-specific one) and stricter about repetition (honors only the last `<<`, where speakeasy merges every one — why expandContent accumulates them all). Neither difference is reachable from a parsed document today, but re-check this against yml.IsMergeKey on any dependency bump.
func PureRefTargetOf ¶
PureRefTargetOf is PureRefTarget over an already-expanded pair list, so a caller that needs both the pairs and the target expands the mapping once. The target is normalized by InternalPointer, so it is a bare pointer ('/a/b'), not the '#/a/b' the source spells.
Types ¶
type Pair ¶
Pair is one effective key/value pair of a mapping node, after alias and merge-key resolution.
type View ¶
type View struct {
// contains filtered or unexported fields
}
View reads a raw yaml.Node tree the way speakeasy's unmarshaller reads it: alias keys and values dereferenced, `<<` merge keys expanded (yml.ResolveAlias and yml.ResolveMergeKeys, applied per mapping in marshaller/unmarshaller.go). Every gap between the raw tree this scan reads and the resolved one speakeasy's resolver reads is a cycle that reaches the resolver and faults the process (GitHub #26) — so every mapping read in this file goes through a View.
It memoizes each mapping's expansion for the scan's lifetime: without that, a merge chain costs O(n) per expansion and O(n) expansions per walk, going cubic in chain length — a hang where the bug being fixed was a crash. The memo is a pure cache: a read answers exactly what a fresh view would answer at the same depth, whatever was read before it, which is what makes one view safe to share across independent walks (see expansion and View.serves). MergeDepthLimit and maxCachedPairs bound the chain depth and cache size respectively, so unlimited memoization can't trade the crash for exhausted memory instead.
It memoizes one thing more, for the walk rather than the expansion: keyIndex projects a memoized mapping into a key map, so descending a JSON pointer costs a map read per token instead of a scan of every pair at each one.
func New ¶
func New() *View
New returns an empty view; a view must not outlive the node tree whose expansions it caches.
func (*View) ChildByToken ¶
ChildByToken returns the child of a mapping (by key) or sequence (by index) node named by one JSON pointer token, or nil when absent. The mapping arm reads through the view, so pointer navigation resolves an alias key and an aliased or merged value exactly as PureRefTarget does.
n itself is not dereferenced, which is where this parts company with its two neighbours: MappingPairs and PureRefTarget both take an alias standing in for a whole mapping and read the mapping it names, while an alias handed here matches neither arm and answers nil. Every caller reaches a node through a walk that dereferences as it goes — PointerPath does it at each hop — so the difference is unreachable rather than harmless, and it is written down because the sibling promising the opposite is one line away.
func (*View) DocumentPath ¶
func (v *View) DocumentPath(root *yaml.Node, pointer jsontext.Pointer) (path []*yaml.Node, complete bool)
DocumentPath walks a pointer that names a position in this document rather than a reference some source wrote, and is otherwise PointerPath.
The two part company on '/'. PointerPath lands it on the root because that is where the resolver lands it, a departure from RFC 6901 that tokenless records. A position built by ids.Ptr carries no such departure: ids.Ptr("") spells the root member whose key is the empty string exactly '/', so reading that as the root walks past the member the pointer names. Only the empty pointer names the root here.
The distinction is load-bearing for a caller reading $id down a path: taking '/' for the root hides an $id written on that member, which is the same dropped-empty-token loss the rest of this walk exists to avoid.
func (*View) Exhausted ¶
Exhausted reports whether the view stopped short of a full expansion at its merge-depth bound. A caller that refuses a document on incomplete information needs to say so rather than report a clean scan, which is the one thing the bound must not be allowed to hide.
func (*View) MappingPairs ¶
MappingPairs returns the effective pairs of a mapping node, following speakeasy's precedence: an explicit key beats one from a merge regardless of where the `<<` appears, an earlier merge source beats a later one on a shared key (yml.resolveMergeKeys), and a key repeated explicitly resolves to its last value.
n is dereferenced, so an alias standing in for a whole mapping can be passed directly; a non-mapping node (including nil) yields no pairs. The returned slice is the view's own memo — callers must treat it as read-only.
func (*View) PointerPath ¶
func (v *View) PointerPath(root *yaml.Node, pointer jsontext.Pointer) (path []*yaml.Node, complete bool)
PointerPath walks a normalized internal JSON pointer ('/a/b', as InternalPointer returns it) against the root node, keeping every node it passes through: element 0 is the root and each later element is the node reached by one more token. complete reports whether every token resolved; when it is false the walk stopped at the last element returned, and there is no destination. Alias nodes along the path are dereferenced so navigation follows structure.
It yields the whole path rather than just the target because a pointer's danger is not always at its destination. speakeasy resolves a reference while holding that reference's own lock and read-locks every reference the pointer walk passes through, so a pointer that traverses a reference already being resolved deadlocks before it ever arrives (v1.24.0, openapi/reference.go resolve/GetObject). A target alone cannot express that.
The leading '/' introduces the first token rather than being one, and every later '/' separates two — so '/a/' carries the tokens "a" and "", the second naming a member whose key is the empty string (RFC 6901 §3, and jsonpointer/navigation.go getNavigationStack, v1.24.0, which reads it the same way). Dropping the empty token instead is what a pointer's danger being upstream of its destination makes unsafe: it turns a node the walk descends *through* into the node it stops at, and the caller's re-entrancy check exempts exactly that node (GitHub #238).
func (*View) PureRefTarget ¶
PureRefTarget reports the internal $ref target of a node that carries a top-level internal ('#/...') $ref. Sibling keys do not disqualify it: speakeasy follows a node's top-level $ref before any concrete sibling, so a $ref node with a type or properties sibling still drives the crash. The chain terminates only at a node with no top-level $ref at all.