3
0

Refactor expr bits for the new path utils

This commit is contained in:
Tomaž Jerman
2024-01-30 11:28:16 +01:00
parent 6c499217ef
commit 9cb5d05c34
8 changed files with 188 additions and 173 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/PaesslerAG/gval"
@@ -94,17 +93,21 @@ var _ expr.DeepFieldAssigner = &ComposeRecord{}
//
// We need to reroute value assigning for record-value-sets because
// we loose the reference to record-value slice
func (t *ComposeRecord) AssignFieldValue(kk []string, val expr.TypedValue) error {
func (t *ComposeRecord) AssignFieldValue(p expr.Pather, val expr.TypedValue) (err error) {
t.mux.Lock()
defer t.mux.Unlock()
switch kk[0] {
switch p.Get() {
case "values":
return assignToComposeRecordValues(t.value, kk[1:], val)
err = p.Next()
if err != nil {
return
}
return assignToComposeRecordValues(t.value, p, val)
// case "labels":
// @todo deep setting labels
default:
return assignToComposeRecord(t.value, kk[0], val)
return assignToComposeRecord(t.value, p.Get(), val)
}
}
@@ -114,7 +117,6 @@ var _ gval.Selector = &ComposeRecord{}
//
// It allows gval lib to access Record's underlying value (*types.Record)
// and it's fields
//
func (t *ComposeRecord) SelectGVal(_ context.Context, k string) (interface{}, error) {
t.mux.RLock()
defer t.mux.RUnlock()
@@ -234,15 +236,14 @@ func CastToComposeRecordValues(val interface{}) (out types.RecordValueSet, err e
}
}
func (t *ComposeRecordValues) AssignFieldValue(pp []string, val expr.TypedValue) error {
return assignToComposeRecordValues(t.value, pp, val)
func (t *ComposeRecordValues) AssignFieldValue(p expr.Pather, val expr.TypedValue) error {
return assignToComposeRecordValues(t.value, p, val)
}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access Record's underlying value (*types.RecordValues)
// and it's fields
//
func (t *ComposeRecordValues) SelectGVal(_ context.Context, k string) (interface{}, error) {
return composeRecordValuesGValSelector(t.value, k)
}
@@ -347,8 +348,8 @@ func composeRecordValuesTypedValueSelector(res *types.Record, k string) (expr.Ty
// assignToRecordValuesSet is field value setter for *types.RecordValueSet
//
// We'll be using types.Record for the base (and not types.RecordValueSet)
func assignToComposeRecordValues(res *types.Record, pp []string, val interface{}) (err error) {
if len(pp) < 1 {
func assignToComposeRecordValues(res *types.Record, p expr.Pather, val interface{}) (err error) {
if !p.More() {
switch val := expr.UntypedValue(val).(type) {
case types.RecordValueSet:
res.Values = val
@@ -362,7 +363,7 @@ func assignToComposeRecordValues(res *types.Record, pp []string, val interface{}
}
var (
k = pp[0]
k = p.Get()
rv = &types.RecordValue{Name: k}
setSliceOfValues = func(vv []interface{}) error {
@@ -370,7 +371,7 @@ func assignToComposeRecordValues(res *types.Record, pp []string, val interface{}
// @todo this should use field context (when available) to determinate if we're actually
// setting array to a multi-value field
if len(pp) == 2 {
if !p.IsLast() {
// Tying to assign an array of values to a single value; that will not work
return fmt.Errorf("can not assign array of values to a single value in a record value set")
}
@@ -416,9 +417,14 @@ func assignToComposeRecordValues(res *types.Record, pp []string, val interface{}
return
}
if len(pp) == 2 {
if rv.Place, err = cast.ToUintE(expr.UntypedValue(pp[1])); err != nil {
return fmt.Errorf("failed to decode record value place from '%s': %w", strings.Join(pp, "."), err)
if !p.IsLast() {
err = p.Next()
if err != nil {
return
}
if rv.Place, err = cast.ToUintE(expr.UntypedValue(p.Get())); err != nil {
return fmt.Errorf("failed to decode record value place from '%s': %w", p.String(), err)
}
}

View File

@@ -1,94 +1,48 @@
package expr
import (
"bufio"
"fmt"
"strings"
)
const (
pathDelimiter = "."
)
var (
invalidPathErr = fmt.Errorf("invalid path format")
)
func PathSplit(path string) ([]string, error) {
out := make([]string, 0)
s := bufio.NewScanner(strings.NewReader(path))
s.Split(pathSplitter)
for s.Scan() {
// checks if two consecutive path parts are empty
if len(s.Text()) == 0 && len(out) > 0 && len(out[len(out)-1]) == 0 {
return nil, invalidPathErr
}
out = append(out, s.Text())
}
if s.Err() != nil {
return nil, s.Err()
}
return out, nil
}
func pathSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for i := 0; i < len(data); i += 1 {
switch data[i] {
case '.', '[':
return i + 1, data[start:i], nil
case ']':
// When at closing bracket but not at the end, make sure we properly split the token
if i == len(data)-1 {
return i + 1, data[start:i], nil
}
if data[i+1] != '.' && data[i+1] != '[' {
return 0, nil, invalidPathErr
}
return i + 2, data[start:i], nil
}
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
// Request more data.
return start, nil, nil
}
func PathBase(path string) string {
return strings.Split(path, ".")[0]
}
func Assign(base TypedValue, path string, val TypedValue) error {
pp, err := PathSplit(path)
func Assign(base TypedValue, path string, val TypedValue) (err error) {
pp := Path(path)
err = pp.Next()
if err != nil {
return err
return
}
if len(pp) == 0 {
if !pp.More() {
panic("setting value with empty path")
}
var (
key = pp[0]
key = ""
)
// descend lower by the path but
// stop before the last part of the path
for len(pp) > 1 {
for !pp.IsLast() {
switch s := base.(type) {
case DeepFieldAssigner:
return s.AssignFieldValue(pp, val)
case FieldSelector:
key, pp = pp[0], pp[1:]
key = pp.Get()
err = pp.Next()
if err != nil {
return
}
if base, err = s.Select(key); err != nil {
return err
}
@@ -99,7 +53,7 @@ func Assign(base TypedValue, path string, val TypedValue) error {
}
}
key = pp[0]
key = pp.Get()
// try with field setter first
// if not a FieldSetter it has to be a Selector
@@ -124,14 +78,15 @@ func Assign(base TypedValue, path string, val TypedValue) error {
}
func Select(base TypedValue, path string) (TypedValue, error) {
pp, err := PathSplit(path)
func Select(base TypedValue, path string) (out TypedValue, err error) {
pp := Path(path)
err = pp.Next()
if err != nil {
return nil, err
return
}
if len(pp) == 0 {
panic("selecting value with empty path")
if !pp.More() {
panic("setting value with empty path")
}
var (
@@ -141,17 +96,21 @@ func Select(base TypedValue, path string) (TypedValue, error) {
// descend lower by the path but
// stop before the last part of the path
for len(pp) > 0 {
for pp.More() {
s, is := base.(FieldSelector)
if !is {
return nil, failure
}
key, pp = pp[0], pp[1:]
key = pp.Get()
err = pp.Next()
if err != nil {
return
}
if base, err = s.Select(key); err != nil {
return nil, err
}
}
return base, nil

View File

@@ -6,39 +6,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestPathSplit(t *testing.T) {
tcc := []struct {
p string
r []string
err string
}{
{p: "a", r: []string{"a"}},
{p: "foo.bar", r: []string{"foo", "bar"}},
{p: "a.b[1]", r: []string{"a", "b", "1"}},
{p: "a.b[1].baz[0]", r: []string{"a", "b", "1", "baz", "0"}},
{p: "a.[]", err: invalidPathErr.Error()},
{p: "a[]", r: []string{"a", ""}},
{p: "a[1]bzz", err: invalidPathErr.Error()},
{p: "a[b][c].d[1]", r: []string{"a", "b", "c", "d", "1"}},
{p: "a.Content-Type", r: []string{"a", "Content-Type"}},
}
for _, tc := range tcc {
t.Run(tc.p, func(t *testing.T) {
req := require.New(t)
pp, err := PathSplit(tc.p)
if len(tc.err) == 0 {
req.NoError(err)
} else {
req.EqualError(err, tc.err)
}
req.Equal(tc.r, pp)
})
}
}
func TestVars(t *testing.T) {
var (
req = require.New(t)
@@ -82,3 +49,45 @@ func TestVars(t *testing.T) {
req.NoError(Assign(vars, "three.two.one.go", tv("!!!")))
req.Equal("!!!", Must(Select(vars, "three.two.one.go")).Get().(string))
}
func TestAssign(t *testing.T) {
base := &Vars{
value: map[string]TypedValue{
"a": &Vars{value: map[string]TypedValue{
"b": &Vars{value: map[string]TypedValue{
"c": &Vars{value: map[string]TypedValue{
"d": &Vars{value: map[string]TypedValue{
"e": &Vars{value: map[string]TypedValue{}},
}},
}},
}},
}},
},
}
val := Must(NewInteger(10))
err := Assign(base, "a.b.c.d.e.f", val)
require.NoError(t, err)
}
func BenchmarkAssign(b *testing.B) {
base := &Vars{
value: map[string]TypedValue{
"a": &Vars{value: map[string]TypedValue{
"b": &Vars{value: map[string]TypedValue{
"c": &Vars{value: map[string]TypedValue{
"d": &Vars{value: map[string]TypedValue{
"e": &Vars{value: map[string]TypedValue{}},
}},
}},
}},
}},
},
}
val := Must(NewInteger(10))
b.ResetTimer()
for n := 0; n < b.N; n++ {
Assign(base, "a.b.c.d.e.f", val)
}
}

View File

@@ -290,7 +290,6 @@ func (t *Array) Decode(dst reflect.Value) error {
//
// It allows gval lib to access Record's underlying value (*types.Array)
// and it's fields
//
func (t Array) SelectGVal(ctx context.Context, k string) (interface{}, error) {
if s, err := t.Select(k); err != nil {
return nil, err
@@ -616,55 +615,65 @@ func (t *KV) Delete(keys ...string) (out TypedValue, err error) {
return kv, nil
}
func (t *KVV) AssignFieldValue(key []string, val TypedValue) error {
return assignToKVV(t, key, val)
func (t *KVV) AssignFieldValue(p Pather, val TypedValue) error {
return assignToKVV(t, p, val)
}
func assignToKVV(t *KVV, kk []string, val TypedValue) error {
func assignToKVV(t *KVV, p Pather, val TypedValue) (err error) {
if t.value == nil {
t.value = make(map[string][]string)
}
switch len(kk) {
case 2:
str, err := cast.ToStringE(val.Get())
if err != nil {
return err
}
k := p.Get()
key, ind := kk[0], kk[1]
if len(ind) > 0 {
// handles kvv.field[42] = "value"
index, err := cast.ToIntE(ind)
if err != nil {
return err
}
if index >= 0 && index < len(t.value[key]) {
// handles positive & in-range indexes
t.value[key][index] = str
return nil
}
//negative & out-of-range indexes are always appended
}
// handles kvv.field[] = "value"
t.value[key] = append(t.value[key], str)
case 1:
str, err := cast.ToStringSliceE(val.Get())
if err != nil {
return err
}
t.value[kk[0]] = str
default:
return fmt.Errorf("cannot set value on %s with path '%s'", t.Type(), strings.Join(kk, "."))
err = p.Next()
if err != nil {
return
}
// Only specified the key, no index
if !p.More() {
var str []string
str, err = cast.ToStringSliceE(val.Get())
if err != nil {
return err
}
t.value[k] = str
return
}
if !p.IsLast() {
return fmt.Errorf("cannot set value on %s with path '%s'", t.Type(), p.String())
}
// Specified the key and index
str, err := cast.ToStringE(val.Get())
if err != nil {
return err
}
ind := p.Get()
if len(ind) > 0 {
// handles kvv.field[42] = "value"
index, err := cast.ToIntE(ind)
if err != nil {
return err
}
if index >= 0 && index < len(t.value[k]) {
// handles positive & in-range indexes
t.value[k][index] = str
return nil
}
//negative & out-of-range indexes are always appended
}
// handles kvv.field[] = "value"
t.value[k] = append(t.value[k], str)
return nil
}

View File

@@ -8,22 +8,39 @@ type (
start, end int
}
Pather interface {
String() string
More() bool
IsLast() bool
Get() string
Rest() string
Next() (err error)
}
)
// Path initializes a new exprPath helper to efficiently traverse the path
func Path(p string) (out exprPath) {
return exprPath{path: p}
func Path(p string) (out *exprPath) {
return &exprPath{path: p}
}
func (p exprPath) More() bool {
func (p *exprPath) String() string {
return p.path
}
func (p *exprPath) More() bool {
return p.start < len(p.path)
}
func (p exprPath) Get() string {
func (p *exprPath) IsLast() bool {
return p.isLast
}
func (p *exprPath) Get() string {
return p.path[p.start:p.end]
}
func (p exprPath) Rest() string {
func (p *exprPath) Rest() string {
var rest string
if p.end+1 < len(p.path) {
rest = p.path[p.end:]
@@ -41,9 +58,9 @@ func (p exprPath) Rest() string {
return rest
}
func (p exprPath) Next() (out exprPath, err error) {
func (p *exprPath) Next() (err error) {
if !p.More() {
return p, nil
return nil
}
if p.end > 0 {
@@ -54,12 +71,12 @@ func (p exprPath) Next() (out exprPath, err error) {
p.start, p.end, p.isLast, err = nxtRange(p.path, p.start)
if err != nil {
return p, err
return err
}
p.i++
return p, nil
return nil
}
func nxtRange(path string, start int) (startOut, end int, isLast bool, err error) {

View File

@@ -80,6 +80,16 @@ func TestPath(t *testing.T) {
expBits: []string{"a", "b"},
expRests: []string{"b", ""},
}, {
path: "a.Content-Type",
expBits: []string{"a", "Content-Type"},
expRests: []string{"Content-Type", ""},
}, {
path: "a[0]",
expBits: []string{"a", "0"},
expRests: []string{"[0]", ""},
}, {
path: "a[b][c]",
@@ -102,7 +112,7 @@ func TestPath(t *testing.T) {
for {
i++
pp, err = pp.Next()
err = pp.Next()
require.NoError(t, err)
if !pp.More() {
@@ -134,7 +144,7 @@ func BenchmarkPath(b *testing.B) {
pp := Path(path)
for {
pp, _ = pp.Next()
pp.Next()
if !pp.More() {
break
}

View File

@@ -39,7 +39,7 @@ type (
}
DeepFieldAssigner interface {
AssignFieldValue([]string, TypedValue) error
AssignFieldValue(Pather, TypedValue) error
}
Iterator interface {

View File

@@ -5,10 +5,11 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/sql"
"reflect"
"strings"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/spf13/cast"
@@ -93,6 +94,10 @@ func (t *Vars) Merge(nn ...Iterator) (out TypedValue, err error) {
return t.MustMerge(nn...), nil
}
func (t *Vars) IsEmpty() bool {
return t == nil || len(t.value) == 0
}
// MustMerge returns Vars after merging the given Vars(es) into it
func (t *Vars) MustMerge(nn ...Iterator) *Vars {
if t != nil {
@@ -103,7 +108,7 @@ func (t *Vars) MustMerge(nn ...Iterator) *Vars {
}
var (
out = &Vars{value: make(map[string]TypedValue)}
out = &Vars{value: make(map[string]TypedValue, 4)}
)
for _, i := range nn {