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) (string, bool)
- func IsMergeKey(n *yaml.Node) bool
- func PureRefTargetOf(pairs []Pair) (string, bool)
- type Pair
- type View
- func (v *View) ChildByToken(n *yaml.Node, token string) *yaml.Node
- func (v *View) DocumentPath(root *yaml.Node, pointer string) (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 string) (path []*yaml.Node, complete bool)
- func (v *View) PureRefTarget(n *yaml.Node) (string, 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 it names 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.
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. A cached expansion is always the depth-0 expansion, independent of the path that first reached it. 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 ¶
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 ¶
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.