Share semantic document traversal helpers
This commit is contained in:
@@ -8,7 +8,8 @@ module and package documentation boundary. Its implemented public primitives are
|
|||||||
- canonical, control-safe diagnostic rendering and structured `LoadError` access;
|
- canonical, control-safe diagnostic rendering and structured `LoadError` access;
|
||||||
- opaque file, byte, reader, and no-input source construction in `source.go`,
|
- opaque file, byte, reader, and no-input source construction in `source.go`,
|
||||||
with shared regular-file checks and bounded read mechanics in `source_file.go`;
|
with shared regular-file checks and bounded read mechanics in `source_file.go`;
|
||||||
- a parser-independent ordered tree in `internal/document`;
|
- a parser-independent ordered tree in `internal/document`, with root-private
|
||||||
|
semantic lookup, indexing, and value traversal in `document_walk.go`;
|
||||||
- the sole upstream YAML integration in `internal/yaml`, which translates validated
|
- the sole upstream YAML integration in `internal/yaml`, which translates validated
|
||||||
input into that owned tree and exposes no parser types outside its boundary;
|
input into that owned tree and exposes no parser types outside its boundary;
|
||||||
`parse.go` owns shared translation into detached, ordered public diagnostics;
|
`parse.go` owns shared translation into detached, ordered public diagnostics;
|
||||||
|
|||||||
@@ -163,7 +163,14 @@ reliably induce an important filesystem failure. Run race tests for this unit.
|
|||||||
|
|
||||||
## Stage 4 — Establish shared document traversal ownership
|
## Stage 4 — Establish shared document traversal ownership
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
|
Completion: `document_walk.go` owns nil-safe preorder value walking, borrowed
|
||||||
|
indexing, and semantic lookup. Metadata shapes and projected sensitivity use the
|
||||||
|
walker with defensive path copies. Specialized origins and budget traversals
|
||||||
|
remain explicit. Full offline tests (including historical path shapes, metadata
|
||||||
|
detachment, and dynamic secrets), vet, build, and whitespace checks passed with
|
||||||
|
Go 1.26.7 on Linux/amd64.
|
||||||
|
|
||||||
Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat
|
Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat
|
||||||
semantic tree traversal in [load_metadata.go](../../load_metadata.go),
|
semantic tree traversal in [load_metadata.go](../../load_metadata.go),
|
||||||
|
|||||||
66
document_walk.go
Normal file
66
document_walk.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
|
||||||
|
// walkDocument visits value nodes in preorder, preserving stored mapping order
|
||||||
|
// and sequence index order. Mapping keys are not separate nodes in this walk.
|
||||||
|
// The callback borrows paths and nodes; clone any mutable state it retains.
|
||||||
|
// Specialized origin, pruning, and budget traversals retain their own semantics.
|
||||||
|
func walkDocument(node *document.Node, path Path, visit func(Path, *document.Node)) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visit(path, node)
|
||||||
|
switch node.Kind {
|
||||||
|
case document.Mapping:
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
walkDocument(entry.Value, path.Key(entry.Key), visit)
|
||||||
|
}
|
||||||
|
case document.Sequence:
|
||||||
|
for index, child := range node.Sequence {
|
||||||
|
walkDocument(child, path.Index(index), visit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexDocument borrows nodes from its caller-owned tree.
|
||||||
|
func indexDocument(node *document.Node, path Path, index map[string]*document.Node) {
|
||||||
|
walkDocument(node, path, func(path Path, node *document.Node) {
|
||||||
|
index[path.String()] = node
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
|
||||||
|
if root == nil || !path.valid() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
current := root
|
||||||
|
for _, segment := range path.segments {
|
||||||
|
switch current.Kind {
|
||||||
|
case document.Mapping:
|
||||||
|
if segment.kind == pathSegmentIndex {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, entry := range current.Mapping {
|
||||||
|
if entry.Key == segment.key {
|
||||||
|
current = entry.Value
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
case document.Sequence:
|
||||||
|
index, ok := schemaIndex(segment)
|
||||||
|
if !ok || index >= len(current.Sequence) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
current = current.Sequence[index]
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current, true
|
||||||
|
}
|
||||||
35
load.go
35
load.go
@@ -400,41 +400,6 @@ func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
|
|||||||
return canonicalDiagnostics(state.diagnostics)
|
return canonicalDiagnostics(state.diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
|
|
||||||
if root == nil || !path.valid() {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
current := root
|
|
||||||
for _, segment := range path.segments {
|
|
||||||
switch current.Kind {
|
|
||||||
case document.Mapping:
|
|
||||||
if segment.kind == pathSegmentIndex {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, entry := range current.Mapping {
|
|
||||||
if entry.Key == segment.key {
|
|
||||||
current = entry.Value
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
case document.Sequence:
|
|
||||||
index, ok := schemaIndex(segment)
|
|
||||||
if !ok || index >= len(current.Sequence) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
current = current.Sequence[index]
|
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return current, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func documentLocationPointer(location document.Location) *Location {
|
func documentLocationPointer(location document.Location) *Location {
|
||||||
if !locationKnown(location) {
|
if !locationKnown(location) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -159,23 +159,6 @@ func collectFieldInfo(node *document.Node, path Path, decoded decodedConfigurati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexDocument(node *document.Node, path Path, index map[string]*document.Node) {
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
index[path.String()] = node
|
|
||||||
switch node.Kind {
|
|
||||||
case document.Mapping:
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
indexDocument(entry.Value, path.Key(entry.Key), index)
|
|
||||||
}
|
|
||||||
case document.Sequence:
|
|
||||||
for position, child := range node.Sequence {
|
|
||||||
indexDocument(child, path.Index(position), index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneMetadata(metadata Metadata) Metadata {
|
func cloneMetadata(metadata Metadata) Metadata {
|
||||||
if metadata.fields == nil && metadata.history == nil {
|
if metadata.fields == nil && metadata.history == nil {
|
||||||
return Metadata{}
|
return Metadata{}
|
||||||
|
|||||||
23
metadata.go
23
metadata.go
@@ -85,8 +85,8 @@ func (m Metadata) hasHistoricalPath(path Path) bool {
|
|||||||
func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) {
|
func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) {
|
||||||
m.effectivePaths = make(map[string]Path)
|
m.effectivePaths = make(map[string]Path)
|
||||||
m.historicalPaths = make(map[string][]Path)
|
m.historicalPaths = make(map[string][]Path)
|
||||||
walkDocumentPaths(final, Path{}, func(path Path) { m.effectivePaths[path.String()] = path.clone() })
|
walkDocument(final, Path{}, func(path Path, _ *document.Node) { m.effectivePaths[path.String()] = path.clone() })
|
||||||
add := func(path Path) {
|
add := func(path Path, _ *document.Node) {
|
||||||
pointer := path.String()
|
pointer := path.String()
|
||||||
for _, prior := range m.historicalPaths[pointer] {
|
for _, prior := range m.historicalPaths[pointer] {
|
||||||
if matchesPathShape(path, prior) {
|
if matchesPathShape(path, prior) {
|
||||||
@@ -95,9 +95,9 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his
|
|||||||
}
|
}
|
||||||
m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone())
|
m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone())
|
||||||
}
|
}
|
||||||
walkDocumentPaths(final, Path{}, add)
|
walkDocument(final, Path{}, add)
|
||||||
for _, tree := range earlier {
|
for _, tree := range earlier {
|
||||||
walkDocumentPaths(tree, Path{}, add)
|
walkDocument(tree, Path{}, add)
|
||||||
}
|
}
|
||||||
for _, application := range applications {
|
for _, application := range applications {
|
||||||
// Overrides have mapping-only prefixes; parsed tokens are keys in this layer.
|
// Overrides have mapping-only prefixes; parsed tokens are keys in this layer.
|
||||||
@@ -105,19 +105,6 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his
|
|||||||
for _, segment := range application.path.segments {
|
for _, segment := range application.path.segments {
|
||||||
prefix = prefix.Key(segment.key)
|
prefix = prefix.Key(segment.key)
|
||||||
}
|
}
|
||||||
walkDocumentPaths(application.value, prefix, add)
|
walkDocument(application.value, prefix, add)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func walkDocumentPaths(node *document.Node, path Path, visit func(Path)) {
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
visit(path)
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
walkDocumentPaths(entry.Value, path.Key(entry.Key), visit)
|
|
||||||
}
|
|
||||||
for i, child := range node.Sequence {
|
|
||||||
walkDocumentPaths(child, path.Index(i), visit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
secret.go
15
secret.go
@@ -144,21 +144,10 @@ func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) {
|
|||||||
// is equal. Validators must preserve this inspection metadata as well as values.
|
// is equal. Validators must preserve this inspection metadata as well as values.
|
||||||
func projectedSensitivity(root *document.Node) map[string]Path {
|
func projectedSensitivity(root *document.Node) map[string]Path {
|
||||||
result := make(map[string]Path)
|
result := make(map[string]Path)
|
||||||
var visit func(*document.Node, Path)
|
walkDocument(root, Path{}, func(path Path, node *document.Node) {
|
||||||
visit = func(node *document.Node, path Path) {
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if node.Sensitive {
|
if node.Sensitive {
|
||||||
result[path.String()] = path.clone()
|
result[path.String()] = path.clone()
|
||||||
}
|
}
|
||||||
for _, entry := range node.Mapping {
|
})
|
||||||
visit(entry.Value, path.Key(entry.Key))
|
|
||||||
}
|
|
||||||
for i, child := range node.Sequence {
|
|
||||||
visit(child, path.Index(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visit(root, Path{})
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user