forked from akuity/kargo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(yaml): enrich YAML modification utilities
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
- Loading branch information
Showing
6 changed files
with
823 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
Oops, something went wrong.