216 lines
4.8 KiB
Go
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
|
|
}
|