Index mappings while assembling configuration reports

This commit is contained in:
2026-10-12 00:14:20 +00:00
parent 7bf89e9b56
commit 98c9c3f00d
3 changed files with 49 additions and 17 deletions

View File

@@ -56,7 +56,13 @@ func decodeJSONPointer(pointer string) ([]string, error) {
return parts, nil
}
func placeYAMLValue(category *yaml.Node, path []string, value *yaml.Node) (*yaml.Node, error) {
// yamlValuePlacer indexes each mapping once while assembling one report.
// Content remains the ordered representation used by the YAML encoder.
type yamlValuePlacer struct {
keys map[*yaml.Node]map[string]int
}
func (placer *yamlValuePlacer) placeValue(category *yaml.Node, path []string, value *yaml.Node) (*yaml.Node, error) {
if value == nil {
return nil, fmt.Errorf("place YAML value: value node is nil")
}
@@ -69,18 +75,21 @@ func placeYAMLValue(category *yaml.Node, path []string, value *yaml.Node) (*yaml
current := category
for index, token := range path {
keyIndex := mappingKeyIndex(current, token)
keys := placer.mappingKeys(current)
keyIndex, exists := keys[token]
if index == len(path)-1 {
if keyIndex >= 0 {
if exists {
current.Content[keyIndex+1] = value
} else {
keys[token] = len(current.Content)
current.Content = append(current.Content, stringKey(token), value)
}
return category, nil
}
if keyIndex < 0 {
if !exists {
child := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
keys[token] = len(current.Content)
current.Content = append(current.Content, stringKey(token), child)
current = child
continue
@@ -95,14 +104,24 @@ func placeYAMLValue(category *yaml.Node, path []string, value *yaml.Node) (*yaml
return category, nil
}
func mappingKeyIndex(mapping *yaml.Node, key string) int {
func (placer *yamlValuePlacer) mappingKeys(mapping *yaml.Node) map[string]int {
if keys, exists := placer.keys[mapping]; exists {
return keys
}
if placer.keys == nil {
placer.keys = make(map[*yaml.Node]map[string]int)
}
keys := make(map[string]int, len(mapping.Content)/2)
for index := 0; index+1 < len(mapping.Content); index += 2 {
candidate := mapping.Content[index]
if candidate.Kind == yaml.ScalarNode && candidate.Tag == "!!str" && candidate.Value == key {
return index
if candidate.Kind == yaml.ScalarNode && candidate.Tag == "!!str" {
if _, exists := keys[candidate.Value]; !exists {
keys[candidate.Value] = index
}
}
}
return -1
placer.keys[mapping] = keys
return keys
}
func stringKey(value string) *yaml.Node {
@@ -122,6 +141,7 @@ func pointerFromTokens(tokens []string) string {
}
func renderDiffReport(differences []configkit.Difference) ([]byte, error) {
var placer yamlValuePlacer
additions := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
changes := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
deletions := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
@@ -138,7 +158,7 @@ func renderDiffReport(differences []configkit.Difference) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("render addition at %q: %w", path, err)
}
additions, err = placeYAMLValue(additions, tokens, value)
additions, err = placer.placeValue(additions, tokens, value)
if err != nil {
return nil, fmt.Errorf("render addition at %q: %w", path, err)
}
@@ -151,7 +171,7 @@ func renderDiffReport(differences []configkit.Difference) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("render change at %q: %w", path, err)
}
changes, err = placeYAMLValue(changes, tokens, value)
changes, err = placer.placeValue(changes, tokens, value)
if err != nil {
return nil, fmt.Errorf("render change at %q: %w", path, err)
}

View File

@@ -74,6 +74,7 @@ func TestDecodeJSONPointerPreservesSemanticTokens(t *testing.T) {
}
func TestPlaceYAMLValueBuildsOrderedSemanticMappings(t *testing.T) {
var placer yamlValuePlacer
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
paths := [][]string{
{"parent", "0", "dot.name", "50%"},
@@ -83,7 +84,7 @@ func TestPlaceYAMLValueBuildsOrderedSemanticMappings(t *testing.T) {
}
for index, path := range paths {
value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: string(rune('a' + index))}
updated, err := placeYAMLValue(category, path, value)
updated, err := placer.placeValue(category, path, value)
if err != nil {
t.Fatalf("place %q: %v", pointerFromTokens(path), err)
}
@@ -108,16 +109,17 @@ func TestPlaceYAMLValueBuildsOrderedSemanticMappings(t *testing.T) {
}
func TestPlaceYAMLValueHandlesRootAndEmptyKey(t *testing.T) {
var placer yamlValuePlacer
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
root := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}
updated, err := placeYAMLValue(category, nil, root)
updated, err := placer.placeValue(category, nil, root)
if err != nil || updated != root {
t.Fatalf("root placement = %#v, %v", updated, err)
}
category = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "empty-key"}
updated, err = placeYAMLValue(category, []string{""}, value)
updated, err = placer.placeValue(category, []string{""}, value)
if err != nil {
t.Fatal(err)
}
@@ -127,10 +129,11 @@ func TestPlaceYAMLValueHandlesRootAndEmptyKey(t *testing.T) {
}
func TestPlaceYAMLValueRejectsIncompatibleAncestors(t *testing.T) {
var placer yamlValuePlacer
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{
stringKey("parent"), {Kind: yaml.ScalarNode, Tag: "!!str", Value: "leaf"},
}}
_, err := placeYAMLValue(category, []string{"parent", "child"}, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"})
_, err := placer.placeValue(category, []string{"parent", "child"}, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"})
if err == nil || !strings.Contains(err.Error(), "/parent/child") || !strings.Contains(err.Error(), "/parent") {
t.Fatalf("incompatible ancestor error = %v", err)
}