3
0

Improvments on expressions, gw tests

This commit is contained in:
Denis Arh 2021-03-30 14:39:00 +02:00
parent 723c361171
commit 597c18fd16
8 changed files with 319 additions and 161 deletions

View File

@ -388,13 +388,6 @@ func (t *ComposeRecord) Assign(val interface{}) error {
}
}
// Select is field accessor for *types.Record
//
// Similar to SelectGVal but returns typed values
func (t ComposeRecord) Select(k string) (TypedValue, error) {
return composeRecordTypedValueSelector(t.value, k)
}
func (t ComposeRecord) Has(k string) bool {
switch k {
case "ID", "recordID":
@ -576,41 +569,3 @@ func (t *ComposeRecordValueErrorSet) Assign(val interface{}) error {
return nil
}
}
// ComposeRecordValues is an expression type, wrapper for types.RecordValueSet type
type ComposeRecordValues struct{ value types.RecordValueSet }
// NewComposeRecordValues creates new instance of ComposeRecordValues expression type
func NewComposeRecordValues(val interface{}) (*ComposeRecordValues, error) {
if c, err := CastToComposeRecordValues(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeRecordValues: %w", err)
} else {
return &ComposeRecordValues{value: c}, nil
}
}
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) Get() interface{} { return t.value }
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) GetValue() types.RecordValueSet { return t.value }
// Return type name
func (ComposeRecordValues) Type() string { return "ComposeRecordValues" }
// Convert value to types.RecordValueSet
func (ComposeRecordValues) Cast(val interface{}) (TypedValue, error) {
return NewComposeRecordValues(val)
}
// Assign new value to ComposeRecordValues
//
// value is first passed through CastToComposeRecordValues
func (t *ComposeRecordValues) Assign(val interface{}) error {
if c, err := CastToComposeRecordValues(val); err != nil {
return err
} else {
t.value = c
return nil
}
}

View File

