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.
2024-01-24 18:38:46 +00:00
|
|
|
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.
|
2024-03-07 13:56:50 +00:00
|
|
|
fn func(Path, Value) (Value, error)
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
2024-03-08 14:33:01 +00:00
|
|
|
func visit(v Value, prefix Path, suffix Pattern, opts visitOptions) (Value, error) {
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
if len(suffix) == 0 {
|
2024-03-07 13:56:50 +00:00
|
|
|
return opts.fn(prefix, v)
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
suffix = suffix[1:]
|
|
|
|
|
2024-03-08 14:33:01 +00:00
|
|
|
// Visit the value with the current component.
|
|
|
|
return component.visit(v, prefix, suffix, opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (component pathComponent) visit(v Value, prefix Path, suffix Pattern, opts visitOptions) (Value, error) {
|
|
|
|
path := prefix.Append(component)
|
|
|
|
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
switch {
|
|
|
|
case component.isKey():
|
|
|
|
// Expect a map to be set if this is a key.
|
|
|
|
m, ok := v.AsMap()
|
|
|
|
if !ok {
|
2024-03-08 14:33:01 +00:00
|
|
|
return InvalidValue, fmt.Errorf("expected a map to index %q, found %s", path, v.Kind())
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup current value in the map.
|
|
|
|
ev, ok := m[component.key]
|
2024-03-07 14:13:04 +00:00
|
|
|
if !ok {
|
2024-03-08 14:33:01 +00:00
|
|
|
return InvalidValue, noSuchKeyError{path}
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Recursively transform the value.
|
2024-03-08 14:33:01 +00:00
|
|
|
nv, err := visit(ev, path, suffix, opts)
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
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 {
|
2024-03-08 14:33:01 +00:00
|
|
|
return InvalidValue, fmt.Errorf("expected a sequence to index %q, found %s", path, v.Kind())
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup current value in the sequence.
|
|
|
|
if component.index < 0 || component.index >= len(s) {
|
2024-03-08 14:33:01 +00:00
|
|
|
return InvalidValue, indexOutOfBoundsError{path}
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Recursively transform the value.
|
|
|
|
ev := s[component.index]
|
2024-03-08 14:33:01 +00:00
|
|
|
nv, err := visit(ev, path, suffix, opts)
|
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.
2024-01-24 18:38:46 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|