Inline logic to set a value in `dyn.SetByPath` (#1261)

## Changes

This removes the need for the `allowMissingKeyInMap` option to the
private `visit` function and ensures that the body of the visit function
doesn't add or remove values of the configuration it traverses.

This in turn prepares for visiting a path pattern that yields more than
one callback, which doesn't match well with the now-removed option.

## Tests

Unit tests pass and fully cover the inlined code.
This commit is contained in:
Pieter Noordhuis 2024-03-07 15:13:04 +01:00 committed by GitHub
parent c05c0cd941
commit 16a4c711e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 10 deletions

View File

@ -45,10 +45,6 @@ type visitOptions struct {
// If this function returns an error, the original visit function call
// returns this error and the value is left unmodified.
fn func(Path, 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) {
@ -76,7 +72,7 @@ func visit(v Value, prefix, suffix Path, opts visitOptions) (Value, error) {
// Lookup current value in the map.
ev, ok := m[component.key]
if !ok && !opts.allowMissingKeyInMap {
if !ok {
return InvalidValue, noSuchKeyError{prefix}
}

View File

@ -1,5 +1,11 @@
package dyn
import (
"fmt"
"maps"
"slices"
)
// 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) {
@ -14,11 +20,59 @@ func Set(v Value, path string, nv Value) (Value, error) {
// 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(_ Path, _ Value) (Value, error) {
// Return the incoming value to set it.
lp := len(p)
if lp == 0 {
return nv, nil
}
parent := p[:lp-1]
component := p[lp-1]
return visit(v, EmptyPath, parent, visitOptions{
fn: func(prefix Path, v Value) (Value, error) {
path := prefix.Append(component)
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", path, v.Kind())
}
// 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", path, v.Kind())
}
// Lookup current value in the sequence.
if component.index < 0 || component.index >= len(s) {
return InvalidValue, indexOutOfBoundsError{prefix}
}
// 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")
}
},
allowMissingKeyInMap: true,
})
}