Add YAML pkg with Patch function
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Encode(v any, indent int) ([]byte, error) {
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
e := yaml.NewEncoder(b)
|
||||||
|
e.SetIndent(indent)
|
||||||
|
|
||||||
|
if err := e.Encode(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch - change key/value pair in YAML file without break formatting
|
||||||
|
func Patch(src []byte, key string, value any, path ...string) ([]byte, error) {
|
||||||
|
nodeParent, err := FindParent(src, path...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dst []byte
|
||||||
|
|
||||||
|
if nodeParent != nil {
|
||||||
|
dst, err = AddOrReplace(src, key, value, nodeParent)
|
||||||
|
} else {
|
||||||
|
dst, err = AddToEnd(src, key, value, path...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = yaml.Unmarshal(dst, map[string]any{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindParent - return YAML Node from path of keys (tree)
|
||||||
|
func FindParent(src []byte, path ...string) (*yaml.Node, error) {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var root yaml.Node
|
||||||
|
if err := yaml.Unmarshal(src, &root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.Content == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := root.Content[0] // yaml.DocumentNode
|
||||||
|
for _, name := range path {
|
||||||
|
if parent == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, parent = FindChild(parent, name)
|
||||||
|
}
|
||||||
|
return parent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindChild - search and return YAML key/value pair for current Node
|
||||||
|
func FindChild(node *yaml.Node, name string) (key, value *yaml.Node) {
|
||||||
|
for i, child := range node.Content {
|
||||||
|
if child.Value != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return child, node.Content[i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FirstChild(node *yaml.Node) *yaml.Node {
|
||||||
|
if node.Content == nil {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return node.Content[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func LastChild(node *yaml.Node) *yaml.Node {
|
||||||
|
if node.Content == nil {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return node.Content[len(node.Content)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]byte, error) {
|
||||||
|
v := map[string]any{key: value}
|
||||||
|
put, err := Encode(v, 2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodeKey, nodeValue := FindChild(nodeParent, key); nodeKey != nil {
|
||||||
|
put = AddIndent(put, nodeKey.Column-1)
|
||||||
|
|
||||||
|
i0 := LineOffset(src, nodeKey.Line)
|
||||||
|
i1 := LineOffset(src, LastChild(nodeValue).Line+1)
|
||||||
|
|
||||||
|
dst := make([]byte, 0, len(src)+len(put))
|
||||||
|
dst = append(dst, src[:i0]...)
|
||||||
|
if value != nil {
|
||||||
|
dst = append(dst, put...)
|
||||||
|
}
|
||||||
|
return append(dst, src[i1:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
put = AddIndent(put, FirstChild(nodeParent).Column-1)
|
||||||
|
|
||||||
|
i := LineOffset(src, LastChild(nodeParent).Line+1)
|
||||||
|
|
||||||
|
dst := make([]byte, 0, len(src)+len(put))
|
||||||
|
dst = append(dst, src[:i]...)
|
||||||
|
if value != nil {
|
||||||
|
dst = append(dst, put...)
|
||||||
|
}
|
||||||
|
return append(dst, src[i:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddToEnd(src []byte, key string, value any, path ...string) ([]byte, error) {
|
||||||
|
if len(path) > 1 || value == nil {
|
||||||
|
return nil, errors.New("config: path not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := map[string]map[string]any{
|
||||||
|
path[0]: {key: value},
|
||||||
|
}
|
||||||
|
put, err := Encode(v, 2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := make([]byte, 0, len(src)+len(put)+10)
|
||||||
|
dst = append(dst, src...)
|
||||||
|
if l := len(src); l > 0 && src[l-1] != '\n' {
|
||||||
|
dst = append(dst, '\n')
|
||||||
|
}
|
||||||
|
return append(dst, put...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPrefix(src, pre []byte) (dst []byte) {
|
||||||
|
for len(src) > 0 {
|
||||||
|
dst = append(dst, pre...)
|
||||||
|
i := bytes.IndexByte(src, '\n') + 1
|
||||||
|
if i == 0 {
|
||||||
|
dst = append(dst, src...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dst = append(dst, src[:i]...)
|
||||||
|
src = src[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddIndent(src []byte, indent int) (dst []byte) {
|
||||||
|
pre := make([]byte, indent)
|
||||||
|
for i := 0; i < indent; i++ {
|
||||||
|
pre[i] = ' '
|
||||||
|
}
|
||||||
|
return AddPrefix(src, pre)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LineOffset(b []byte, line int) (offset int) {
|
||||||
|
for l := 1; ; l++ {
|
||||||
|
if l == line {
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
i := bytes.IndexByte(b[offset:], '\n') + 1
|
||||||
|
if i == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPatch(t *testing.T) {
|
||||||
|
b := []byte(`# prefix`)
|
||||||
|
|
||||||
|
b, err := Patch(b, "camera1", "url1", "streams")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `# prefix
|
||||||
|
streams:
|
||||||
|
camera1: url1
|
||||||
|
`, string(b))
|
||||||
|
|
||||||
|
b, err = Patch(b, "camera2", []string{"url2", "url3"}, "streams")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `# prefix
|
||||||
|
streams:
|
||||||
|
camera1: url1
|
||||||
|
camera2:
|
||||||
|
- url2
|
||||||
|
- url3
|
||||||
|
`, string(b))
|
||||||
|
|
||||||
|
b, err = Patch(b, "camera1", "url4", "streams")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `# prefix
|
||||||
|
streams:
|
||||||
|
camera1: url4
|
||||||
|
camera2:
|
||||||
|
- url2
|
||||||
|
- url3
|
||||||
|
`, string(b))
|
||||||
|
|
||||||
|
b, err = Patch(b, "camera2", "url5", "streams")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `# prefix
|
||||||
|
streams:
|
||||||
|
camera1: url4
|
||||||
|
camera2: url5
|
||||||
|
`, string(b))
|
||||||
|
|
||||||
|
b, err = Patch(b, "camera1", nil, "streams")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `# prefix
|
||||||
|
streams:
|
||||||
|
camera2: url5
|
||||||
|
`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchParings(t *testing.T) {
|
||||||
|
b := []byte(`homekit:
|
||||||
|
camera1:
|
||||||
|
pin: 123-45-678
|
||||||
|
streams:
|
||||||
|
camera1: url1
|
||||||
|
`)
|
||||||
|
|
||||||
|
pairings := map[string]string{
|
||||||
|
"client1": "public1",
|
||||||
|
"client2": "public2",
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := Patch(b, "pairings", pairings, "homekit", "camera1")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, `homekit:
|
||||||
|
camera1:
|
||||||
|
pin: 123-45-678
|
||||||
|
pairings:
|
||||||
|
client1: public1
|
||||||
|
client2: public2
|
||||||
|
streams:
|
||||||
|
camera1: url1
|
||||||
|
`, string(b))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user