package ckconfig import ( "bytes" "fmt" "strings" configkit "gitea.maximumdirect.net/eric/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 } if pointer[0] != '/' { return nil, fmt.Errorf("decode JSON pointer %q: path must start with /", pointer) } parts := strings.Split(pointer[1:], "/") for index, part := range parts { var decoded strings.Builder decoded.Grow(len(part)) for offset := 0; offset < len(part); offset++ { if part[offset] != '~' { decoded.WriteByte(part[offset]) continue } if offset+1 >= len(part) { return nil, fmt.Errorf("decode JSON pointer %q: incomplete escape in token %d", pointer, index) } offset++ switch part[offset] { case '0': decoded.WriteByte('~') case '1': decoded.WriteByte('/') default: return nil, fmt.Errorf("decode JSON pointer %q: invalid escape in token %d", pointer, index) } } parts[index] = decoded.String() } return parts, nil } func placeYAMLValue(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") } 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 := category for index, token := range path { keyIndex := mappingKeyIndex(current, token) if index == len(path)-1 { if keyIndex >= 0 { current.Content[keyIndex+1] = value } else { current.Content = append(current.Content, stringKey(token), value) } return category, nil } if keyIndex < 0 { child := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} current.Content = append(current.Content, stringKey(token), child) current = child 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])) } current = child } return category, nil } func mappingKeyIndex(mapping *yaml.Node, key string) int { 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 } } return -1 } 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) { additions := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} changes := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} deletions := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} 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 = placeYAMLValue(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 = placeYAMLValue(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)) } } 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 }