Files
dockwatch/internal/composeedit/composeedit.go
T
jbergner 45ca18b74e
release-tag / release-image (push) Failing after 1m20s
init
2026-08-31 17:09:21 +02:00

216 lines
4.8 KiB
Go

package composeedit
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type ParseResult struct {
Value any `json:"value"`
}
type Patch struct {
Path []string `json:"path"`
Value any `json:"value"`
Delete bool `json:"delete"`
}
func Parse(src string) (ParseResult, error) {
var doc yaml.Node
if err := yaml.Unmarshal([]byte(src), &doc); err != nil {
return ParseResult{}, fmt.Errorf("yaml: %w", err)
}
if len(doc.Content) == 0 {
return ParseResult{Value: map[string]any{}}, nil
}
var v any
if err := doc.Content[0].Decode(&v); err != nil {
return ParseResult{}, err
}
v = normalize(v)
return ParseResult{Value: v}, nil
}
func Apply(src string, p Patch) (string, error) {
if len(p.Path) == 0 {
return "", errors.New("path is required")
}
var doc yaml.Node
if err := yaml.Unmarshal([]byte(src), &doc); err != nil {
return "", fmt.Errorf("yaml: %w", err)
}
if len(doc.Content) == 0 {
return "", errors.New("empty yaml document")
}
root := doc.Content[0]
parent, last, err := walkParent(root, p.Path, !p.Delete)
if err != nil {
return "", err
}
if p.Delete {
if err := deleteChild(parent, last); err != nil {
return "", err
}
} else {
n, err := valueNode(p.Value)
if err != nil {
return "", err
}
if err := setChild(parent, last, n); err != nil {
return "", err
}
}
var b bytes.Buffer
enc := yaml.NewEncoder(&b)
enc.SetIndent(2)
if err := enc.Encode(&doc); err != nil {
return "", err
}
_ = enc.Close()
return strings.TrimSuffix(b.String(), "\n") + "\n", nil
}
func normalize(v any) any {
switch x := v.(type) {
case map[string]any:
out := make(map[string]any, len(x))
for k, v := range x {
out[k] = normalize(v)
}
return out
case map[any]any:
out := map[string]any{}
for k, v := range x {
out[fmt.Sprint(k)] = normalize(v)
}
return out
case []any:
out := make([]any, len(x))
for i, v := range x {
out[i] = normalize(v)
}
return out
default:
return x
}
}
func walkParent(root *yaml.Node, path []string, create bool) (*yaml.Node, string, error) {
cur := root
for _, seg := range path[:len(path)-1] {
if cur.Kind == yaml.DocumentNode && len(cur.Content) > 0 {
cur = cur.Content[0]
}
switch cur.Kind {
case yaml.MappingNode:
n := mapGet(cur, seg)
if n == nil {
if !create {
return nil, "", fmt.Errorf("path %q not found", seg)
}
n = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
mapSet(cur, seg, n)
}
cur = n
case yaml.SequenceNode:
i, err := strconv.Atoi(seg)
if err != nil || i < 0 || i >= len(cur.Content) {
return nil, "", fmt.Errorf("invalid array index %q", seg)
}
cur = cur.Content[i]
default:
return nil, "", fmt.Errorf("cannot descend through scalar at %q", seg)
}
}
return cur, path[len(path)-1], nil
}
func mapGet(m *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(m.Content); i += 2 {
if m.Content[i].Value == key {
return m.Content[i+1]
}
}
return nil
}
func mapSet(m *yaml.Node, key string, v *yaml.Node) {
for i := 0; i+1 < len(m.Content); i += 2 {
if m.Content[i].Value == key {
old := m.Content[i+1]
v.HeadComment = old.HeadComment
v.LineComment = old.LineComment
v.FootComment = old.FootComment
m.Content[i+1] = v
return
}
}
m.Content = append(m.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, v)
}
func setChild(p *yaml.Node, key string, v *yaml.Node) error {
switch p.Kind {
case yaml.MappingNode:
mapSet(p, key, v)
return nil
case yaml.SequenceNode:
if key == "-" {
p.Content = append(p.Content, v)
return nil
}
i, err := strconv.Atoi(key)
if err != nil || i < 0 || i > len(p.Content) {
return fmt.Errorf("invalid array index %q", key)
}
if i == len(p.Content) {
p.Content = append(p.Content, v)
} else {
old := p.Content[i]
v.HeadComment, v.LineComment, v.FootComment = old.HeadComment, old.LineComment, old.FootComment
p.Content[i] = v
}
return nil
default:
return errors.New("parent is not a map or array")
}
}
func deleteChild(p *yaml.Node, key string) error {
switch p.Kind {
case yaml.MappingNode:
for i := 0; i+1 < len(p.Content); i += 2 {
if p.Content[i].Value == key {
p.Content = append(p.Content[:i], p.Content[i+2:]...)
return nil
}
}
return nil
case yaml.SequenceNode:
i, err := strconv.Atoi(key)
if err != nil || i < 0 || i >= len(p.Content) {
return fmt.Errorf("invalid array index %q", key)
}
p.Content = append(p.Content[:i], p.Content[i+1:]...)
return nil
default:
return errors.New("parent is not a map or array")
}
}
func valueNode(v any) (*yaml.Node, error) {
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
var x any
if err := json.Unmarshal(raw, &x); err != nil {
return nil, err
}
var n yaml.Node
if err := n.Encode(x); err != nil {
return nil, err
}
return &n, nil
}