@ -97,7 +97,7 @@ var _ expr.DeepFieldAssigner = &ComposeRecord{}
func (t *ComposeRecord) AssignFieldValue(kk []string, val expr.TypedValue) error {
switch kk[0] {
case "values":
return assignToComposeRecordValues(&t.value.Values, kk[1:], val)
return assignToComposeRecordValues(t.value, kk[1:], val)
// @todo deep setting labels
default:
return assignToComposeRecord(t.value, kk[0], val)
@ -123,6 +123,23 @@ func (t ComposeRecord) SelectGVal(ctx context.Context, k string) (interface{}, e
return composeRecordGValSelector(t.value, k)
}
// Select is field accessor for *types.ComposeRecord
//
// Similar to SelectGVal but returns typed values
func (t ComposeRecord) Select(k string) (expr.TypedValue, error) {
if k == "values" {
if t.value.Values == nil {
t.value.Values = types.RecordValueSet{}
}
return &ComposeRecordValues{value: t.value}, nil
}
return composeRecordTypedValueSelector(t.value, k)
}
type ComposeRecordValues struct{ value *types.Record }
func CastToComposeRecordValues(val interface{}) (out types.RecordValueSet, err error) {
out = types.RecordValueSet{}
switch val := val.(type) {
@ -174,7 +191,7 @@ 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)
return assignToComposeRecordValues(t.value, pp, val)
}
// SelectGVal implements gval.Selector requirements
@ -182,34 +199,60 @@ func (t *ComposeRecordValues) AssignFieldValue(pp []string, val expr.TypedValue)
// It allows gval lib to access Record's underlying value (*types.RecordValues)
// and it's fields
//
func (t ComposeRecordValues) SelectGVal(ctx context.Context, k string) (interface{}, error) {
func (t *ComposeRecordValues) SelectGVal(ctx context.Context, k string) (interface{}, error) {
return composeRecordValuesGValSelector(t.value, k)
}
// Select is field accessor for *types.Record
//
// Similar to SelectGVal but returns typed values
func (t ComposeRecordValues) Select(k string) (expr.TypedValue, error) {
func (t *ComposeRecordValues) Select(k string) (expr.TypedValue, error) {
return composeRecordValuesTypedValueSelector(t.value, k)
}
func (t ComposeRecordValues) Has(k string) bool {
return t.value.Get(k, 0) != nil
return t.value.Values.Get(k, 0) != nil
}
// recordGValSelector is field accessor for *types.RecordValueSet
func composeRecordValuesGValSelector(res types.RecordValueSet, k string) (interface{}, error) {
vv := res.FilterByName(k)
func composeRecordValuesGValSelector(res *types.Record, k string) (interface{}, error) {
var (
vv = res.Values.FilterByName(k)
multiValueField bool
field *types.ModuleField
)
if mod := res.GetModule(); mod != nil {
fld := mod.Fields.FindByName(k)
if fld == nil {
return nil, fmt.Errorf("field '%s' does not exist on module %s", k, mod.Name)
}
multiValueField = fld.Multi
}
switch {
case len(vv) == 0:
if field != nil && field.IsBoolean() {
return false, nil
}
switch len(vv) {
case 0:
return nil, nil
case 1:
return vv[0].Value, nil
case len(vv) == 1 && !multiValueField:
return recordValueCast(field, vv[0])
default:
out := make([]string, 0, len(vv))
out := make([]interface{}, 0, len(vv))
return out, vv.Walk(func(v *types.RecordValue) error {
out = append(out, v.Value)
i, err := recordValueCast(field, v)
if err != nil {
return err
}
out = append(out, i)
return nil
})
}
@ -218,34 +261,49 @@ func composeRecordValuesGValSelector(res types.RecordValueSet, k string) (interf
// recordValuesTypedValueSelector is field accessor for *types.RecordValueSet
//
// @todo return appropriate types (atm all values are returned as String)
func composeRecordValuesTypedValueSelector(res types.RecordValueSet, k string) (expr.TypedValue, error) {
vv := res.FilterByName(k)
func composeRecordValuesTypedValueSelector(res *types.Record, k string) (expr.TypedValue, error) {
var (
vv = res.Values.FilterByName(k)
multiValueField bool
field *types.ModuleField
)
if mod := res.GetModule(); mod != nil {
field = mod.Fields.FindByName(k)
if field == nil {
return nil, fmt.Errorf("field '%s' does not exist on module %s", k, mod.Name)
}
multiValueField = field.Multi
}
switch {
case len(vv) == 0:
return nil, nil
case len(vv) == 1:
return expr.NewString(vv[0].Value)
default:
mval := make([]expr.TypedValue, 0, len(vv))
_ = vv.Walk(func(v *types.RecordValue) error {
mval = append(mval, expr.Must(expr.NewString(v.Value)))
return nil
})
if field != nil && field.IsBoolean() {
return &expr.Boolean{}, nil
}
return expr.NewArray(mval)
return nil, nil
case len(vv) == 1 && !multiValueField:
return recordValueToExprTypedValue(field, vv[0])
default:
return recordValueSetoToExprArray(field, vv...)
}
}
// assignToRecordValuesSet is field value setter for *types.Record
func assignToComposeRecordValues(res *types.RecordValueSet, pp []string, val interface{}) (err error) {
// 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 {
switch val := expr.UntypedValue(val).(type) {
case types.RecordValueSet:
*res = val
res.Values = val
return
case *types.Record:
*res = val.Values
*res = *val
return
}
@ -273,7 +331,7 @@ func assignToComposeRecordValues(res *types.RecordValueSet, pp []string, val int
return err
}
*res = res.Set(rv)
res.Values = res.Values.Set(rv)
}
return nil
@ -317,11 +375,51 @@ func assignToComposeRecordValues(res *types.RecordValueSet, pp []string, val int
}
}
*res = res.Set(rv)
res.Values = res.Values.Set(rv)
return nil
}
// NewComposeRecordValues creates new instance of ComposeRecordValues expression type
func NewComposeRecordValues(val interface{}) (*ComposeRecordValues, error) {
// Try to cast to ComposeRecord first
if rec, err := CastToComposeRecord(val); err == nil {
return &ComposeRecordValues{value: rec}, nil
}
if c, err := CastToComposeRecordValues(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeRecordValues: %w", err)
} else {
return &ComposeRecordValues{value: &types.Record{Values: c}}, nil
}
}
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) Get() interface{} { return t.value }
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) GetValue() types.RecordValueSet { return t.value.Values }
// Return type name
func (ComposeRecordValues) Type() string { return "ComposeRecordValues" }
// Convert value to types.RecordValueSet
func (ComposeRecordValues) Cast(val interface{}) (expr.TypedValue, error) {
return NewComposeRecordValues(val)
}
// Assign new value to ComposeRecordValues
//
// value is first passed through CastToComposeRecordValues
func (t *ComposeRecordValues) Assign(val interface{}) error {
if c, err := CastToComposeRecordValues(val); err != nil {
return err
} else {
t.value.Values = c
return nil
}
}
func CastToComposeRecordValueErrorSet(val interface{}) (out *types.RecordValueErrorSet, err error) {
switch val := expr.UntypedValue(val).(type) {
case *types.RecordValueErrorSet:
@ -330,3 +428,80 @@ func CastToComposeRecordValueErrorSet(val interface{}) (out *types.RecordValueEr
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func recordValueCast(field *types.ModuleField, rv *types.RecordValue) (interface{}, error) {
if field == nil {
// safe fallback to string
return rv.Value, nil
}
switch {
case field.IsRef():
return rv.Ref, nil
case field.IsDateTime():
return cast.ToTimeE(rv.Value)
case field.IsBoolean():
return cast.ToBoolE(rv.Value)
case field.IsNumeric():
if field.Options.Precision() == 0 {
return cast.ToInt64E(rv.Value)
}
return cast.ToFloat64E(rv.Value)
default:
return rv.Value, nil
}
}
func recordValueToExprTypedValue(field *types.ModuleField, rv *types.RecordValue) (expr.TypedValue, error) {
if field == nil {
// safe fallback to string
return expr.NewString(rv.Value)
}
switch {
case field.IsRef():
return expr.NewID(rv.Ref)
case field.IsDateTime():
return expr.NewDateTime(rv.Value)
case field.IsBoolean():
return expr.NewBoolean(rv.Value)
case field.IsNumeric():
if field.Options.Precision() == 0 {
return expr.NewInteger(rv.Value)
}
return expr.NewFloat(rv.Value)
default:
return expr.NewString(rv.Value)
}
}
func recordValueSetoToExprArray(field *types.ModuleField, vv ...*types.RecordValue) (arr *expr.Array, err error) {
var (
tv expr.TypedValue
)
arr = &expr.Array{}
for _, v := range vv {
tv, err = recordValueToExprTypedValue(field, v)
if err != nil {
return
}
arr.Push(tv)
}
return
}

View File

@ -40,11 +40,13 @@ types:
- { name: 'updatedBy', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedBy', exprType: 'ID', goType: 'uint64', mode: ro }
customGValSelector: true
customSelector: true
customGValSelector: true
customFieldAssigner: true
ComposeRecordValues:
as: 'types.RecordValueSet'
customType: true
as: 'types.RecordValueSet'
ComposeRecordValueErrorSet:
as: '*types.RecordValueErrorSet'

View File

@ -13,13 +13,13 @@ func TestSetRecordValuesWithPath(t *testing.T) {
var (
r = require.New(t)
rvs = &ComposeRecordValues{types.RecordValueSet{}}
rvs = &ComposeRecordValues{&types.Record{}}
)
r.NoError(expr.Assign(rvs, "field1", expr.Must(expr.NewString("a"))))
r.NoError(expr.Assign(rvs, "field1.1", expr.Must(expr.NewString("a"))))
r.True(rvs.value.Has("field1", 0))
r.True(rvs.value.Has("field1", 1))
r.True(rvs.value.Values.Has("field1", 0))
r.True(rvs.value.Values.Has("field1", 1))
})
t.Run("cast string map", func(t *testing.T) {
@ -56,14 +56,17 @@ func TestRecordFieldValuesAccess(t *testing.T) {
&types.ModuleField{Name: "m1", Multi: true},
&types.ModuleField{Name: "m2", Multi: true},
&types.ModuleField{Name: "s2", Multi: false},
&types.ModuleField{Name: "b0", Multi: false, Kind: "Bool"},
&types.ModuleField{Name: "b1", Multi: false, Kind: "Bool"},
}}
raw = &types.Record{Values: types.RecordValueSet{
&types.RecordValue{Name: "s1", Value: "sVal1"},
&types.RecordValue{Name: "m1", Value: "mVal1.0"},
&types.RecordValue{Name: "m1", Value: "mVal1.0", Place: 0},
&types.RecordValue{Name: "m1", Value: "mVal1.1", Place: 1},
&types.RecordValue{Name: "m1", Value: "mVal1.2", Place: 2},
&types.RecordValue{Name: "m2", Value: "mVal2.0"},
&types.RecordValue{Name: "m2", Value: "mVal2.0", Place: 0},
&types.RecordValue{Name: "b1", Value: "1", Place: 0},
}}
tval = &ComposeRecord{value: raw}
@ -74,61 +77,70 @@ func TestRecordFieldValuesAccess(t *testing.T) {
raw.SetModule(mod)
t.Run("via typed value", func(t *testing.T) {
var (
req = require.New(t)
)
tcc := []struct {
expects interface{}
path string
}{
{"sVal1", "rec.values.s1"},
{"mVal1.0", "rec.values.m1.0"},
{"mVal1.1", "rec.values.m1.1"},
{"mVal2.0", "rec.values.m2.0"},
// expecting valid value (false) even when boolean fields are not set
{false, "rec.values.b0"},
{true, "rec.values.b1"},
}
v, err = expr.Select(scope, "rec.values.s1")
req.NoError(err)
req.NotEmpty(v)
req.Equal("sVal1", v.Get())
for _, tc := range tcc {
t.Run(tc.path, func(t *testing.T) {
var (
req = require.New(t)
)
v, err = expr.Select(scope, "rec.values.m1.0")
req.NoError(err)
req.NotEmpty(v)
req.Equal("mVal1.0", v.Get())
v, err = expr.Select(scope, "rec.values.m1.1")
req.NoError(err)
req.NotEmpty(v)
req.Equal("mVal1.1", v.Get())
// @todo when RecordValueSet supports back-ref to record,
// we can employ better field access:
// - no error on missing values when field exists
// - proper handling of multi-value field values
// - proper value-types that corelate to field types
//v, err = expr.Select(scope, "rec.values.m2.0")
//req.NoError(err)
//req.NotEmpty(v)
//req.Equal("mVal2.0", v.Get())
v, err = expr.Select(scope, tc.path)
req.NoError(err)
req.Equal(tc.expects, v.Get())
})
}
})
t.Run("via gval selector", func(t *testing.T) {
var (
req = require.New(t)
parser = expr.NewParser()
)
tcc := []struct {
test bool
expr string
}{
{true, `rec.values.s1 == "sVal1"`},
{false, `rec.values.s1 == "sVal2"`},
{true, `rec.values.s1`},
{false, `rec.values.s2`},
{true, `rec.values.s1 != "foo"`},
{true, `rec.values.s2 != "foo"`},
{true, `!rec.values.s2`},
{true, `rec.values.s2 != "foo"`},
{false, `rec.values.s2 == "foo"`},
{true, `rec.values.m1[0] == "mVal1.0"`},
{true, `rec.values.m1[1] == "mVal1.1"`},
{true, `rec.values.m2[0] == "mVal2.0"`},
{true, `!rec.values.b0`},
{false, `rec.values.b0`},
{true, `rec.values.b1`},
{false, `!rec.values.b1`},
}
eval, err := parser.Parse(`rec.values.s1 == "sVal1"`)
req.NoError(err)
req.True(eval.Test(context.Background(), scope))
for _, tc := range tcc {
var (
req = require.New(t)
parser = expr.NewParser()
)
eval, err = parser.Parse(`rec.values.s1 != "foo"`)
req.NoError(err)
req.True(eval.Test(context.Background(), scope))
t.Run(tc.expr, func(t *testing.T) {
eval, err := parser.Parse(tc.expr)
req.NoError(err)
eval, err = parser.Parse(`rec.values.m1[0] == "mVal1.0"`)
req.NoError(err)
req.True(eval.Test(context.Background(), scope))
eval, err = parser.Parse(`rec.values.m1[1] == "mVal1.1"`)
req.NoError(err)
req.True(eval.Test(context.Background(), scope))
eval, err = parser.Parse(`rec.values.m2[0] == "mVal2.0"`)
req.NoError(err)
req.True(eval.Test(context.Background(), scope))
test, err := eval.Test(context.Background(), scope)
req.NoError(err)
req.Equal(tc.test, test)
})
}
})
}
@ -136,63 +148,63 @@ func TestAssignToComposeRecordValues(t *testing.T) {
t.Run("assign simple", func(t *testing.T) {
var (
req = require.New(t)
target = types.RecordValueSet{}
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(&target, []string{"a"}, "b"))
req.Len(target, 1)
req.True(target.Has("a", 0))
req.NoError(assignToComposeRecordValues(&target, []string{"a", "1"}, "b"))
req.Len(target, 2)
req.True(target.Has("a", 0))
req.True(target.Has("a", 1))
req.NoError(assignToComposeRecordValues(target, []string{"a"}, "b"))
req.Len(target.Values, 1)
req.True(target.Values.Has("a", 0))
req.NoError(assignToComposeRecordValues(target, []string{"a", "1"}, "b"))
req.Len(target.Values, 2)
req.True(target.Values.Has("a", 0))
req.True(target.Values.Has("a", 1))
})
t.Run("assign rvs", func(t *testing.T) {
var (
req = require.New(t)
target = types.RecordValueSet{}
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(&target, nil, types.RecordValueSet{{}}))
req.Len(target, 1)
req.NoError(assignToComposeRecordValues(target, nil, types.RecordValueSet{{}}))
req.Len(target.Values, 1)
})
t.Run("assign record", func(t *testing.T) {
var (
req = require.New(t)
target = types.RecordValueSet{}
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(&target, nil, &types.Record{Values: types.RecordValueSet{{}}}))
req.Len(target, 1)
req.NoError(assignToComposeRecordValues(target, nil, &types.Record{Values: types.RecordValueSet{{}}}))
req.Len(target.Values, 1)
})
t.Run("overwrite rvs", func(t *testing.T) {
var (
req = require.New(t)
target = types.RecordValueSet{{Name: "a"}}
target = &types.Record{Values: types.RecordValueSet{{Name: "a"}}}
)
req.NoError(assignToComposeRecordValues(&target, nil, types.RecordValueSet{{Name: "b"}}))
req.Len(target, 1)
req.False(target.Has("a", 0))
req.True(target.Has("b", 0))
req.NoError(assignToComposeRecordValues(target, nil, types.RecordValueSet{{Name: "b"}}))
req.Len(target.Values, 1)
req.False(target.Values.Has("a", 0))
req.True(target.Values.Has("b", 0))
})
t.Run("assign multiple values", func(t *testing.T) {
var (
req = require.New(t)
target = types.RecordValueSet{}
target = &types.Record{Values: types.RecordValueSet{}}
)
req.Error(assignToComposeRecordValues(&target, []string{"a", "2"}, expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target, 0)
req.Error(assignToComposeRecordValues(target, []string{"a", "2"}, expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target.Values, 0)
req.NoError(assignToComposeRecordValues(&target, []string{"a"}, expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target, 2)
req.NoError(assignToComposeRecordValues(target, []string{"a"}, expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target.Values, 2)
req.NoError(assignToComposeRecordValues(&target, []string{"a"}, expr.Must(expr.NewAny([]string{"1", "2"}))))
req.Len(target, 2)
req.NoError(assignToComposeRecordValues(target, []string{"a"}, expr.Must(expr.NewAny([]string{"1", "2"}))))
req.Len(target.Values, 2)
})
}

View File

@ -23,6 +23,7 @@ var _ = context.Background
var _ = fmt.Errorf
{{ range $exprType, $def := .Types }}
{{ if not $def.CustomType }}
// {{ $exprType }} is an expression type, wrapper for {{ $def.As }} type
type {{ $exprType }} struct{ value {{ $def.As }} }
@ -81,12 +82,14 @@ func (t {{ $exprType }}) SelectGVal(ctx context.Context, k string) (interface{},
}
{{ end }}
{{ if not $def.CustomSelector }}
// Select is field accessor for {{ $def.As }}
//
// Similar to SelectGVal but returns typed values
func (t {{ $exprType }}) Select(k string) (TypedValue, error) {
return {{ unexport $exprType "TypedValueSelector" }}(t.value, k)
}
{{ end }}
func (t {{ $exprType }}) Has(k string) bool {
switch k {
@ -150,7 +153,7 @@ func {{ $def.AssignerFn }}(res {{ $def.As }}, k string, val interface{}) (error)
return fmt.Errorf("unknown field '%s'", k)
}
{{ end }}
{{ end }}
{{ end }}
{{ end }} {{/* if $def.Struct */}}
{{ end }} {{/* if not $def.CustomType */}}
{{ end }} {{/* types loop */}}

View File

@ -28,7 +28,9 @@ type (
AssignerFn string `yaml:"assignerFn"`
BuiltInCastFn bool
BuiltInAssignerFn bool
CustomType bool `yaml:"customType"`
CustomGValSelector bool `yaml:"customGValSelector"`
CustomSelector bool `yaml:"customSelector"`
CustomFieldAssigner bool `yaml:"customFieldAssigner"`
Struct []*exprTypeStructDef

View File

@ -213,6 +213,11 @@ func (t Array) Has(k string) bool {
}
}
// Push appends value to array
func (t *Array) Push(v TypedValue) {
t.value = append(t.value, v)
}
// Select is field accessor for *types.Array
//
// Similar to SelectGVal but returns typed values

View File

@ -3,7 +3,6 @@ package expr
import (
"context"
"fmt"
"github.com/PaesslerAG/gval"
)
@ -71,7 +70,12 @@ func (e *gvalEval) Eval(ctx context.Context, scope *Vars) (interface{}, error) {
}
func (e *gvalEval) Test(ctx context.Context, scope *Vars) (bool, error) {
return e.evaluable.EvalBool(ctx, scope.Dict())
r, err := e.evaluable(ctx, scope.Dict())
if err != nil {
return false, err
}
return !isEmpty(r), nil
}
func Parser(ll ...gval.Language) gval.Language {