Move configuration reports to shared rendering
This commit is contained in:
@@ -1,26 +1,13 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func decodeSafeSnapshot(snapshot string) (*yaml.Node, error) {
|
||||
var document yaml.Node
|
||||
decoder := yaml.NewDecoder(strings.NewReader(snapshot))
|
||||
if err := decoder.Decode(&document); err != nil {
|
||||
return nil, fmt.Errorf("decode safe YAML snapshot: %w", err)
|
||||
}
|
||||
if document.Kind != yaml.DocumentNode || len(document.Content) == 0 {
|
||||
return nil, fmt.Errorf("decode safe YAML snapshot: document has no value")
|
||||
}
|
||||
return document.Content[0], nil
|
||||
}
|
||||
|
||||
func decodeJSONPointer(pointer string) ([]string, error) {
|
||||
if pointer == "" {
|
||||
return nil, nil
|
||||
@@ -56,139 +43,68 @@ func decodeJSONPointer(pointer string) ([]string, error) {
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
func placeReportValue(root any, path []string, value any) (any, error) {
|
||||
if len(path) == 0 {
|
||||
return value, nil
|
||||
}
|
||||
if category == nil || category.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("place YAML value at %q: category ancestor is not a mapping", pointerFromTokens(path[:1]))
|
||||
current, ok := root.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report path ancestor is not a mapping")
|
||||
}
|
||||
|
||||
current := category
|
||||
for index, token := range path {
|
||||
keys := placer.mappingKeys(current)
|
||||
keyIndex, exists := keys[token]
|
||||
if index == len(path)-1 {
|
||||
if exists {
|
||||
current.Content[keyIndex+1] = value
|
||||
} else {
|
||||
keys[token] = len(current.Content)
|
||||
current.Content = append(current.Content, stringKey(token), value)
|
||||
}
|
||||
return category, nil
|
||||
current[token] = value
|
||||
return root, nil
|
||||
}
|
||||
|
||||
child, exists := current[token]
|
||||
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
|
||||
next := make(map[string]any)
|
||||
current[token] = next
|
||||
current = next
|
||||
continue
|
||||
}
|
||||
|
||||
child := current.Content[keyIndex+1]
|
||||
if child.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("place YAML value at %q: ancestor %q is not a mapping", pointerFromTokens(path), pointerFromTokens(path[:index+1]))
|
||||
next, ok := child.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report path ancestor %q is not a mapping", token)
|
||||
}
|
||||
current = child
|
||||
current = next
|
||||
}
|
||||
return category, nil
|
||||
return root, nil
|
||||
}
|
||||
|
||||
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" {
|
||||
if _, exists := keys[candidate.Value]; !exists {
|
||||
keys[candidate.Value] = index
|
||||
}
|
||||
}
|
||||
}
|
||||
placer.keys[mapping] = keys
|
||||
return keys
|
||||
type diffReport struct {
|
||||
Additions any `json:"additions"`
|
||||
Changes any `json:"changes"`
|
||||
Deletions []string `json:"deletions"`
|
||||
}
|
||||
|
||||
func stringKey(value string) *yaml.Node {
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value}
|
||||
}
|
||||
|
||||
func pointerFromTokens(tokens []string) string {
|
||||
if len(tokens) == 0 {
|
||||
return ""
|
||||
}
|
||||
var pointer strings.Builder
|
||||
for _, token := range tokens {
|
||||
pointer.WriteByte('/')
|
||||
pointer.WriteString(strings.ReplaceAll(strings.ReplaceAll(token, "~", "~0"), "/", "~1"))
|
||||
}
|
||||
return pointer.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"}
|
||||
|
||||
func renderDiffReport(differences []configkit.Difference) (any, error) {
|
||||
report := diffReport{Additions: map[string]any{}, Changes: map[string]any{}, Deletions: []string{}}
|
||||
for _, difference := range differences {
|
||||
path := difference.Path.String()
|
||||
switch {
|
||||
case !difference.Left.Present && difference.Right.Present:
|
||||
tokens, err := decodeJSONPointer(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render addition at %q: %w", path, err)
|
||||
}
|
||||
value, err := decodeSafeSnapshot(difference.Right.YAML)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render addition at %q: %w", path, err)
|
||||
}
|
||||
additions, err = placer.placeValue(additions, tokens, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render addition at %q: %w", path, err)
|
||||
}
|
||||
case difference.Left.Present && difference.Right.Present:
|
||||
tokens, err := decodeJSONPointer(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render change at %q: %w", path, err)
|
||||
}
|
||||
value, err := decodeSafeSnapshot(difference.Right.YAML)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render change at %q: %w", path, err)
|
||||
}
|
||||
changes, err = placer.placeValue(changes, tokens, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render change at %q: %w", path, err)
|
||||
}
|
||||
case difference.Left.Present && !difference.Right.Present:
|
||||
deletions.Content = append(deletions.Content, stringKey(path))
|
||||
if difference.Left.Present && !difference.Right.Present {
|
||||
report.Deletions = append(report.Deletions, path)
|
||||
continue
|
||||
}
|
||||
if !difference.Right.Present {
|
||||
continue
|
||||
}
|
||||
tokens, err := decodeJSONPointer(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("difference at %q: %w", path, err)
|
||||
}
|
||||
value, err := commandkit.DecodeYAMLReport(strings.NewReader(difference.Right.YAML))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("difference at %q: %w", path, err)
|
||||
}
|
||||
if difference.Left.Present {
|
||||
report.Changes, err = placeReportValue(report.Changes, tokens, value)
|
||||
} else {
|
||||
report.Additions, err = placeReportValue(report.Additions, tokens, value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("difference at %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{
|
||||
stringKey("additions"), additions,
|
||||
stringKey("changes"), changes,
|
||||
stringKey("deletions"), deletions,
|
||||
}}
|
||||
document := &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{root}}
|
||||
var output bytes.Buffer
|
||||
if err := encodeYAML(&output, document); err != nil {
|
||||
return nil, fmt.Errorf("encode configuration diff report: %w", err)
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
return report, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user