Skip to content

Commit

Permalink
feat(yaml): enrich YAML modification utilities
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
  • Loading branch information
hiddeco committed Sep 3, 2024
1 parent f86af5d commit 483e68e
Show file tree
Hide file tree
Showing 6 changed files with 823 additions and 2 deletions.
58 changes: 58 additions & 0 deletions internal/yaml/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package yaml

import (
"fmt"
"strings"

"sigs.k8s.io/yaml/goyaml.v3"
)

const pathSeparator = "."

// DecodeField retrieves the value at the specified path in the YAML document
// and decodes it into the provided value. The path is specified using a
// dot-separated string, similar to the UpdateField function.
func DecodeField(node *yaml.Node, path string, out any) error {
parts := strings.Split(path, pathSeparator)
targetNode, err := findNode(node, parts)
if err != nil {
return err
}
return targetNode.Decode(out)
}

// findNode traverses the YAML structure to find the node at the specified path.
func findNode(node *yaml.Node, parts []string) (*yaml.Node, error) {
if len(parts) == 0 {
return node, nil
}

currentPart := parts[0]
remainingParts := parts[1:]

switch node.Kind {
case yaml.DocumentNode:
return findNode(node.Content[0], parts)
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
if node.Content[i].Value == currentPart {
return findNode(node.Content[i+1], remainingParts)
}
}
return nil, fmt.Errorf("field '%s' not found", currentPart)
case yaml.SequenceNode:
index, err := parseIndex(currentPart)
if err != nil {
return nil, err
}
if index < 0 || index >= len(node.Content) {
return nil, fmt.Errorf("index out of range: %d", index)
}
return findNode(node.Content[index], remainingParts)
default:
if len(parts) > 0 {
return nil, fmt.Errorf("cannot access nested field on scalar node")
}
return node, nil
}
}
198 changes: 198 additions & 0 deletions internal/yaml/decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package yaml

import (
"testing"

"github.com/stretchr/testify/assert"
"sigs.k8s.io/yaml/goyaml.v3"
)

func TestDecodeField(t *testing.T) {
tests := []struct {
name string
yaml string
path string
assertions func(*testing.T, any, error)
}{
{
name: "simple string",
yaml: "key: value",
path: "key",
assertions: func(t *testing.T, result any, err error) {
assert.NoError(t, err)
assert.Equal(t, "value", result)
},
},
{
name: "nested map",
yaml: "outer:\n inner: nested value",
path: "outer.inner",
assertions: func(t *testing.T, result any, err error) {
assert.NoError(t, err)
assert.Equal(t, "nested value", result)
},
},
{
name: "array access",
yaml: "array:\n - item1\n - item2",
path: "array.[1]",
assertions: func(t *testing.T, result any, err error) {
assert.NoError(t, err)
assert.Equal(t, "item2", result)
},
},
{
name: "complex nested structure",
yaml: `
root:
nested:
array:
- key: value1
- key: value2
map:
key1: val1
key2: val2
`,
path: "root.nested.array.[1].key",
assertions: func(t *testing.T, result any, err error) {
assert.NoError(t, err)
assert.Equal(t, "value2", result)
},
},
{
name: "non-existent field",
yaml: "key: value",
path: "nonexistent",
assertions: func(t *testing.T, result any, err error) {
assert.EqualError(t, err, "field 'nonexistent' not found")
},
},
{
name: "invalid array index",
yaml: "array:\n - item1\n - item2",
path: "array.[2]",
assertions: func(t *testing.T, result any, err error) {
assert.EqualError(t, err, "index out of range: 2")
},
},
{
name: "access nested field on scalar",
yaml: "key: value",
path: "key.nested",
assertions: func(t *testing.T, result any, err error) {
assert.EqualError(t, err, "cannot access nested field on scalar node")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var node yaml.Node
err := yaml.Unmarshal([]byte(tt.yaml), &node)
assert.NoError(t, err, "Failed to unmarshal YAML")

var result any
err = DecodeField(&node, tt.path, &result)

tt.assertions(t, result, err)
})
}
}

func Test_findNode(t *testing.T) {
tests := []struct {
name string
yaml string
path []string
assertions func(*testing.T, *yaml.Node, error)
}{
{
name: "root document node",
yaml: "key: value",
path: []string{"key"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "value", result.Value)
},
},
{
name: "nested mapping",
yaml: "outer:\n inner: nested value",
path: []string{"outer", "inner"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "nested value", result.Value)
},
},
{
name: "sequence access",
yaml: "array:\n - item1\n - item2",
path: []string{"array", "[1]"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "item2", result.Value)
},
},
{
name: "empty path",
yaml: "key: value",
path: []string{},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, yaml.DocumentNode, result.Kind)
},
},
{
name: "non-existent field",
yaml: "key: value",
path: []string{"nonexistent"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.EqualError(t, err, "field 'nonexistent' not found")
assert.Nil(t, result)
},
},
{
name: "invalid sequence index",
yaml: "array:\n - item1\n - item2",
path: []string{"array", "invalid"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid index format")
assert.Nil(t, result)
},
},
{
name: "out of range sequence index",
yaml: "array:\n - item1\n - item2",
path: []string{"array", "[5]"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.EqualError(t, err, "index out of range: 5")
assert.Nil(t, result)
},
},
{
name: "access nested field on scalar",
yaml: "key: value",
path: []string{"key", "nested"},
assertions: func(t *testing.T, result *yaml.Node, err error) {
assert.EqualError(t, err, "cannot access nested field on scalar node")
assert.Nil(t, result)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var node yaml.Node
err := yaml.Unmarshal([]byte(tt.yaml), &node)
assert.NoError(t, err)

result, err := findNode(&node, tt.path)
tt.assertions(t, result, err)
})
}
}
Loading

0 comments on commit 483e68e

Please sign in to comment.