mirror of https://github.com/databricks/cli.git
Add functionality to visit values in `dyn.Value` tree (#1142)
## Changes This change adds the following functions: * `dyn.Get(value, "foo.bar") -> (dyn.Value, error)` * `dyn.Set(value, "foo.bar", newValue) -> (dyn.Value, error)` * `dyn.Map(value, "foo.bar", func) -> (dyn.Value, error)` And equivalent functions that take a previously constructed `dyn.Path`: * `dyn.GetByPath(value, dyn.Path) -> (dyn.Value, error)` * `dyn.SetByPath(value, dyn.Path, newValue) -> (dyn.Value, error)` * `dyn.MapByPath(value, dyn.Path, func) -> (dyn.Value, error)` Changes made by the "set" and "map" functions are never reflected in the input argument; they return new `dyn.Value` instances for all nodes in the path leading up to the changed value. ## Tests New unit tests cover all critical paths.
This commit is contained in:
parent
cf2a1c38ba
commit
ff6e0354b9
|
@ -10,6 +10,14 @@ type pathComponent struct {
|
||||||
index int
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c pathComponent) isKey() bool {
|
||||||
|
return c.key != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c pathComponent) isIndex() bool {
|
||||||
|
return c.key == ""
|
||||||
|
}
|
||||||
|
|
||||||
// Path represents a path to a value in a [Value] configuration tree.
|
// Path represents a path to a value in a [Value] configuration tree.
|
||||||
type Path []pathComponent
|
type Path []pathComponent
|
||||||
|
|
||||||
|
|
|
@ -134,3 +134,28 @@ func (v Value) MarkAnchor() Value {
|
||||||
func (v Value) IsAnchor() bool {
|
func (v Value) IsAnchor() bool {
|
||||||
return v.anchor
|
return v.anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eq is an internal only method that compares two values.
|
||||||
|
// It is used to determine if a value has changed during a visit.
|
||||||
|
// We need a custom implementation because maps and slices
|
||||||
|
// cannot be compared with the regular == operator.
|
||||||
|
func (v Value) eq(w Value) bool {
|
||||||
|
if v.k != w.k || v.l != w.l {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.k {
|
||||||
|
case KindMap:
|
||||||
|
// Compare pointers to the underlying map.
|
||||||
|
// This is safe because we don't allow maps to be mutated.
|
||||||
|
return &v.v == &w.v
|
||||||
|
case KindSequence:
|
||||||
|
// Compare pointers to the underlying slice and slice length.
|
||||||
|
// This is safe because we don't allow slices to be mutated.
|
||||||
|
vs := v.v.([]Value)
|
||||||
|
ws := w.v.([]Value)
|
||||||
|
return &vs[0] == &ws[0] && len(vs) == len(ws)
|
||||||
|
default:
|
||||||
|
return v.v == w.v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
package dyn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noSuchKeyError struct {
|
||||||
|
p Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e noSuchKeyError) Error() string {
|
||||||
|
return fmt.Sprintf("key not found at %q", e.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNoSuchKeyError(err error) bool {
|
||||||
|
var target noSuchKeyError
|
||||||
|
return errors.As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexOutOfBoundsError struct {
|
||||||
|
p Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e indexOutOfBoundsError) Error() string {
|
||||||
|
return fmt.Sprintf("index out of bounds at %q", e.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsIndexOutOfBoundsError(err error) bool {
|
||||||
|
var target indexOutOfBoundsError
|
||||||
|
return errors.As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
type visitOptions struct {
|
||||||
|
// The function to apply to the value once found.
|
||||||
|
//
|
||||||
|
// If this function returns the same value as it receives as argument,
|
||||||
|
// the original visit function call returns the original value unmodified.
|
||||||
|
//
|
||||||
|
// If this function returns a new value, the original visit function call
|
||||||
|
// returns a value with all the intermediate values updated.
|
||||||
|
//
|
||||||
|
// If this function returns an error, the original visit function call
|
||||||
|
// returns this error and the value is left unmodified.
|
||||||
|
fn func(Value) (Value, error)
|
||||||
|
|
||||||
|
// If set, tolerate the absence of the last component in the path.
|
||||||
|
// This option is needed to set a key in a map that is not yet present.
|
||||||
|
allowMissingKeyInMap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func visit(v Value, prefix, suffix Path, opts visitOptions) (Value, error) {
|
||||||
|
if len(suffix) == 0 {
|
||||||
|
return opts.fn(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize prefix if it is empty.
|
||||||
|
// It is pre-allocated to its maximum size to avoid additional allocations.
|
||||||
|
if len(prefix) == 0 {
|
||||||
|
prefix = make(Path, 0, len(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
component := suffix[0]
|
||||||
|
prefix = prefix.Append(component)
|
||||||
|
suffix = suffix[1:]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case component.isKey():
|
||||||
|
// Expect a map to be set if this is a key.
|
||||||
|
m, ok := v.AsMap()
|
||||||
|
if !ok {
|
||||||
|
return InvalidValue, fmt.Errorf("expected a map to index %q, found %s", prefix, v.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup current value in the map.
|
||||||
|
ev, ok := m[component.key]
|
||||||
|
if !ok && !opts.allowMissingKeyInMap {
|
||||||
|
return InvalidValue, noSuchKeyError{prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively transform the value.
|
||||||
|
nv, err := visit(ev, prefix, suffix, opts)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original value if the value hasn't changed.
|
||||||
|
if nv.eq(ev) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an updated map value.
|
||||||
|
m = maps.Clone(m)
|
||||||
|
m[component.key] = nv
|
||||||
|
return Value{
|
||||||
|
v: m,
|
||||||
|
k: KindMap,
|
||||||
|
l: v.l,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case component.isIndex():
|
||||||
|
// Expect a sequence to be set if this is an index.
|
||||||
|
s, ok := v.AsSequence()
|
||||||
|
if !ok {
|
||||||
|
return InvalidValue, fmt.Errorf("expected a sequence to index %q, found %s", prefix, v.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup current value in the sequence.
|
||||||
|
if component.index < 0 || component.index >= len(s) {
|
||||||
|
return InvalidValue, indexOutOfBoundsError{prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively transform the value.
|
||||||
|
ev := s[component.index]
|
||||||
|
nv, err := visit(ev, prefix, suffix, opts)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original value if the value hasn't changed.
|
||||||
|
if nv.eq(ev) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an updated sequence value.
|
||||||
|
s = slices.Clone(s)
|
||||||
|
s[component.index] = nv
|
||||||
|
return Value{
|
||||||
|
v: s,
|
||||||
|
k: KindSequence,
|
||||||
|
l: v.l,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid component")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dyn
|
||||||
|
|
||||||
|
// Get returns the value inside the specified value at the specified path.
|
||||||
|
// It is identical to [GetByPath], except that it takes a string path instead of a [Path].
|
||||||
|
func Get(v Value, path string) (Value, error) {
|
||||||
|
p, err := NewPathFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
return GetByPath(v, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByPath returns the value inside the specified value at the specified path.
|
||||||
|
// If the path doesn't exist, it returns InvalidValue and an error.
|
||||||
|
func GetByPath(v Value, p Path) (Value, error) {
|
||||||
|
out := InvalidValue
|
||||||
|
_, err := visit(v, EmptyPath, p, visitOptions{
|
||||||
|
fn: func(ev Value) (Value, error) {
|
||||||
|
// Capture the value argument to return it.
|
||||||
|
out = ev
|
||||||
|
return ev, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return out, err
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package dyn_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetWithEmptyPath(t *testing.T) {
|
||||||
|
// An empty path means to return the value itself.
|
||||||
|
vin := dyn.V(42)
|
||||||
|
vout, err := dyn.GetByPath(vin, dyn.NewPath())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOnNilValue(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
_, err = dyn.GetByPath(dyn.NilValue, dyn.NewPath(dyn.Key("foo")))
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found nil`)
|
||||||
|
_, err = dyn.GetByPath(dyn.NilValue, dyn.NewPath(dyn.Index(42)))
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOnMap(t *testing.T) {
|
||||||
|
vin := dyn.V(map[string]dyn.Value{
|
||||||
|
"foo": dyn.V(42),
|
||||||
|
"bar": dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.GetByPath(vin, dyn.NewPath(dyn.Index(42)))
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found map`)
|
||||||
|
|
||||||
|
_, err = dyn.GetByPath(vin, dyn.NewPath(dyn.Key("baz")))
|
||||||
|
assert.True(t, dyn.IsNoSuchKeyError(err))
|
||||||
|
assert.ErrorContains(t, err, `key not found at "baz"`)
|
||||||
|
|
||||||
|
vfoo, err := dyn.GetByPath(vin, dyn.NewPath(dyn.Key("foo")))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, dyn.V(42), vfoo)
|
||||||
|
|
||||||
|
vbar, err := dyn.GetByPath(vin, dyn.NewPath(dyn.Key("bar")))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, dyn.V(43), vbar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOnSequence(t *testing.T) {
|
||||||
|
vin := dyn.V([]dyn.Value{
|
||||||
|
dyn.V(42),
|
||||||
|
dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.GetByPath(vin, dyn.NewPath(dyn.Key("foo")))
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found sequence`)
|
||||||
|
|
||||||
|
_, err = dyn.GetByPath(vin, dyn.NewPath(dyn.Index(-1)))
|
||||||
|
assert.True(t, dyn.IsIndexOutOfBoundsError(err))
|
||||||
|
assert.ErrorContains(t, err, `index out of bounds at "[-1]"`)
|
||||||
|
|
||||||
|
_, err = dyn.GetByPath(vin, dyn.NewPath(dyn.Index(2)))
|
||||||
|
assert.True(t, dyn.IsIndexOutOfBoundsError(err))
|
||||||
|
assert.ErrorContains(t, err, `index out of bounds at "[2]"`)
|
||||||
|
|
||||||
|
v0, err := dyn.GetByPath(vin, dyn.NewPath(dyn.Index(0)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, dyn.V(42), v0)
|
||||||
|
|
||||||
|
v1, err := dyn.GetByPath(vin, dyn.NewPath(dyn.Index(1)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, dyn.V(43), v1)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package dyn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MapFunc is a function that maps a value to another value.
|
||||||
|
type MapFunc func(Value) (Value, error)
|
||||||
|
|
||||||
|
// Foreach returns a [MapFunc] that applies the specified [MapFunc] to each
|
||||||
|
// value in a map or sequence and returns the new map or sequence.
|
||||||
|
func Foreach(fn MapFunc) MapFunc {
|
||||||
|
return func(v Value) (Value, error) {
|
||||||
|
switch v.Kind() {
|
||||||
|
case KindMap:
|
||||||
|
m := maps.Clone(v.MustMap())
|
||||||
|
for key, value := range m {
|
||||||
|
var err error
|
||||||
|
m[key], err = fn(value)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewValue(m, v.Location()), nil
|
||||||
|
case KindSequence:
|
||||||
|
s := slices.Clone(v.MustSequence())
|
||||||
|
for i, value := range s {
|
||||||
|
var err error
|
||||||
|
s[i], err = fn(value)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewValue(s, v.Location()), nil
|
||||||
|
default:
|
||||||
|
return InvalidValue, fmt.Errorf("expected a map or sequence, found %s", v.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies the given function to the value at the specified path in the specified value.
|
||||||
|
// It is identical to [MapByPath], except that it takes a string path instead of a [Path].
|
||||||
|
func Map(v Value, path string, fn MapFunc) (Value, error) {
|
||||||
|
p, err := NewPathFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
return MapByPath(v, p, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies the given function to the value at the specified path in the specified value.
|
||||||
|
// If successful, it returns the new value with all intermediate values copied and updated.
|
||||||
|
//
|
||||||
|
// If the path contains a key that doesn't exist, or an index that is out of bounds,
|
||||||
|
// it returns the original value and no error. This is because setting a value at a path
|
||||||
|
// that doesn't exist is a no-op.
|
||||||
|
//
|
||||||
|
// If the path is invalid for the given value, it returns InvalidValue and an error.
|
||||||
|
func MapByPath(v Value, p Path, fn MapFunc) (Value, error) {
|
||||||
|
nv, err := visit(v, EmptyPath, p, visitOptions{
|
||||||
|
fn: fn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check for success.
|
||||||
|
if err == nil {
|
||||||
|
return nv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original value if a key or index is missing.
|
||||||
|
if IsNoSuchKeyError(err) || IsIndexOutOfBoundsError(err) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nv, err
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
package dyn_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapWithEmptyPath(t *testing.T) {
|
||||||
|
// An empty path means to return the value itself.
|
||||||
|
vin := dyn.V(42)
|
||||||
|
vout, err := dyn.MapByPath(dyn.InvalidValue, dyn.EmptyPath, func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return vin, nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapOnNilValue(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
_, err = dyn.MapByPath(dyn.NilValue, dyn.NewPath(dyn.Key("foo")), nil)
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found nil`)
|
||||||
|
_, err = dyn.MapByPath(dyn.NilValue, dyn.NewPath(dyn.Index(42)), nil)
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapFuncOnMap(t *testing.T) {
|
||||||
|
vin := dyn.V(map[string]dyn.Value{
|
||||||
|
"foo": dyn.V(42),
|
||||||
|
"bar": dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.MapByPath(vin, dyn.NewPath(dyn.Index(42)), nil)
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found map`)
|
||||||
|
|
||||||
|
// A key that does not exist is not an error.
|
||||||
|
vout, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("baz")), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
|
||||||
|
// Note: in the test cases below we implicitly test that the original
|
||||||
|
// value is not modified as we repeatedly set values on it.
|
||||||
|
vfoo, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("foo")), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
assert.Equal(t, dyn.V(42), v)
|
||||||
|
return dyn.V(44), nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 44,
|
||||||
|
"bar": 43,
|
||||||
|
}, vfoo.AsAny())
|
||||||
|
|
||||||
|
vbar, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("bar")), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
assert.Equal(t, dyn.V(43), v)
|
||||||
|
return dyn.V(45), nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 42,
|
||||||
|
"bar": 45,
|
||||||
|
}, vbar.AsAny())
|
||||||
|
|
||||||
|
// Return error from map function.
|
||||||
|
var ref = fmt.Errorf("error")
|
||||||
|
verr, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("foo")), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return dyn.InvalidValue, ref
|
||||||
|
})
|
||||||
|
assert.Equal(t, dyn.InvalidValue, verr)
|
||||||
|
assert.ErrorIs(t, err, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapFuncOnSequence(t *testing.T) {
|
||||||
|
vin := dyn.V([]dyn.Value{
|
||||||
|
dyn.V(42),
|
||||||
|
dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.MapByPath(vin, dyn.NewPath(dyn.Key("foo")), nil)
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found sequence`)
|
||||||
|
|
||||||
|
// An index that does not exist is not an error.
|
||||||
|
vout, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(2)), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
|
||||||
|
// Note: in the test cases below we implicitly test that the original
|
||||||
|
// value is not modified as we repeatedly set values on it.
|
||||||
|
v0, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(0)), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
assert.Equal(t, dyn.V(42), v)
|
||||||
|
return dyn.V(44), nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{44, 43}, v0.AsAny())
|
||||||
|
|
||||||
|
v1, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(1)), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
assert.Equal(t, dyn.V(43), v)
|
||||||
|
return dyn.V(45), nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{42, 45}, v1.AsAny())
|
||||||
|
|
||||||
|
// Return error from map function.
|
||||||
|
var ref = fmt.Errorf("error")
|
||||||
|
verr, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(0)), func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return dyn.InvalidValue, ref
|
||||||
|
})
|
||||||
|
assert.Equal(t, dyn.InvalidValue, verr)
|
||||||
|
assert.ErrorIs(t, err, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapForeachOnMap(t *testing.T) {
|
||||||
|
vin := dyn.V(map[string]dyn.Value{
|
||||||
|
"foo": dyn.V(42),
|
||||||
|
"bar": dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Run foreach, adding 1 to each of the elements.
|
||||||
|
vout, err := dyn.Map(vin, ".", dyn.Foreach(func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
i, ok := v.AsInt()
|
||||||
|
require.True(t, ok, "expected an integer")
|
||||||
|
return dyn.V(int(i) + 1), nil
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 43,
|
||||||
|
"bar": 44,
|
||||||
|
}, vout.AsAny())
|
||||||
|
|
||||||
|
// Check that the original has not been modified.
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 42,
|
||||||
|
"bar": 43,
|
||||||
|
}, vin.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapForeachOnMapError(t *testing.T) {
|
||||||
|
vin := dyn.V(map[string]dyn.Value{
|
||||||
|
"foo": dyn.V(42),
|
||||||
|
"bar": dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that an error from the map function propagates.
|
||||||
|
var ref = fmt.Errorf("error")
|
||||||
|
_, err := dyn.Map(vin, ".", dyn.Foreach(func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return dyn.InvalidValue, ref
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, err, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapForeachOnSequence(t *testing.T) {
|
||||||
|
vin := dyn.V([]dyn.Value{
|
||||||
|
dyn.V(42),
|
||||||
|
dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Run foreach, adding 1 to each of the elements.
|
||||||
|
vout, err := dyn.Map(vin, ".", dyn.Foreach(func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
i, ok := v.AsInt()
|
||||||
|
require.True(t, ok, "expected an integer")
|
||||||
|
return dyn.V(int(i) + 1), nil
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{43, 44}, vout.AsAny())
|
||||||
|
|
||||||
|
// Check that the original has not been modified.
|
||||||
|
assert.Equal(t, []any{42, 43}, vin.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapForeachOnSequenceError(t *testing.T) {
|
||||||
|
vin := dyn.V([]dyn.Value{
|
||||||
|
dyn.V(42),
|
||||||
|
dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that an error from the map function propagates.
|
||||||
|
var ref = fmt.Errorf("error")
|
||||||
|
_, err := dyn.Map(vin, ".", dyn.Foreach(func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return dyn.InvalidValue, ref
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, err, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapForeachOnOtherError(t *testing.T) {
|
||||||
|
vin := dyn.V(42)
|
||||||
|
|
||||||
|
// Check that if foreach is applied to something other than a map or a sequence, it returns an error.
|
||||||
|
_, err := dyn.Map(vin, ".", dyn.Foreach(func(v dyn.Value) (dyn.Value, error) {
|
||||||
|
return dyn.InvalidValue, nil
|
||||||
|
}))
|
||||||
|
assert.ErrorContains(t, err, "expected a map or sequence, found int")
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dyn
|
||||||
|
|
||||||
|
// Set assigns a new value at the specified path in the specified value.
|
||||||
|
// It is identical to [SetByPath], except that it takes a string path instead of a [Path].
|
||||||
|
func Set(v Value, path string, nv Value) (Value, error) {
|
||||||
|
p, err := NewPathFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidValue, err
|
||||||
|
}
|
||||||
|
return SetByPath(v, p, nv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetByPath assigns the given value at the specified path in the specified value.
|
||||||
|
// If successful, it returns the new value with all intermediate values copied and updated.
|
||||||
|
// If the path doesn't exist, it returns InvalidValue and an error.
|
||||||
|
func SetByPath(v Value, p Path, nv Value) (Value, error) {
|
||||||
|
return visit(v, EmptyPath, p, visitOptions{
|
||||||
|
fn: func(_ Value) (Value, error) {
|
||||||
|
// Return the incoming value to set it.
|
||||||
|
return nv, nil
|
||||||
|
},
|
||||||
|
allowMissingKeyInMap: true,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package dyn_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetWithEmptyPath(t *testing.T) {
|
||||||
|
// An empty path means to return the value itself.
|
||||||
|
vin := dyn.V(42)
|
||||||
|
vout, err := dyn.SetByPath(dyn.InvalidValue, dyn.EmptyPath, vin)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetOnNilValue(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
_, err = dyn.SetByPath(dyn.NilValue, dyn.NewPath(dyn.Key("foo")), dyn.V(42))
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found nil`)
|
||||||
|
_, err = dyn.SetByPath(dyn.NilValue, dyn.NewPath(dyn.Index(42)), dyn.V(42))
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetOnMap(t *testing.T) {
|
||||||
|
vin := dyn.V(map[string]dyn.Value{
|
||||||
|
"foo": dyn.V(42),
|
||||||
|
"bar": dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.SetByPath(vin, dyn.NewPath(dyn.Index(42)), dyn.V(42))
|
||||||
|
assert.ErrorContains(t, err, `expected a sequence to index "[42]", found map`)
|
||||||
|
|
||||||
|
// Note: in the test cases below we implicitly test that the original
|
||||||
|
// value is not modified as we repeatedly set values on it.
|
||||||
|
|
||||||
|
vfoo, err := dyn.SetByPath(vin, dyn.NewPath(dyn.Key("foo")), dyn.V(44))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 44,
|
||||||
|
"bar": 43,
|
||||||
|
}, vfoo.AsAny())
|
||||||
|
|
||||||
|
vbar, err := dyn.SetByPath(vin, dyn.NewPath(dyn.Key("bar")), dyn.V(45))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 42,
|
||||||
|
"bar": 45,
|
||||||
|
}, vbar.AsAny())
|
||||||
|
|
||||||
|
vbaz, err := dyn.SetByPath(vin, dyn.NewPath(dyn.Key("baz")), dyn.V(46))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": 42,
|
||||||
|
"bar": 43,
|
||||||
|
"baz": 46,
|
||||||
|
}, vbaz.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetOnSequence(t *testing.T) {
|
||||||
|
vin := dyn.V([]dyn.Value{
|
||||||
|
dyn.V(42),
|
||||||
|
dyn.V(43),
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = dyn.SetByPath(vin, dyn.NewPath(dyn.Key("foo")), dyn.V(42))
|
||||||
|
assert.ErrorContains(t, err, `expected a map to index "foo", found sequence`)
|
||||||
|
|
||||||
|
// It is not allowed to set a value at an index that is out of bounds.
|
||||||
|
_, err = dyn.SetByPath(vin, dyn.NewPath(dyn.Index(-1)), dyn.V(42))
|
||||||
|
assert.True(t, dyn.IsIndexOutOfBoundsError(err))
|
||||||
|
_, err = dyn.SetByPath(vin, dyn.NewPath(dyn.Index(2)), dyn.V(42))
|
||||||
|
assert.True(t, dyn.IsIndexOutOfBoundsError(err))
|
||||||
|
|
||||||
|
// Note: in the test cases below we implicitly test that the original
|
||||||
|
// value is not modified as we repeatedly set values on it.
|
||||||
|
|
||||||
|
v0, err := dyn.SetByPath(vin, dyn.NewPath(dyn.Index(0)), dyn.V(44))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{44, 43}, v0.AsAny())
|
||||||
|
|
||||||
|
v1, err := dyn.SetByPath(vin, dyn.NewPath(dyn.Index(1)), dyn.V(45))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{42, 45}, v1.AsAny())
|
||||||
|
}
|
Loading…
Reference in New Issue