Refactor expr bits for the new path utils
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type (
|
||||
}
|
||||
|
||||
DeepFieldAssigner interface {
|
||||
AssignFieldValue([]string, TypedValue) error
|
||||
AssignFieldValue(Pather, TypedValue) error
|
||||
}
|
||||
|
||||
Iterator interface {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user