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
|
||||
}
|
||||
|
||||
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.
|
||||
type Path []pathComponent
|
||||
|
||||
|
|
|
@ -134,3 +134,28 @@ func (v Value) MarkAnchor() Value {
|
|||
func (v Value) IsAnchor() bool {
|
||||
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