snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

147
server/pkg/expr/expr.go Normal file
View File

@@ -0,0 +1,147 @@
package expr
import (
"fmt"
"strings"
)
var (
invalidPathErr = fmt.Errorf("invalid path format")
)
func PathBase(path string) string {
return strings.Split(path, ".")[0]
}
func Assign(base TypedValue, path string, val TypedValue) (err error) {
if _, ok := base.(FlatStore); ok {
return assignFlat(base, path, val)
}
return assignPath(base, path, val)
}
func assignFlat(base TypedValue, key string, val TypedValue) error {
switch fa := base.(type) {
case FieldAssigner:
return fa.AssignFieldValue(key, val)
case DeepFieldAssigner:
return fa.AssignFieldValue(&flatPath{key: key}, val)
default:
return fmt.Errorf("cannot set on unexpected type: %T", base)
}
}
// flatPath is a single-segment Pather for flat key assignment — no path splitting.
type flatPath struct {
key string
done bool
}
func (p *flatPath) String() string { return p.key }
func (p *flatPath) More() bool { return !p.done }
func (p *flatPath) IsLast() bool { return true }
func (p *flatPath) Get() string { return p.key }
func (p *flatPath) Rest() string { return "" }
func (p *flatPath) Next() error { p.done = true; return nil }
func assignPath(base TypedValue, path string, val TypedValue) (err error) {
pp := Path(path)
err = pp.Next()
if err != nil {
return
}
if !pp.More() {
panic("setting value with empty path")
}
var (
key = ""
)
// descend lower by the path but
// stop before the last part of the path
for !pp.IsLast() {
switch s := base.(type) {
case DeepFieldAssigner:
return s.AssignFieldValue(pp, val)
case FieldSelector:
key = pp.Get()
err = pp.Next()
if err != nil {
return
}
if base, err = s.Select(key); err != nil {
return err
}
default:
return fmt.Errorf("cannot set value on %s with path '%s'", base.Type(), path)
}
}
key = pp.Get()
// try with field setter first
// if not a FieldSetter it has to be a Selector
// that returns TypedValue that we can set
switch setter := base.(type) {
case DeepFieldAssigner:
return setter.AssignFieldValue(pp, val)
case FieldAssigner:
return setter.AssignFieldValue(key, val)
case FieldSelector:
if base, err = setter.Select(key); err != nil {
return err
}
return base.Assign(val)
default:
return fmt.Errorf("%T does not support value assigning with '%s'", base, path)
}
}
func Select(base TypedValue, path string) (out TypedValue, err error) {
pp := Path(path)
err = pp.Next()
if err != nil {
return
}
if !pp.More() {
panic("setting value with empty path")
}
var (
failure = fmt.Errorf("cannot get value from %s with path '%s'", base.Type(), path)
key string
)
// descend lower by the path but
// stop before the last part of the path
for pp.More() {
s, is := base.(FieldSelector)
if !is {
return nil, failure
}
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

@@ -0,0 +1,93 @@
package expr
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestVars(t *testing.T) {
var (
req = require.New(t)
vars = Must(Typify(map[string]interface{}{
"int": Must(NewInteger(42)),
"sub": map[string]interface{}{
"foo": Must(NewString("foo")),
},
"three": map[string]interface{}{
"two": map[string]interface{}{
"one": map[string]interface{}{
"go": Must(NewString("!")),
},
},
},
}))
tv = func(in interface{}) TypedValue {
switch cnv := in.(type) {
case int:
return Must(NewInteger(cnv))
case string:
return Must(NewString(cnv))
}
return Must(NewAny(in))
}
)
req.NoError(Assign(vars, "int", tv(123)))
req.Equal(int64(123), Must(Select(vars, "int")).Get().(int64))
req.NoError(Assign(vars, "sub.foo", tv("bar")))
req.Equal("bar", Must(Select(vars, "sub.foo")).Get().(string))
req.NoError(Assign(vars, "kv", &KV{}))
req.NoError(Assign(vars, "kv.foo", tv("bar")))
req.Equal("bar", Must(Select(vars, "kv.foo")).Get().(string))
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

@@ -0,0 +1,56 @@
package expr
import (
"fmt"
)
func compareToBoolean(a Boolean, b TypedValue) (int, error) {
c, err := NewBoolean(b)
if err != nil {
return 0, fmt.Errorf("cannot compare %s and %s: %s", a.Type(), b.Type(), err.Error())
}
switch {
case a.value == c.value:
return 0, nil
case !a.value && c.value:
return -1, nil
default:
return 1, nil
}
}
func compareToDateTime(a DateTime, b TypedValue) (int, error) {
c, err := NewDateTime(b)
if err != nil {
return 0, fmt.Errorf("cannot compare %s and %s: %s", a.Type(), b.Type(), err.Error())
}
ar := a.value.Nanosecond()
cr := c.value.Nanosecond()
switch {
case ar == cr:
return 0, nil
case ar < cr:
return -1, nil
default:
return 1, nil
}
}
func compareToDuration(a Duration, b TypedValue) (int, error) {
c, err := NewDuration(b)
if err != nil {
return 0, fmt.Errorf("cannot compare %s and %s: %s", a.Type(), b.Type(), err.Error())
}
switch {
case a.value == c.value:
return 0, nil
case a.value < c.value:
return -1, nil
default:
return 1, nil
}
}

1281
server/pkg/expr/expr_types.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
package: expr
imports:
- io
- time
- github.com/cortezaproject/corteza/server/pkg/http
- net/url
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
types:
Any:
as: 'interface{}'
Array:
as: '[]TypedValue'
Vars:
as: 'map[string]TypedValue'
Boolean:
as: 'bool'
default: 'false'
comparable: true
customComparator: true
ID:
as: 'uint64'
default: '0'
comparable: true
Integer:
as: 'int64'
default: '0'
comparable: true
UnsignedInteger:
as: 'uint64'
default: '0'
comparable: true
Float:
as: 'float64'
default: '0'
comparable: true
String:
as: 'string'
default: '""'
comparable: true
Bytes:
as: '[]byte'
default: 'nil'
comparable: false
Handle:
as: 'string'
default: '""'
comparable: true
DateTime:
as: '*time.Time'
comparable: true
customComparator: true
Duration:
as: 'time.Duration'
default: '0'
comparable: true
customComparator: true
KV:
as: 'map[string]string'
assignFn: assignToKV
KVV:
as: 'map[string][]string'
assignFn: assignToKVV
LabelValue:
as: 'map[string]labelTypes.LabelValue'
assignFn: assignToLabelValue
Meta:
as: 'map[string]any'
Reader:
as: 'io.Reader'
HttpRequest:
as: '*http.Request'
struct:
- { name: 'Method', exprType: 'String', goType: 'string' }
- { name: 'URL', exprType: 'Url', goType: 'url.Url' }
- { name: 'Header', exprType: 'KVV', goType: 'map[string][]string' }
- { name: 'Body', exprType: 'Reader', goType: '*types.BufferedReader' }
- { name: 'Form', exprType: 'KVV', goType: 'map[string][]string' }
- { name: 'PostForm', exprType: 'KVV', goType: 'map[string][]string' }
Url:
as: '*url.URL'

View File

@@ -0,0 +1,734 @@
package expr
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func Example_set_kv() {
var (
kv = &KV{}
p = map[string]interface{}{
"kv": kv,
"key": "k1",
"value": Must(NewString("v11")),
}
)
eval(`set(kv, key, value)`, p)
fmt.Printf("\nOriginal KV should be %v", kv.value)
// output:
// map[k1:v11]
// Original KV should be map[]
}
func Example_merge_kv() {
var (
kv = &KV{}
p = map[string]interface{}{
"kv": kv,
"foo": &KV{value: map[string]string{
"k1": "v1",
}},
"bar": &KV{value: map[string]string{
"k2": "v2",
}},
}
)
eval(`merge(kv, foo, bar)`, p)
fmt.Printf("\nOriginal KV should be %v", kv.value)
// output:
// map[k1:v1 k2:v2]
// Original KV should be map[]
}
func Example_filter_kv() {
var (
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
p = map[string]interface{}{
"kv": kv,
"key1": "k1",
"key2": "k3",
}
)
eval(`filter(kv, key1, key2)`, p)
fmt.Printf("\nOriginal KV should be %v", kv.value)
// output:
// map[k1:v1]
// Original KV should be map[k1:v1 k2:v2]
}
func Example_omit_kv() {
var (
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
"k3": "v3",
}}
p = map[string]interface{}{
"kv": kv,
"key1": "k1",
"key2": "k3",
}
)
eval(`omit(kv, key1, key2)`, p)
fmt.Printf("\nOriginal KV should be %v", kv.value)
// output:
// map[k2:v2]
// Original KV should be map[k1:v1 k2:v2 k3:v3]
}
func Example_set_kvv() {
var (
kvv = &KVV{}
p = map[string]interface{}{
"kvv": kvv,
"key": "foo",
"value": Must(NewString("bar")),
}
)
eval(`set(kvv, key, value)`, p)
fmt.Printf("\nOriginal KVV should be %v", kvv.value)
// output:
// map[foo:[bar]]
// Original KVV should be map[]
}
func Example_merge_kvv() {
var (
kvv = &KVV{}
p = map[string]interface{}{
"kvv": kvv,
"foo": &KVV{value: map[string][]string{
"k1": {"v1"},
}},
"bar": &KVV{value: map[string][]string{
"k1": {"v11"},
"k2": {"v2"},
}},
}
)
eval(`merge(kvv, foo, bar)`, p)
fmt.Printf("\nOriginal KVV should be %v", kvv.value)
// output:
// map[k1:[v1 v11] k2:[v2]]
// Original KVV should be map[]
}
func Example_filter_kvv() {
var (
kvv = &KVV{value: map[string][]string{
"k1": {"v1"},
"k2": {"v2"},
}}
p = map[string]interface{}{
"kv": kvv,
"key1": "k1",
"key2": "k3",
}
)
eval(`filter(kv, key1, key2)`, p)
fmt.Printf("\nOriginal KVV should be %v", kvv.value)
// output:
// map[k1:[v1]]
// Original KVV should be map[k1:[v1] k2:[v2]]
}
func Example_omit_kvv() {
var (
kvv = &KVV{value: map[string][]string{
"k1": {"v1"},
"k2": {"v2"},
"k3": {"v3"},
}}
p = map[string]interface{}{
"kvv": kvv,
"key1": "k1",
"key2": "k3",
}
)
eval(`omit(kvv, key1, key2)`, p)
fmt.Printf("\nOriginal KVV should be %v", kvv.value)
// output:
// map[k2:[v2]]
// Original KVV should be map[k1:[v1] k2:[v2] k3:[v3]]
}
func TestTypedValueOperations(t *testing.T) {
scope, _ := NewVars(map[string]interface{}{
"xUint": uint(1),
"xInt": 1,
"xBoolT": true,
"xBoolF": false,
"xString": "foo",
})
tcc := []struct {
expects interface{}
expr string
}{
// sanity check
{true, "xBoolT"},
{false, "xBoolF"},
// uint ops
{true, "xUint == 1"},
{uint(1), "xUint"},
// uint ops
{true, "xInt == 1"},
{int(1), "xInt"},
// string ops
{true, `xString == "foo"`},
{"foo", "xString"},
}
for _, tc := range tcc {
t.Run(tc.expr, func(t *testing.T) {
var (
req = require.New(t)
parser = NewParser()
eval, err = parser.Parse(tc.expr)
)
req.NoError(err)
result, err := eval.Eval(context.Background(), scope)
req.NoError(err)
req.Equal(tc.expects, UntypedValue(result))
})
}
}
func TestKV_Assign(t *testing.T) {
var (
req = require.New(t)
vars = KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
)
req.NoError(Assign(&vars, "k1", Must(NewString("v11"))))
req.Equal("v11", vars.value["k1"])
req.Equal("v2", vars.value["k2"])
}
func TestKV_Set(t *testing.T) {
var (
req = require.New(t)
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
)
out, err := set(kv, "k1", Must(NewString("v11")))
req.NoError(err)
req.Equal("v11", out.(*KV).GetValue()["k1"])
// Making sure empty KV updates without error
kv = &KV{}
out, err = set(kv, "k1", Must(NewString("v11")))
req.NoError(err)
req.Equal("v11", out.(*KV).GetValue()["k1"])
}
func TestKV_Merge(t *testing.T) {
var (
req = require.New(t)
kv = &KV{}
foo = &KV{value: map[string]string{
"k1": "v1",
}}
bar = &KV{value: map[string]string{
"k2": "v2",
}}
expected = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
)
out, err := merge(kv, foo, bar)
req.NoError(err)
req.Equal(expected, out)
}
func TestKV_Clone(t *testing.T) {
var (
req = require.New(t)
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
expected = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
)
out, err := kv.Merge()
req.NoError(err)
req.Equal(expected, out)
}
func TestKV_Filter(t *testing.T) {
var (
req = require.New(t)
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}}
expected = &KV{value: map[string]string{
"k1": "v1",
}}
)
out, err := filter(kv, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
req.NotEqual(kv, out)
}
func TestKV_Omit(t *testing.T) {
var (
req = require.New(t)
kv = &KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
"k3": "v3",
}}
expected = &KV{value: map[string]string{
"k2": "v2",
}}
)
out, err := omit(kv, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
req.NotEqual(kv, out)
}
func TestKVV_Assign(t *testing.T) {
var (
req = require.New(t)
kvv KVV
)
req.NoError(Assign(&kvv, "foo", Must(NewString("bar"))))
req.Contains(kvv.value, "foo")
req.Equal([]string{"bar"}, kvv.value["foo"])
// Making sure http.Header is properly converted
kvv = KVV{}
req.NoError(kvv.Assign(http.Header{"foo-bar": []string{"bar"}}))
req.Contains(kvv.value, "foo-bar")
req.Equal([]string{"bar"}, kvv.value["foo-bar"])
// Making sure url.Values are properly converted
kvv = KVV{}
req.NoError(kvv.Assign(url.Values{"foo": []string{"bar"}}))
req.Contains(kvv.value, "foo")
req.Equal([]string{"bar"}, kvv.value["foo"])
// KVV is a flat store: keys with dots or brackets are treated as literal keys,
// not as indexed paths. set()/Assign() always replaces the entire value.
kvv = KVV{}
req.NoError(Assign(&kvv, "deep", Must(NewString("bar"))))
req.Contains(kvv.value, "deep")
req.Equal([]string{"bar"}, kvv.value["deep"])
req.NoError(Assign(&kvv, "deep", Must(NewString("baz"))))
req.Equal([]string{"baz"}, kvv.value["deep"])
req.NoError(Assign(&kvv, "key.with.dots", Must(NewString("v"))))
req.Contains(kvv.value, "key.with.dots")
req.Equal([]string{"v"}, kvv.value["key.with.dots"])
}
func TestKVV_Set(t *testing.T) {
var (
req = require.New(t)
kvv = &KVV{value: map[string][]string{
"k1": {"v1"},
"k2": {"v2"},
}}
)
out, err := set(kvv, "k1", Must(NewString("v11")))
req.NoError(err)
req.Equal([]string{"v11"}, out.(*KVV).GetValue()["k1"])
// Making sure empty KV updates without error
kvv = &KVV{}
out, err = set(kvv, "foo", Must(NewString("bar")))
req.NoError(err)
req.Equal([]string{"bar"}, out.(*KVV).GetValue()["foo"])
}
func TestKVV_Merge(t *testing.T) {
var (
req = require.New(t)
kvv *KVV
foo = KVV{value: map[string][]string{
"k1": {"v1"},
}}
bar = KVV{value: map[string][]string{
"k1": {"v11"},
"k2": {"v2"},
}}
expected = &KVV{value: map[string][]string{
"k1": {"v1", "v11"},
"k2": {"v2"},
}}
)
out, err := merge(kvv, &foo, &bar)
req.NoError(err)
req.Equal(expected, out)
}
func TestKVV_Clone(t *testing.T) {
var (
req = require.New(t)
kvv = KVV{value: map[string][]string{
"k1": {"v1", "v11"},
"k2": {"v2"},
}}
expected = &KVV{value: map[string][]string{
"k1": {"v1", "v11"},
"k2": {"v2"},
}}
)
out, err := kvv.Merge()
req.NoError(err)
req.Equal(expected, out)
}
func TestKVV_Filter(t *testing.T) {
var (
req = require.New(t)
kvv = &KVV{value: map[string][]string{
"k1": {"v1"},
"k2": {"v2"},
}}
expected = &KVV{value: map[string][]string{
"k1": {"v1"},
}}
)
out, err := filter(kvv, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
}
func TestKVV_Omit(t *testing.T) {
var (
req = require.New(t)
kvv = &KVV{value: map[string][]string{
"k1": {"v1"},
"k2": {"v2"},
"k3": {"v3"},
}}
expected = &KVV{value: map[string][]string{
"k2": {"v2"},
}}
)
out, err := omit(kvv, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
}
func TestCastEmptyString(t *testing.T) {
var (
req = require.New(t)
)
{
f, err := CastToFloat("")
req.NoError(err)
req.Zero(f)
}
{
i, err := CastToInteger("")
req.NoError(err)
req.Zero(i)
}
{
u, err := CastToUnsignedInteger("")
req.NoError(err)
req.Zero(u)
}
{
u, err := CastToDuration("")
req.NoError(err)
req.Zero(u)
}
{
u, err := CastToBoolean("")
req.NoError(err)
req.False(u)
}
}
func TestCastToArray(t *testing.T) {
var (
req = require.New(t)
)
arr, err := CastToArray([]interface{}{
Must(NewString("abc")),
Must(NewString("123")),
})
req.NoError(err)
req.Len(arr, 2)
}
func TestArrayDecode(t *testing.T) {
var (
req = require.New(t)
foo = struct {
Typed TypedValue
Iface interface{}
Strings []string
Values []TypedValue
}{}
)
arr, err := CastToArray([]interface{}{
Must(NewString("abc")),
Must(NewString("123")),
})
req.NoError(err)
vars, err := NewVars(map[string]interface{}{
"strings": &Array{value: arr},
"iface": Must(NewString("typed")),
"typed": Must(NewString("typed")),
"values": &Array{value: arr},
})
req.NoError(err)
req.NoError(vars.Decode(&foo))
req.Len(foo.Strings, 2)
req.Len(foo.Values, 2)
}
func TestVarsClone(t *testing.T) {
var (
req = require.New(t)
v = &Vars{
value: map[string]TypedValue{
"a1": Must(NewInteger(1)),
"a2": Must(NewInteger(2)),
"a3": Must(NewInteger(3)),
"a4": Must(NewInteger(4)),
"a5": Must(NewInteger(5)),
"a6": Must(NewInteger(6)),
"a7": Must(NewInteger(7)),
"a8": Must(NewInteger(8)),
"a9": Must(NewInteger(9)),
"a10": Must(NewInteger(10)),
},
}
)
check := func(out TypedValue, err error) {
req.NoError(err)
outVars := out.(*Vars)
req.Len(outVars.value, len(v.value))
for k, v := range v.value {
req.Contains(outVars.value, k)
req.Equal(v.Get(), outVars.value[k].Get())
}
}
t.Run("sequential", func(t *testing.T) {
check(v.cloneSeq())
})
t.Run("parallel", func(t *testing.T) {
check(v.cloneParallel(2))
})
t.Run("parallel odd", func(t *testing.T) {
check(v.cloneParallel(3))
})
t.Run("parallel one chunk", func(t *testing.T) {
check(v.cloneParallel(11))
})
}
func TestArrayClone(t *testing.T) {
var (
req = require.New(t)
vv = &Array{
value: []TypedValue{
Must(NewInteger(1)),
Must(NewInteger(2)),
Must(NewInteger(3)),
Must(NewInteger(4)),
Must(NewInteger(5)),
Must(NewInteger(6)),
Must(NewInteger(7)),
Must(NewInteger(8)),
Must(NewInteger(9)),
Must(NewInteger(10)),
},
}
)
check := func(out TypedValue, err error) {
req.NoError(err)
outArray := out.(*Array)
req.Len(outArray.value, len(vv.value))
for i, v := range vv.value {
req.Equal(v.Get(), outArray.value[i].Get())
}
}
t.Run("sequential", func(t *testing.T) {
check(vv.cloneSeq())
})
t.Run("parallel", func(t *testing.T) {
check(vv.cloneParallel(2))
})
t.Run("parallel odd", func(t *testing.T) {
check(vv.cloneParallel(3))
})
t.Run("parallel one chunk", func(t *testing.T) {
check(vv.cloneParallel(11))
})
}
// Original
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/pkg/expr
// BenchmarkVarsClone_10_10-12 2623 459222 ns/op 370399 B/op 4886 allocs/op
// BenchmarkVarsClone_100_100-12 25 46813440 ns/op 42108291 B/op 453755 allocs/op
// BenchmarkVarsClone_1000_1000-12 1 4572438792 ns/op 4004143648 B/op 45064443 allocs/op
// Reworked, all seq.
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/pkg/expr
// BenchmarkVarsClone_10_10-12 37764 31204 ns/op 90607 B/op 859 allocs/op
// BenchmarkVarsClone_100_100-12 303 3630116 ns/op 9128270 B/op 80430 allocs/op
// BenchmarkVarsClone_1000_1000-12 3 372980819 ns/op 898107160 B/op 8004031 allocs/op
// Reworked, seq threshold to parallel
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/pkg/expr
// BenchmarkVarsClone_10_10-12 35852 33396 ns/op 90606 B/op 859 allocs/op
// BenchmarkVarsClone_100_100-12 320 3692905 ns/op 9128270 B/op 80430 allocs/op
// BenchmarkVarsClone_1000_1000-12 7 175036667 ns/op 946114581 B/op 8003063 allocs/op
func benchmarkVarsClone(b *testing.B, rootCount, nestedCount int) {
nest := &Vars{
value: map[string]TypedValue{},
}
for i := 0; i < nestedCount; i++ {
nest.value[fmt.Sprintf("nest_value_%d", i)] = &Vars{
value: map[string]TypedValue{
"nested": &Vars{
value: map[string]TypedValue{
"nested": &Vars{
value: map[string]TypedValue{},
},
},
},
},
}
}
v := &Vars{
value: map[string]TypedValue{
"a11": Must(NewVars(map[string]any{})),
"a22": Must(NewVars(map[string]any{})),
"a33": Must(NewVars(map[string]any{})),
"a44": Must(NewVars(map[string]any{})),
"a55": Must(NewVars(map[string]any{})),
"a66": Must(NewVars(map[string]any{})),
"a77": Must(NewVars(map[string]any{})),
"a88": Must(NewVars(map[string]any{})),
"a99": Must(NewVars(map[string]any{})),
"a1010": Must(NewVars(map[string]any{})),
"a1111": Must(NewVars(map[string]any{})),
"a1212": Must(NewVars(map[string]any{})),
"a1313": Must(NewVars(map[string]any{})),
},
}
for i := 0; i < rootCount; i++ {
v.value[fmt.Sprintf("nest_value_%d", i)] = nest
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.Clone()
}
}
func BenchmarkVarsClone_10_10(b *testing.B) {
benchmarkVarsClone(b, 10, 10)
}
func BenchmarkVarsClone_100_100(b *testing.B) {
benchmarkVarsClone(b, 100, 100)
}
func BenchmarkVarsClone_1000_1000(b *testing.B) {
benchmarkVarsClone(b, 1000, 1000)
}

311
server/pkg/expr/func_arr.go Normal file
View File

@@ -0,0 +1,311 @@
package expr
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/PaesslerAG/gval"
)
func ArrayFunctions() []gval.Language {
return []gval.Language{
gval.Function("push", push),
gval.Function("pop", pop),
gval.Function("shift", shift),
gval.Function("count", count),
gval.Function("has", Has),
gval.Function("hasAll", hasAll),
gval.Function("find", find),
gval.Function("sort", sortSlice),
gval.Function("splice", splice),
}
}
// push adds a value to the end of slice, returns copy
func push(arr interface{}, nn ...interface{}) (out interface{}, err error) {
if arr == nil {
// If base is empty, return pushed items directly
return nn, nil
} else if i, is := arr.([]interface{}); is {
// Simple append if we're dealing with []interface{} base
return append(i, nn...), nil
} else if arr, err = toSlice(arr); err != nil {
return
}
if stv, is := arr.([]TypedValue); is {
// slice of typed values, this will make things easier
for _, n := range nn {
if tv, is := n.(TypedValue); is {
stv = append(stv, tv)
} else {
// wrap unknown types...
stv = append(stv, Must(Typify(n)))
}
}
return stv, nil
}
var (
c = reflect.ValueOf(arr)
nval = reflect.MakeSlice(c.Type(), c.Len()+len(nn), c.Cap()+len(nn))
)
reflect.Copy(nval, c)
for i, n := range nn {
nt := reflect.ValueOf(n).Type()
it := nval.Index(c.Len() + i).Type()
if nt != it {
return nil, fmt.Errorf("can not push %v to %v slice", nt, it)
}
nval.Index(c.Len() + i).Set(reflect.ValueOf(n))
}
return nval.Interface(), nil
}
// pop takes the last value in slice, does not modify original
func pop(arr interface{}) (out interface{}, err error) {
if arr, err = toSlice(arr); err != nil {
return
}
c := reflect.ValueOf(arr)
if c.Len() == 0 {
return nil, nil
}
return c.Index(c.Len() - 1).Interface(), nil
}
// shifts takes the first value in slice, does not modify original
func shift(arr interface{}) (out interface{}, err error) {
if arr, err = toSlice(arr); err != nil {
return
}
c := reflect.ValueOf(arr)
if c.Len() == 0 {
return nil, nil
}
return c.Index(0).Interface(), nil
}
// count gets the count of occurrences in the first params(can be string, slice)
// If given only one parameter then it returns length as count
func count(arr interface{}, v ...interface{}) (count int, err error) {
typeErr := fmt.Errorf("unexpected type: %T, expecting slice", arr)
arr = UntypedValue(arr)
if stv, is := arr.([]TypedValue); is {
if len(v) == 0 {
return len(stv), nil
}
for _, vv := range v {
if occ, err := find(stv, vv); err != nil {
return 0, err
} else if occ != -1 {
count++
}
}
return count, nil
}
var (
occ int
c = reflect.ValueOf(arr)
)
switch c.Kind() {
case reflect.String:
if len(v) == 0 {
return len(c.String()), nil
}
for _, ww := range v {
count += strings.Count(c.String(), reflect.ValueOf(ww).String())
}
return
case reflect.Slice:
if len(v) == 0 {
return c.Len(), nil
}
for _, vv := range v {
if occ, err = find(arr, vv); err != nil {
return 0, err
} else if occ != -1 {
count++
}
}
default:
return 0, typeErr
}
return
}
// has finds any occurrence of the values in slice or key in a map
func Has(arr interface{}, vv ...interface{}) (b bool, err error) {
arr = UntypedValue(arr)
if isMap(arr) {
for _, v := range vv {
if reflect.ValueOf(arr).MapIndex(reflect.ValueOf(v)).IsValid() {
return true, nil
}
}
return
}
var c int
if c, err = count(arr, vv...); err != nil {
return
}
return c > 0, nil
}
// hasAll finds all the occurrences in the slice
func hasAll(arr interface{}, v ...interface{}) (b bool, err error) {
if arr, err = toSlice(arr); err != nil {
return
}
var c int
if c, err = count(arr, v...); err != nil {
return
}
return c == len(v), nil
}
// find takes a value and gets the position in slice
// if no results, returns -1
func find(arr interface{}, v interface{}) (p int, err error) {
if arr, err = toSlice(arr); err != nil {
return
}
for p = 0; p < reflect.ValueOf(arr).Len(); p++ {
c := reflect.ValueOf(arr)
if UntypedValue(c.Index(p).Interface()) == v {
return
}
}
return -1, nil
}
// splice slices slice
func splice(arr interface{}, s, e float64) interface{} {
var (
err error
start = int(s)
end = int(e)
)
if arr, err = toSlice(arr); err != nil {
return nil
}
v := reflect.ValueOf(arr)
if start >= v.Len() {
return v.Interface()
}
if end == -1 || end > v.Len() {
end = v.Len()
}
return v.Slice(start, end).Interface()
}
// sortSlice sorts slice
func sortSlice(arr interface{}, desc bool) (out interface{}, err error) {
if arr, err = toSlice(arr); err != nil {
return
}
// sort slice of native type values
var (
c = reflect.ValueOf(arr)
v = reflect.MakeSlice(c.Type(), c.Len(), c.Cap())
vi = v.Interface()
)
reflect.Copy(v, c)
// sortStable sorts the slice x using the provided less
// function, keeping equal elements in their original order
//
// sort the array if error is nil;
// we trap the error(s) in the outer scope from the less function,
// and returning the last error at a time since all error will the same in almost every scenario.
sort.SliceStable(vi, func(i, j int) bool {
if err != nil {
return false
}
var (
nVal = v.Index(i)
mVal = v.Index(j)
)
if desc {
nVal = v.Index(j)
mVal = v.Index(i)
}
switch getKind(vi) {
case reflect.String:
return nVal.String() < mVal.String()
case reflect.Int, reflect.Int32, reflect.Int64:
return nVal.Int() < mVal.Int()
case reflect.Float32, reflect.Float64:
return nVal.Float() < mVal.Float()
default:
// sort slice of typed values
if stv, is := vi.([]TypedValue); is {
if casted, ok := stv[i].(Comparable); ok {
var cmp int
cmp, err = casted.Compare(stv[j])
if err != nil {
return false
}
if desc {
return cmp > 0
} else {
return cmp < 0
}
} else {
err = fmt.Errorf("cannot compare %s and %s: unknown state", stv[i].Type(), stv[j].Type())
return false
}
}
err = fmt.Errorf("cannot compare %s and %s: unknown state", getKind(nVal), getKind(mVal))
return false
}
})
// returns error from less function
if err != nil {
return arr, err
}
return vi, nil
}

View File

@@ -0,0 +1,706 @@
package expr
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
var (
intArr = []int{}
stringArr = []string{"first"}
boolArr = []bool{true, true, false}
floatArr = []float64{69.420}
strArr = []string{"5", "3", "1", "2"}
vals = map[string]interface{}{
"intArr": intArr,
"stringArr": stringArr,
"boolArr": boolArr,
"floatArr": floatArr,
"strArr": strArr,
"intVal": 42,
"stringVal": "foobar",
"boolVal": false,
}
)
func Example_push_int() {
eval(`push(intArr, intVal)`, vals)
// output:
// [42]
}
func Example_push_string() {
eval(`push(stringArr, stringVal)`, vals)
// output:
// [first foobar]
}
func Example_push_bool() {
eval(`push(boolArr, boolVal)`, vals)
// output:
// [true true false false]
}
func Example_push_float() {
eval(`push(floatArr, 3.14)`, vals)
// output:
// [69.42 3.14]
}
func Example_pop_string() {
eval(`pop(stringArr)`, vals)
// output:
// first
}
func Example_pop_int() {
eval(`pop(intArr)`, vals)
// output:
// <nil>
}
func Example_pop_float() {
eval(`pop(floatArr)`, vals)
// output:
// 69.42
}
func Example_sort_string_asc() {
eval(`sort(strArr, false)`, vals)
// output:
// [1 2 3 5]
}
func Example_sort_string_desc() {
eval(`sort(strArr, true)`, vals)
// output:
// [5 3 2 1]
}
func Test_push(t *testing.T) {
tcc := []struct {
base interface{}
new []interface{}
expect interface{}
expError bool
}{
{
base: []string{"1", "2", "3"},
new: []interface{}{"4"},
expect: []string{"1", "2", "3", "4"},
},
{
base: Must(NewArray(nil)),
new: []interface{}{"foo"},
expect: []TypedValue{Must(NewString("foo"))},
},
{
base: Must(NewArray([]interface{}{"foo"})),
new: []interface{}{"bar"},
expect: []TypedValue{Must(NewString("foo")), Must(NewString("bar"))},
},
{
base: Must(NewArray([]interface{}{Must(NewString("foo"))})),
new: []interface{}{"bar"},
expect: []TypedValue{Must(NewString("foo")), Must(NewString("bar"))},
},
{
base: Must(NewArray([]interface{}{Must(NewString("foo"))})),
new: []interface{}{Must(NewString("bar"))},
expect: []TypedValue{Must(NewString("foo")), Must(NewString("bar"))},
},
{
base: []string{"1", "2", "3"},
new: []interface{}{4},
expError: true,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
out, err = push(tc.base, tc.new...)
)
if tc.expError {
req.Error(err)
return
}
req.NoError(err)
req.Equal(tc.expect, out)
})
}
}
func Test_shift(t *testing.T) {
tcc := []struct {
value interface{}
expect interface{}
expError bool
}{
{
value: []string{"1", "2", "3"},
expect: "1",
},
{
value: map[string]string{"test": "123"},
expect: nil,
expError: true,
},
{
value: []int{4, 5, 6, 7},
expect: 4,
},
{
value: []int{},
expect: nil,
},
{
value: int(1),
expect: nil,
expError: true,
},
{
value: []float64{11.1},
expect: 11.1,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
val, err = shift(tc.value)
)
if tc.expError {
req.Error(err)
return
}
req.NoError(err)
req.Equal(tc.expect, val)
})
}
}
func Test_find(t *testing.T) {
tcc := []struct {
expect interface{}
arr interface{}
val interface{}
}{
{
arr: must(CastToArray([]string{"123", "456"})),
val: "456",
expect: 1,
},
{
arr: []string{"1", "2", "3"},
val: "3",
expect: 2,
},
{
arr: []bool{true, false, true},
val: true,
expect: 0,
},
{
arr: []int{4, 5, 6, 7},
val: 7,
expect: 3,
},
{
arr: []int{},
val: 0,
expect: -1,
},
{
arr: []float64{11.1, 12.4},
val: 11.1,
expect: 0,
},
{
arr: []float64{11.1, 12.4},
val: 11.2,
expect: -1,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
loc, err = find(tc.arr, tc.val)
)
req.NoError(err)
req.Equal(tc.expect, loc)
})
}
}
func Test_count(t *testing.T) {
tcc := []struct {
expect interface{}
arr interface{}
val []interface{}
}{
{
arr: must(CastToArray([]string{"123"})),
val: []interface{}{"123", "567"},
expect: 1,
},
{
arr: []string{"1", "2", "3"},
val: []interface{}{"0", "3"},
expect: 1,
},
{
arr: []bool{true, true},
val: []interface{}{false, false},
expect: 0,
},
{
arr: []bool{true, true},
val: []interface{}{false, true},
expect: 1,
},
{
arr: []int{4, 5, 6, 7},
val: []interface{}{7, 4},
expect: 2,
},
{
arr: []float64{11.1, 12.4},
val: []interface{}{0.1, 1.1},
expect: 0,
},
{
arr: "foo",
val: nil,
expect: 3,
},
{
arr: "foo",
val: []interface{}{},
expect: 3,
},
{
arr: "foo bar",
val: []interface{}{},
expect: 7,
},
{
arr: []bool{true, true},
val: []interface{}{},
expect: 2,
},
{
arr: "foo",
val: []interface{}{"bar", "baz"},
expect: 0,
},
{
arr: "foo",
val: []interface{}{"o", 12},
expect: 2,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
err error
loc int
)
loc, err = count(tc.arr, tc.val...)
req.NoError(err)
req.Equal(tc.expect, loc)
})
}
}
func Test_has(t *testing.T) {
tcc := []struct {
expect interface{}
arr interface{}
val []interface{}
}{
{
arr: []string{"1", "2", "3"},
val: []interface{}{"0", "3"},
expect: true,
},
{
arr: []bool{true, true},
val: []interface{}{false, false},
expect: false,
},
{
arr: []bool{true, true},
val: []interface{}{false, true},
expect: true,
},
{
arr: []int{4, 5, 6, 7},
val: []interface{}{7, 4},
expect: true,
},
{
arr: []float64{11.1, 12.4},
val: []interface{}{0.1, 1.1},
expect: false,
},
{
arr: map[string]interface{}{"a": 1},
val: []interface{}{"a", "b"},
expect: true,
},
{
arr: map[string]interface{}{"a": 1},
val: []interface{}{"b"},
expect: false,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
loc bool
err error
)
loc, err = Has(tc.arr, tc.val...)
req.NoError(err)
req.Equal(tc.expect, loc)
})
}
}
func Test_hasAll(t *testing.T) {
tcc := []struct {
arr interface{}
val []interface{}
hasAll bool
expError bool
}{
{
arr: []string{"1", "2", "3"},
val: []interface{}{"0", "3"},
hasAll: false,
},
{
arr: []bool{true, true},
val: []interface{}{false, false},
hasAll: false,
},
{
arr: []bool{true, true},
val: []interface{}{false, true},
hasAll: false,
},
{
arr: []int{4, 5, 6, 7},
val: []interface{}{7, 4},
hasAll: true,
},
{
arr: []float64{11.1, 12.4},
val: []interface{}{0.1, 1.1},
hasAll: false,
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
rval, err = hasAll(tc.arr, tc.val...)
)
if tc.expError {
req.Error(err)
return
}
req.NoError(err)
req.Equal(tc.hasAll, rval)
})
}
}
func Test_splice(t *testing.T) {
tcc := []struct {
vals []float64
arr interface{}
expect interface{}
}{
{
vals: []float64{0, 3},
arr: []string{"1", "2", "3"},
expect: []string{"1", "2", "3"},
},
{
vals: []float64{0, 1},
arr: []string{"1", "2", "3"},
expect: []string{"1"},
},
{
vals: []float64{2, 3},
arr: []bool{true, true},
expect: []bool{true, true},
},
{
vals: []float64{1, -1},
arr: []int{4, 5, 6, 7},
expect: []int{5, 6, 7},
},
{
vals: []float64{3, -1},
arr: []float64{11.1, 12.4},
expect: []float64{11.1, 12.4},
},
}
for p, tc := range tcc {
t.Run(fmt.Sprintf("%d", p), func(t *testing.T) {
var (
req = require.New(t)
ss = splice(tc.arr, tc.vals[0], tc.vals[1])
)
req.Equal(tc.expect, ss)
})
}
}
func Test_sortSlice(t *testing.T) {
var (
s1 = Must(NewString("1"))
s2 = Must(NewString("2"))
s3 = Must(NewString("3"))
s5 = Must(NewString("5"))
f1 = Must(NewFloat(11.1))
f2 = Must(NewString(22.2))
f3 = Must(NewString(33.3))
f5 = Must(NewString(55.5))
a1 = Must(NewAny("1"))
a2 = Must(NewAny("2"))
a3 = Must(NewAny("3"))
a5 = Must(NewAny("5"))
tcc = []struct {
name string
desc bool
arr interface{}
cloneArr interface{}
expect interface{}
expectErr error
}{
{
name: "ascending sorting for string array",
arr: []string{"3", "1", "2", "5"},
cloneArr: []string{"3", "1", "2", "5"},
expect: []string{"1", "2", "3", "5"},
},
{
name: "ascending sorting for string array with multiple identical element",
arr: []string{"1", "3", "2", "2"},
cloneArr: []string{"1", "3", "2", "2"},
expect: []string{"1", "2", "2", "3"},
},
{
name: "descending sorting for string array",
desc: true,
arr: []string{"3", "1", "2", "5"},
cloneArr: []string{"3", "1", "2", "5"},
expect: []string{"5", "3", "2", "1"},
},
{
name: "ascending sorting for int array",
arr: []int{1, 3, 5, 2},
cloneArr: []int{1, 3, 5, 2},
expect: []int{1, 2, 3, 5},
},
{
name: "descending sorting for int array",
desc: true,
arr: []int{1, 3, 5, 2},
cloneArr: []int{1, 3, 5, 2},
expect: []int{5, 3, 2, 1},
},
{
name: "ascending sorting for float32 array",
arr: []float32{11.1, 33.3, 55.5, 22.2},
cloneArr: []float32{11.1, 33.3, 55.5, 22.2},
expect: []float32{11.1, 22.2, 33.3, 55.5},
},
{
name: "descending sorting for float32 array",
desc: true,
arr: []float32{11.1, 33.3, 55.5, 22.2},
cloneArr: []float32{11.1, 33.3, 55.5, 22.2},
expect: []float32{55.5, 33.3, 22.2, 11.1},
},
{
name: "ascending sorting for float64 array",
arr: []float64{11.1, 33.3, 55.5, 22.2},
cloneArr: []float64{11.1, 33.3, 55.5, 22.2},
expect: []float64{11.1, 22.2, 33.3, 55.5},
},
{
name: "descending sorting for float64 array",
desc: true,
arr: []float64{11.1, 33.3, 55.5, 22.2},
cloneArr: []float64{11.1, 33.3, 55.5, 22.2},
expect: []float64{55.5, 33.3, 22.2, 11.1},
},
{
name: "ascending sorting for typedValue array of string",
arr: []TypedValue{
s5,
s3,
s2,
s1,
},
cloneArr: []TypedValue{
s5,
s3,
s2,
s1,
},
expect: []TypedValue{
s1,
s2,
s3,
s5,
},
},
{
name: "descending sorting for typedValue array of string",
desc: true,
arr: []TypedValue{
s3,
s1,
s2,
s5,
},
cloneArr: []TypedValue{
s3,
s1,
s2,
s5,
},
expect: []TypedValue{
s5,
s3,
s2,
s1,
},
},
{
name: "ascending sorting for typedValue array of float",
arr: []TypedValue{
f5,
f3,
f2,
f1,
},
cloneArr: []TypedValue{
f5,
f3,
f2,
f1,
},
expect: []TypedValue{
f1,
f2,
f3,
f5,
},
},
{
name: "descending sorting for typedValue array of float",
desc: true,
arr: []TypedValue{
f3,
f1,
f2,
f5,
},
cloneArr: []TypedValue{
f3,
f1,
f2,
f5,
},
expect: []TypedValue{
f5,
f3,
f2,
f1,
},
},
{
name: "expect error due to sorting for typedValue array of Any(Not Comparable)",
arr: []TypedValue{
a5,
a3,
a2,
a1,
},
cloneArr: []TypedValue{
a5,
a3,
a2,
a1,
},
expectErr: fmt.Errorf("cannot compare Any and Any: unknown state"),
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
)
ss, err := sortSlice(tc.arr, tc.desc)
if tc.expectErr != nil {
req.Equal(tc.expectErr, err)
return
}
req.NoError(err)
req.Equal(tc.expect, ss)
req.Equal(tc.cloneArr, tc.arr)
})
}
}
func must(v []TypedValue, err error) []TypedValue {
if err != nil {
panic(err)
}
return v
}

View File

@@ -0,0 +1,136 @@
package expr
import (
"fmt"
"reflect"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/gvalfnc"
)
type (
empty interface {
IsEmpty() bool
}
)
func GenericFunctions() []gval.Language {
return []gval.Language{
gval.Function("coalesce", coalesce),
gval.Function("isEmpty", isEmpty),
gval.Function("isNil", isNil),
gval.Function("length", length),
}
}
func coalesce(aa ...interface{}) interface{} {
for _, a := range aa {
if !isNil(a) {
return a
}
}
return nil
}
func length(i interface{}) int {
i = UntypedValue(i)
if isEmpty(i) {
return 0
}
switch reflect.TypeOf(i).Kind() {
case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map, reflect.String:
return reflect.ValueOf(i).Len()
}
return 0
}
func isNil(i interface{}) bool {
_, isTyped := i.(TypedValue)
if isTyped {
switch i.(type) {
case *Duration, *DateTime, *Bytes, *Array, *Integer:
i = i.(TypedValue).Get()
}
}
return gvalfnc.IsNil(i)
}
// empty checks values and slices
func isEmpty(i interface{}) bool {
if isNil(i) {
return true
}
if c, ok := i.(empty); ok {
return c.IsEmpty()
}
switch reflect.TypeOf(i).Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
return reflect.ValueOf(i).Len() == 0
}
return reflect.ValueOf(i).IsZero()
}
func isInt(v interface{}) bool {
switch getKind(v) {
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8:
return true
}
return false
}
func isString(v interface{}) bool {
return getKind(v) == reflect.String
}
func isFloat(v interface{}) bool {
switch getKind(v) {
case reflect.Float32, reflect.Float64:
return true
}
return false
}
func isBool(v interface{}) bool {
return getKind(v) == reflect.Bool
}
func getKind(v interface{}) reflect.Kind {
kind := reflect.TypeOf(v).Kind()
if isSlice(v) {
kind = reflect.TypeOf(v).Elem().Kind()
}
return kind
}
func isSlice(v interface{}) bool {
return reflect.TypeOf(v).Kind() == reflect.Slice || reflect.TypeOf(v).Kind() == reflect.Array
}
func isMap(v interface{}) bool {
return reflect.TypeOf(v).Kind() == reflect.Map
}
// toArray removes expr types (if wrapped) and checks if the variable is slice
// internal only
func toSlice(vv interface{}) (interface{}, error) {
vv = UntypedValue(vv)
if !isSlice(vv) {
return nil, fmt.Errorf("unexpected type: %T, expecting slice", vv)
}
return vv, nil
}

View File

@@ -0,0 +1,316 @@
package expr
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_isEmpty(t *testing.T) {
var (
req = require.New(t)
unsetSliceString []string
unsetSliceInt []int8
unsetSliceBool []bool
unsetSliceFloat []float32
unsetString string
unsetInt64 int64
unsetDateTime *time.Time
unsetDuration time.Duration
tcc = []struct {
value interface{}
expect interface{}
sc string
}{
{
value: []string{},
expect: true,
sc: "empty string slice",
},
{
value: map[string]string{},
expect: true,
sc: "empty map of strings",
},
{
value: unsetSliceString,
expect: true,
sc: "undefined string slice",
},
{
value: []int{},
expect: true,
sc: "empty int slice",
},
{
value: []int{1},
expect: false,
sc: "1-elem int slice",
},
{
value: unsetSliceInt,
expect: true,
sc: "undefined int slice",
},
{
value: unsetSliceBool,
expect: true,
sc: "undefined slice bool",
},
{
value: int(1),
expect: false,
sc: "defined int",
},
{
value: int(0),
expect: true,
sc: "defined int 0",
},
{
value: "",
expect: true,
sc: "emty string",
},
{
value: unsetString,
expect: true,
sc: "undefined string",
},
{
value: unsetSliceFloat,
expect: true,
sc: "undefined slice float",
},
{
value: unsetInt64,
expect: true,
sc: "undefined slice int64",
},
{
value: []float32{11.1},
expect: false,
sc: "non-empty slice float32",
},
{
value: []float32{},
expect: true,
sc: "empty slice float32",
},
{
value: unsetDateTime,
expect: true,
sc: "undefined datetime",
},
{
value: Must(NewInteger(nil)),
expect: false,
sc: "undefined Integer expr",
},
{
value: Must(NewDateTime(nil)),
expect: true,
sc: "undefined DateTime expr",
},
{
value: Must(NewDateTime(unsetDateTime)),
expect: true,
sc: "undefined DateTime expr",
},
{
value: Must(NewArray([]string{"A"})),
expect: false,
sc: "non empty Array expr",
},
{
value: Must(NewBoolean(nil)),
expect: false,
sc: "undefined Boolean expr",
},
{
value: Must(NewBytes(nil)),
expect: true,
sc: "undefined Bytes expr",
},
{
value: Must(NewDuration(unsetDuration)),
expect: false,
sc: "undefined Duration expr",
},
}
)
for _, tst := range tcc {
req.Equal(tst.expect, isEmpty(tst.value), "Failed isEmpty test: %s", tst.sc)
}
}
func Test_isNil(t *testing.T) {
var (
req = require.New(t)
unsetSliceString []string
unsetSliceInt []int8
unsetSliceBool []bool
unsetSliceFloat []float32
unsetString string
unsetInt64 int64
unsetDateTime *time.Time
unsetDuration time.Duration
tcc = []struct {
value interface{}
expect interface{}
sc string
}{
{
value: []string{},
expect: false,
sc: "empty string slice",
},
{
value: map[string]string{},
expect: false,
sc: "empty map of strings",
},
{
// @todo
value: unsetSliceString,
expect: false,
sc: "undefined string slice",
},
{
value: []int{},
expect: false,
sc: "empty int slice",
},
{
value: []int{1},
expect: false,
sc: "1-elem int slice",
},
{
// @todo
value: unsetSliceInt,
expect: false,
sc: "undefined int slice",
},
{
value: unsetSliceBool,
expect: false,
sc: "undefined slice bool",
},
{
value: int(1),
expect: false,
sc: "defined int",
},
{
value: int(0),
expect: false,
sc: "defined int 0",
},
{
value: "",
expect: false,
sc: "emty string",
},
{
value: unsetString,
expect: false,
sc: "undefined string",
},
{
value: unsetSliceFloat,
expect: false,
sc: "undefined slice float",
},
{
value: unsetInt64,
expect: false,
sc: "undefined slice int64",
},
{
value: []float32{11.1},
expect: false,
sc: "non-empty slice float32",
},
{
value: []float32{},
expect: false,
sc: "empty slice float32",
},
{
value: unsetDateTime,
expect: true,
sc: "undefined datetime",
},
{
value: Must(NewInteger(nil)),
expect: false,
sc: "undefined Integer expr",
},
{
value: Must(NewDateTime(nil)),
expect: true,
sc: "nil DateTime expr",
},
{
value: Must(NewDateTime(unsetDateTime)),
expect: true,
sc: "undefined DateTime expr",
},
{
value: Must(NewArray([]string{"A"})),
expect: false,
sc: "non empty Array expr",
},
{
value: Must(NewBoolean(nil)),
expect: false,
sc: "undefined Boolean expr",
},
{
value: Must(NewBytes(nil)),
expect: false,
sc: "undefined Bytes expr",
},
{
value: Must(NewDuration(unsetDuration)),
expect: false,
sc: "undefined Duration expr",
},
}
)
for _, tst := range tcc {
req.Equal(tst.expect, isNil(tst.value), "Failed isNil test: %s", tst.sc)
}
}
func Test_length(t *testing.T) {
var (
req = require.New(t)
tcc = []struct {
len int
value interface{}
}{
{0, []string{}},
{0, map[string]string{}},
{3, "foo"},
{0, make(chan string)},
{0, 34234},
{2, Must(NewAny([]int{1,2}))},
{6, Must(NewAny("foobar"))},
{5, Must(NewArray([]string{"A","R","R","A","Y"}))},
{0, Must(NewArray([]string{}))},
}
)
for _, tst := range tcc {
req.Equal(tst.len, length(tst.value))
}
}

View File

@@ -0,0 +1,21 @@
package expr
import (
"encoding/json"
"github.com/PaesslerAG/gval"
)
func JsonFunctions() []gval.Language {
return []gval.Language{
// TODO: Json decoding, parsing, stringify, JQ, JSONPath
gval.Function("toJSON", toJSON),
}
}
func toJSON(f interface{}) string {
if _, is := f.(json.Marshaler); !is {
f = UntypedValue(f)
}
b, _ := json.Marshal(f)
return string(b)
}

View File

@@ -0,0 +1,37 @@
package expr
func Example_toJSON() {
var (
p = map[string]interface{}{
"vars": Must(NewVars(
&Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
}},
)),
}
)
eval(`toJSON(vars)`, p)
// output:
// {"k1":{"@value":"v1","@type":"String"},"k2":{"@value":"v2","@type":"String"}}
}
func Example_kv_toJSON() {
var (
p = map[string]interface{}{
"kv": Must(NewKV(
&KV{value: map[string]string{
"k1": "v1",
"k2": "v2",
}},
)),
}
)
eval(`toJSON(kv)`, p)
// output:
// {"k1":"v1","k2":"v2"}
}

162
server/pkg/expr/func_num.go Normal file
View File

@@ -0,0 +1,162 @@
package expr
import (
"fmt"
"math"
"math/rand"
"strconv"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/gvalfnc"
"github.com/spf13/cast"
)
func NumericFunctions() []gval.Language {
return []gval.Language{
gval.Function("min", min),
gval.Function("max", max),
gval.Function("round", round),
gval.Function("floor", floor),
gval.Function("ceil", ceil),
gval.Function("abs", math.Abs),
gval.Function("log", math.Log10),
gval.Function("pow", math.Pow),
gval.Function("sqrt", math.Sqrt),
gval.Function("sum", sum),
gval.Function("average", average),
gval.Function("random", random),
gval.Function("int", toInt64),
gval.Function("float", gvalfnc.CastFloat),
}
}
func min(aa ...interface{}) (min float64) {
return findMinMax(-1, aa...)
}
func max(aa ...interface{}) (min float64) {
return findMinMax(1, aa...)
}
func findMinMax(dir int, aa ...interface{}) (mm float64) {
var (
set bool
flt float64
)
for i := range aa {
switch conv := aa[i].(type) {
case int:
flt = float64(conv)
case int64:
flt = float64(conv)
case uint:
flt = float64(conv)
case uint64:
flt = float64(conv)
case float32:
flt = float64(conv)
case float64:
flt = conv
default:
continue
}
if !set {
set = true
mm = flt
} else if dir < 0 {
mm = math.Min(mm, flt)
} else if dir > 0 {
mm = math.Max(mm, flt)
}
}
return mm
}
func round(f float64, d float64) float64 {
p := math.Pow(10, d)
return math.Round(f*p) / p
}
func floor(f float64) float64 {
return math.Floor(f)
}
func ceil(f float64) float64 {
return math.Ceil(f)
}
func sum(v ...interface{}) float64 {
var sum float64
for _, vv := range v {
c, err := CastToFloat(vv)
if err != nil {
continue
}
sum += c
}
return sum
}
func average(v ...interface{}) float64 {
var i int
var j, sum float64
for ; i < len(v); i++ {
c, err := CastToFloat(v[i])
if err != nil {
continue
}
j++
sum += c
}
return sum / j
}
func random(v ...float64) (out float64, err error) {
var (
from, to float64
totalArgs = len(v)
)
if totalArgs == 0 || totalArgs > 2 {
return 0, fmt.Errorf("expecting 1 or 2 parameter, got %d", totalArgs)
}
if totalArgs > 0 {
if to = v[0]; to < 0 {
return 0, fmt.Errorf("unexpected input type of 1st parameter")
}
}
if totalArgs > 1 {
if to = v[1]; to < 0 {
return 0, fmt.Errorf("unexpected input type of 2nd parameter")
}
from = v[0]
}
out = from + rand.Float64()*(to-from)
return
}
func toInt64(aa interface{}) (i int64) {
// Parse strings as base-10 to avoid cast.ToInt64's octal interpretation of leading zeros
if str, ok := aa.(string); ok {
parsed, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0
}
return parsed
}
return cast.ToInt64(aa)
}

View File

@@ -0,0 +1,148 @@
package expr
func Example_min() {
eval(`min(2, 1, 3)`, nil)
// output:
// 1
}
func Example_max() {
eval(`max(2, 1, 3)`, nil)
// output:
// 3
}
func Example_round() {
eval(`round(3.14,1)`, nil)
// output:
// 3.1
}
func Example_floor() {
eval(`floor(3.14)`, nil)
// output:
// 3
}
func Example_ceil() {
eval(`ceil(3.14)`, nil)
// output:
// 4
}
func Example_abs() {
eval(`abs(-3.14)`, nil)
// output:
// 3.14
}
func Example_log() {
eval(`log(100)`, nil)
// output:
// 2
}
func Example_ln() {
eval(`pow(2, 3)`, nil)
// output:
// 8
}
func Example_sqrt() {
eval(`sqrt(16)`, nil)
// output:
// 4
}
func Example_sum() {
eval(`sum(1, 2, "foo", 3.4, 4, "3")`, 10.4)
// output:
// 13.4
}
func Example_average() {
eval(`average(1, 2, 3, "foo", 10)`, 10.4)
// output:
// 4
}
func Example_randomWithSingleInput() {
eval(`max(random(6), 7)`, nil)
// output:
// 7
}
func Example_randomWithTwoInput() {
eval(`min(random(2, 6), 1)`, nil)
// output:
// 1
}
func Example_castStringToInt() {
eval(`int("abc")`, nil)
// output:
// 0
}
func Example_castStringNumberToInt() {
eval(`int("42690")`, nil)
// output:
// 42690
}
func Example_castFloatToInt() {
eval(`int("42.690")`, nil)
// output:
// 0
}
func Example_castStringWithLeadingZero08() {
eval(`int("08")`, nil)
// output:
// 8
}
func Example_castStringWithLeadingZero09() {
eval(`int("09")`, nil)
// output:
// 9
}
func Example_castStringWithLeadingZero010() {
eval(`int("010")`, nil)
// output:
// 10
}
func Example_castStringWithLeadingZero011() {
eval(`int("011")`, nil)
// output:
// 11
}
func Example_castStringWithLeadingZero012() {
eval(`int("012")`, nil)
// output:
// 12
}

257
server/pkg/expr/func_str.go Normal file
View File

@@ -0,0 +1,257 @@
package expr
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"github.com/spf13/cast"
"github.com/PaesslerAG/gval"
valid "github.com/asaskevich/govalidator"
)
func StringFunctions() []gval.Language {
return []gval.Language{
gval.Function("trim", strings.TrimSpace),
gval.Function("trimLeft", strings.TrimLeft),
gval.Function("trimRight", strings.TrimRight),
gval.Function("toLower", strings.ToLower),
gval.Function("toUpper", strings.ToUpper),
gval.Function("shortest", shortest),
gval.Function("longest", longest),
gval.Function("format", fmt.Sprintf),
gval.Function("title", title),
gval.Function("untitle", untitle),
gvalFunc("repeat", strings.Repeat),
gvalFunc("replace", strings.Replace),
gval.Function("isUrl", valid.IsURL),
gval.Function("isEmail", valid.IsEmail),
gval.Function("split", strings.Split),
gval.Function("join", join),
gval.Function("hasSubstring", hasSubstring),
gvalFunc("substring", substring),
gval.Function("hasPrefix", strings.HasPrefix),
gval.Function("hasSuffix", strings.HasSuffix),
gvalFunc("shorten", shorten),
gval.Function("camelize", camelize),
gval.Function("snakify", snakify),
gval.Function("match", match),
gval.Function("base64encode", base64encode),
}
}
// gvalFunc cast any nth number of float64 param to int
func gvalFunc(name string, fn interface{}) gval.Language {
return gval.Function(name, func(params ...interface{}) (out interface{}) {
in := make([]reflect.Value, len(params))
for i, param := range params {
switch param.(type) {
case float32, float64,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64:
param = cast.ToInt(param)
}
in[i] = reflect.ValueOf(param)
}
fun := reflect.ValueOf(fn)
res := fun.Call(in)
if len(res) > 0 {
out = cast.ToString(reflect.ValueOf(res[0]).Interface())
}
return
})
}
func shortest(f string, aa ...string) string {
for _, s := range aa {
if len(f) > len(s) {
f = s
}
}
return f
}
func longest(f string, aa ...string) string {
for _, s := range aa {
if len(f) < len(s) {
f = s
}
}
return f
}
// title works similarly as strings.ToTitle, with the expception
// of uppercasing only the first word in line
func title(s string) string {
split := strings.Split(s, " ")
return fmt.Sprintf("%s %s", strings.Title(split[0]), strings.Join(split[1:], " "))
}
// untitle works only on the first word in line
func untitle(s string) string {
split := strings.Split(s, " ")
first := strings.ToLower(split[0][:1]) + split[0][1:]
return fmt.Sprintf("%s %s", first, strings.Join(split[1:], " "))
}
func join(arr interface{}, sep string) (out string, err error) {
if arr == nil {
// If base is empty, nothing to do
return "", nil
} else if i, is := arr.([]string); is {
// If string slice, we are good to go
return strings.Join(i, sep), nil
} else if i, is := arr.([]interface{}); is {
// If slice of interfaces, we can try to cast them
var aux []string
aux, err = CastToStringSlice(i)
if err != nil {
return
}
return strings.Join(aux, sep), nil
} else if arr, err = toSlice(arr); err != nil {
return
}
// Make an aux string slice so the join operation can use it
stv, is := arr.([]TypedValue)
if !is {
return "", fmt.Errorf("could not cast array to string array")
}
aux := make([]string, len(stv))
for i, rv := range stv {
aux[i], err = CastToString(rv)
if err != nil {
return
}
}
return strings.Join(aux, sep), nil
}
// hasSubstring checks if a substring exists in original string
// use watchCase if need case sensitivity
func hasSubstring(s, substring string, watchCase bool) bool {
if watchCase {
return strings.Contains(s, substring)
}
return strings.Contains(strings.ToLower(s), strings.ToLower(substring))
}
// substring extracts a substring from original string
// specifying end value will not match till end of string
func substring(s string, start, end int) string {
if end == -1 {
end = len(s)
}
if start >= len(s) {
return ""
}
end++
if end > len(s) {
end = len(s)
}
return s[start:end]
}
// shorten trims by whole words or only chars by
// the specified amount
func shorten(s, t string, count int) string {
var joined string
if t == "char" {
if count >= len(s) {
return s
}
joined = s[:count]
} else {
fields := strings.Fields(s)
if count >= len(fields) {
return s
}
joined = strings.Join(fields[:count], " ")
}
reg, err := regexp.Compile("[^a-zA-Z0-9]$")
if err != nil {
return ""
}
joined = reg.ReplaceAllString(joined, "")
return joined + " …"
}
func camelize(s string) string {
return untitle(strings.Replace(strings.Title(s), " ", "", -1))
}
func snakify(s string) string {
return strings.ToLower(strings.Replace(strings.Title(s), " ", "_", -1))
}
func base64encode(s interface{}) string {
var (
buf bytes.Buffer
)
switch s.(type) {
case string:
s = []byte(s.(string))
case []byte:
s = s.([]byte)
case io.Reader:
buf := new(bytes.Buffer)
buf.ReadFrom(s.(io.Reader))
s = buf.Bytes()
default:
return ""
}
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
if _, err := encoder.Write(s.([]byte)); err != nil {
return ""
}
if err := encoder.Close(); err != nil {
return ""
}
return buf.String()
}
func match(s string, regex interface{}) (b bool, err error) {
var (
r *regexp.Regexp
)
switch v := regex.(type) {
case *regexp.Regexp:
r = v
case string:
if r, err = regexp.Compile(v); err != nil {
return
}
}
b = r.MatchString(s)
return
}

View File

@@ -0,0 +1,205 @@
package expr
func Example_trim() {
eval(`trim(" foo ")`, nil)
// output:
// foo
}
func Example_trimLeft() {
eval(`trimLeft(" foo ", " ")`, nil)
// output:
// foo
}
func Example_trimRight() {
eval(`trimRight(" foo ", " ")`, nil)
// output:
// foo
}
func Example_toLower() {
eval(`toLower("FOO")`, nil)
// output:
// foo
}
func Example_toUpper() {
eval(`toUpper("foo")`, nil)
// output:
// FOO
}
func Example_shortest() {
eval(`shortest("foo", "foobar")`, nil)
// output:
// foo
}
func Example_longest() {
eval(`longest("foo", "foobar")`, nil)
// output:
// foobar
}
func Example_title() {
eval(`title("foo bAR")`, nil)
// output:
// Foo bAR
}
func Example_untitle() {
eval(`untitle("Foo Bar")`, nil)
// output:
// foo Bar
}
func Example_repeat() {
eval(`repeat("duran ", c)`, map[string]interface{}{"c": 2})
// output:
// duran duran
}
func Example_replace_all() {
eval(`replace("foobar baz", "ba", "BA", c)`, map[string]interface{}{"c": -1})
// output:
// fooBAr BAz
}
func Example_replace_first() {
eval(`replace("foobar baz", "ba", "BA", c)`, map[string]interface{}{"c": 1})
// output:
// fooBAr baz
}
func Example_isUrl() {
eval(`isUrl("http:/example.tld")`, nil)
// output:
// false
}
func Example_isEmail() {
eval(`isUrl("example.user+valid@example.tld")`, nil)
// output:
// true
}
func Example_split() {
eval(`split("This will be split in:2-parts", ":")`, nil)
// output:
// [This will be split in 2-parts]
}
func Example_join() {
eval(`join(exploded, ",")`, map[string][]string{"exploded": {"One", "two", "three"}})
// output:
// One,two,three
}
func Example_hasSubstring_caseS() {
eval(`hasSubstring("foo BAR", "o b", true)`, nil)
// output:
// false
}
func Example_hasSubstring_caseI() {
eval(`hasSubstring("foo BAR", "o b", false)`, nil)
// output:
// true
}
func Example_hasSuffix() {
eval(`hasSuffix("foo bar", "ar")`, nil)
// output:
// true
}
func Example_hasPrefix() {
eval(`hasPrefix("foo bar", "foo ")`, nil)
// output:
// true
}
func Example_shorten_word() {
eval(`shorten("foo bar 1337, this is. test one - or three", "word", c)`, map[string]int{"c": 3})
// output:
// foo bar 1337 …
}
func Example_shorten_char() {
eval(`shorten("foo bar 1337, this is. test one - or three", "char", c)`, map[string]int{"c": 22})
// output:
// foo bar 1337, this is …
}
func Example_camelize() {
eval(`camelize("foo bar baz_test")`, nil)
// output:
// fooBarBaz_test
}
func Example_snakify() {
eval(`snakify("foo bar baz_test")`, nil)
// output:
// foo_bar_baz_test
}
func Example_substring() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": -1})
// output:
// o bar baz_test
}
func Example_substring_highStart() {
eval(`substring("foo", start, end)`, map[string]interface{}{"start": 3, "end": -1})
// output:
//
}
func Example_substring_withEnd() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": 4})
// output:
// o b
}
func Example_substring_endOverflow() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": 100})
// output:
// o bar baz_test
}
func Example_match() {
eval(`match("foo bar baz_test", "^foo\\s.*$")`, nil)
// output:
// true
}
func Example_base64encode() {
eval(`base64encode("foo bar baz_test")`, nil)
// output:
// Zm9vIGJhciBiYXpfdGVzdA==
}

View File

@@ -0,0 +1,186 @@
package expr
import (
"fmt"
"time"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/gvalfnc"
)
func TimeFunctions() []gval.Language {
return []gval.Language{
gval.Function("earliest", earliest),
gval.Function("latest", latest),
gval.Function("parseISOTime", parseISOTime),
gval.Function("modTime", modTime),
gval.Function("modDate", modDate),
gval.Function("modWeek", modWeek),
gval.Function("modMonth", modMonth),
gval.Function("modYear", modYear),
gval.Function("parseDuration", time.ParseDuration),
gval.Function("strftime", gvalfnc.StrfTime),
gval.Function("isLeapYear", isLeapYear),
gval.Function("now", now),
gval.Function("isWeekDay", isWeekDay),
gval.Function("sub", sub),
}
}
func now() time.Time {
return gvalfnc.Now()
}
func isLeapYear(base interface{}) (bool, error) {
t, _, err := gvalfnc.PrepMod(base, 0)
if err != nil {
return false, err
}
return time.Date(t.Year(), time.December, 31, 0, 0, 0, 0, time.Local).YearDay() == 366, nil
}
func isLeapDay(base interface{}) (bool, error) {
t, _, err := gvalfnc.PrepMod(base, 0)
if err != nil {
return false, err
}
return t.Day() == 29 && t.Month() == 2, nil
}
func isWeekDay(base interface{}) (bool, error) {
t, _, err := gvalfnc.PrepMod(base, 0)
if err != nil {
return false, err
}
return time.Sunday < t.Weekday() && t.Weekday() < time.Saturday, nil
}
func earliest(f interface{}, aa ...interface{}) (*time.Time, error) {
t, _, err := gvalfnc.PrepMod(f, 0)
if err != nil {
return nil, err
}
for _, a := range aa {
s, _, err := gvalfnc.PrepMod(a, 0)
if err != nil {
return nil, err
}
if (*s).Before(*t) {
t = s
}
}
return t, nil
}
func latest(f interface{}, aa ...interface{}) (*time.Time, error) {
t, _, err := gvalfnc.PrepMod(f, 0)
if err != nil {
return nil, err
}
for _, a := range aa {
s, _, err := gvalfnc.PrepMod(a, 0)
if err != nil {
return nil, err
}
if s.After(*t) {
t = s
}
}
return t, nil
}
func parseISOTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
func modTime(base interface{}, mod interface{}) (*time.Time, error) {
var (
err error
d time.Duration
t *time.Time
)
t, _, err = gvalfnc.PrepMod(base, 0)
if err != nil {
return nil, err
}
switch c := mod.(type) {
case time.Duration:
d = c
case string:
d, err = time.ParseDuration(c)
}
if err != nil {
return t, err
}
tmp := t.Add(d)
return &tmp, nil
}
func modDate(base interface{}, mod interface{}) (*time.Time, error) {
t, m, err := gvalfnc.PrepMod(base, mod)
if err != nil {
return nil, err
}
tmp := t.AddDate(0, 0, m)
return &tmp, nil
}
func modWeek(base interface{}, mod interface{}) (*time.Time, error) {
t, m, err := gvalfnc.PrepMod(base, mod)
if err != nil {
return nil, err
}
tmp := t.AddDate(0, 0, 7*m)
return &tmp, nil
}
func modMonth(base interface{}, mod interface{}) (*time.Time, error) {
t, m, err := gvalfnc.PrepMod(base, mod)
if err != nil {
return nil, err
}
tmp := t.AddDate(0, m, 0)
return &tmp, nil
}
func modYear(base interface{}, mod interface{}) (*time.Time, error) {
t, m, err := gvalfnc.PrepMod(base, mod)
if err != nil {
return nil, err
}
tmp := t.AddDate(m, 0, 0)
return &tmp, nil
}
// sub returns difference between two date into milliseconds
func sub(from interface{}, to interface{}) (out int64, err error) {
t1, _, err := gvalfnc.PrepMod(from, 0)
if err != nil {
return
}
t2, _, err := gvalfnc.PrepMod(to, 0)
if err != nil {
return
}
var duration time.Duration
if t1.After(*t2) {
duration = t1.Sub(*t2)
} else {
return -1, fmt.Errorf("expecting 2nd input to be less than 1st input")
}
return duration.Milliseconds(), nil
}

View File

@@ -0,0 +1,150 @@
package expr
import (
"time"
)
var (
// human genome project end
hgp = time.Date(2003, 4, 14, 0, 0, 0, 0, time.UTC)
// world's first antibiotic
wfa = time.Date(1928, 9, 128, 0, 0, 0, 0, time.UTC)
// groundhog day
ghd = time.Date(1993, 2, 2, 6, 0, 0, 0, time.FixedZone("", -5*60*60))
exampleTimeParams = map[string]interface{}{
"hgp": hgp,
"wfa": wfa,
"ghd": ghd,
"now": now,
}
)
func Example_strftime() {
eval(`strftime(ghd, "%Y-%m-%dT%H:%M:%S")`, exampleTimeParams)
// output:
// 1993-02-02T06:00:00
}
func Example_strftimeWithModTime() {
eval(`strftime(modTime(ghd, "+30m"), "%Y-%m-%dT%H:%M:%S")`, exampleTimeParams)
// output:
// 1993-02-02T06:30:00
}
func Example_strftimeWithStringValue() {
eval(`strftime("1993-02-02T06:30:00", "%Y-%m-%dT%H:%M:%S")`, exampleTimeParams)
// output:
// 1993-02-02T06:30:00
}
func Example_parseISODate() {
eval(`date("1993-02-02T06:00:00-05:00")`, nil)
// output:
// 1993-02-02 06:00:00 -0500 -0500
}
func Example_parseDate() {
eval(`date("1993-02-02 06:00:00+01:10")`, nil)
// output:
// 1993-02-02 06:00:00 +0110 +0110
}
func Example_parseDuration() {
eval(`parseDuration("2h")`, nil)
// output:
// 2h0m0s
}
func Example_earliest() {
eval(`earliest(hgp, wfa)`, exampleTimeParams)
// output:
// 1929-01-06 00:00:00 +0000 UTC
}
func Example_latest() {
eval(`latest(ghd, wfa)`, exampleTimeParams)
// output:
// 1993-02-02 06:00:00 -0500 -0500
}
func Example_isLeapYear() {
eval(`isLeapYear(ghd)`, exampleTimeParams)
// output:
// false
}
func Example_isWeekDay() {
eval(`isWeekDay(ghd)`, exampleTimeParams)
// output:
// true
}
func Example_modTime() {
eval(`modTime(ghd, "+30m")`, exampleTimeParams)
// output:
// 1993-02-02 06:30:00 -0500 -0500
}
func Example_modWeek() {
eval(`modWeek(ghd, "1")`, exampleTimeParams)
// output:
// 1993-02-09 06:00:00 -0500 -0500
}
func Example_modDate() {
eval(`modDate(ghd, "5")`, exampleTimeParams)
// output:
// 1993-02-07 06:00:00 -0500 -0500
}
func Example_modMonth() {
eval(`modMonth(ghd, "1")`, exampleTimeParams)
// output:
// 1993-03-02 06:00:00 -0500 -0500
}
func Example_modYear() {
eval(`modYear(ghd, "5")`, exampleTimeParams)
// output:
// 1998-02-02 06:00:00 -0500 -0500
}
func Example_sub() {
eval(`sub(hgp, ghd)`, exampleTimeParams)
// output:
// 321627600000
}
func Example_subErrorCheck() {
eval(`sub(ghd, hgp)`, exampleTimeParams)
// output:
// error: can not evaluate sub(ghd, hgp): expecting 2nd input to be less than 1st input
}
func Example_unix() {
eval(`strftime(ghd, "%L")`, exampleTimeParams)
// output:
// 728650800
}

128
server/pkg/expr/parser.go Normal file
View File

@@ -0,0 +1,128 @@
package expr
import (
"context"
"fmt"
"github.com/PaesslerAG/gval"
)
type (
Parsable interface {
Parse(string) (Evaluable, error)
ParseEvaluators(ee ...Evaluator) error
}
Evaluable interface {
Eval(context.Context, *Vars) (interface{}, error)
Test(context.Context, *Vars) (bool, error)
}
Evaluator interface {
GetExpr() string
SetEval(eval Evaluable)
}
gvalParser struct {
lang gval.Language
}
gvalEval struct {
evaluable gval.Evaluable
}
)
var (
parser gval.Language
initialized = false
)
func Init(ext ...func() []gval.Language) {
aux := make([]gval.Language, 0, len(ext))
for _, e := range ext {
aux = append(aux, e()...)
}
initialized = true
parser = gval.Full(aux...)
}
// Parser returns global parser instance
func Parser() gval.Language {
if !initialized {
Init(AllFunctions)
}
return parser
}
func NewParser() Parsable {
return NewGvalParser()
}
func NewGvalParser() *gvalParser {
return &gvalParser{lang: Parser()}
}
func (p *gvalParser) Parse(expr string) (Evaluable, error) {
var (
ge = &gvalEval{}
err error
)
if ge.evaluable, err = p.lang.NewEvaluable(expr); err != nil {
return nil, err
}
return ge, err
}
func (p *gvalParser) ParseEvaluators(ee ...Evaluator) error {
for _, e := range ee {
evaluable, err := p.Parse(e.GetExpr())
if err != nil {
return err
}
e.SetEval(evaluable)
}
return nil
}
func (e *gvalEval) Eval(ctx context.Context, scope *Vars) (interface{}, error) {
return e.evaluable(ctx, scope)
}
func (e *gvalEval) Test(ctx context.Context, scope *Vars) (bool, error) {
r, err := e.evaluable(ctx, scope)
if err != nil {
return false, err
}
return !isEmpty(r), nil
}
func AllFunctions() []gval.Language {
ff := make([]gval.Language, 0, 100)
ff = append(ff, GenericFunctions()...)
ff = append(ff, StringFunctions()...)
ff = append(ff, JsonFunctions()...)
ff = append(ff, NumericFunctions()...)
ff = append(ff, TimeFunctions()...)
ff = append(ff, ArrayFunctions()...)
ff = append(ff, KvFunctions()...)
return ff
}
// utility function for examples
func eval(e string, p interface{}) {
result, err := Parser().Evaluate(e, p)
if err != nil {
fmt.Printf("error: %v", err)
} else {
fmt.Printf("%+v", UntypedValue(result))
}
}

View File

@@ -0,0 +1,110 @@
package expr
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func Example_simpleExpression() {
eval(`40 + 2`, nil)
// output:
// 42
}
func TestParser(t *testing.T) {
var (
req = require.New(t)
ctx = context.Background()
p = Parser()
e, err = p.NewEvaluable("0 == 0")
result bool
)
req.NoError(err)
result, err = e.EvalBool(ctx, nil)
req.NoError(err)
req.True(result)
}
func TestGvalParser(t *testing.T) {
var (
req = require.New(t)
ctx = context.Background()
p = NewGvalParser()
vv, err = NewVars(map[string]interface{}{
"vars": &Vars{},
"key": "foo",
"value": Must(NewString("foo")),
})
result interface{}
)
req.NoError(err)
pp, err := p.Parse("toJSON(set(vars, key, value))")
req.NoError(err)
result, err = pp.Eval(ctx, vv)
req.NoError(err)
req.Equal("{\"foo\":{\"@value\":\"foo\",\"@type\":\"String\"}}", result)
}
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/pkg/expr
// cpu: Apple M3 Pro
// BenchmarkParsing-12 295779 4052 ns/op 4952 B/op 133 allocs/op
func BenchmarkParsing(b *testing.B) {
expr := `(1 + length(trim(" asdf asdf asdf ")) * 71 + min(3, 7777777)) + (1 + length(trim(" asdf asdf asdf ")) * 71 + min(3, 7777777))`
p := NewGvalParser()
for n := 0; n < b.N; n++ {
e, err := p.Parse(expr)
if err != nil {
panic(err)
}
_ = e
}
}
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/pkg/expr
// cpu: Apple M3 Pro
// BenchmarkEval_short-12 886696 1344 ns/op 544 B/op 14 allocs/op
// BenchmarkEval_med-12 299616 4020 ns/op 1280 B/op 36 allocs/op
// BenchmarkEval_long-12 146112 7878 ns/op 2488 B/op 71 allocs/op
func BenchmarkEval_short(b *testing.B) {
expr := `min(3, 7777777)`
benchmarkEval(b, expr)
}
func BenchmarkEval_med(b *testing.B) {
expr := `1 + length(trim(" asdf asdf asdf ")) * 71 + min(3, 7777777)`
benchmarkEval(b, expr)
}
func BenchmarkEval_long(b *testing.B) {
expr := `(1 + length(trim(" asdf asdf asdf ")) * 71 + min(3, 7777777)) + (1 + length(trim(" asdf asdf asdf ")) * 71 + min(3, 7777777))`
benchmarkEval(b, expr)
}
func benchmarkEval(b *testing.B, expr string) {
e, err := NewGvalParser().Parse(expr)
if err != nil {
panic(err)
}
ctx := context.Background()
for n := 0; n < b.N; n++ {
_, err = e.Eval(ctx, EmptyVars())
// _ = err
if err != nil {
panic(err)
}
}
}

114
server/pkg/expr/path.go Normal file
View File

@@ -0,0 +1,114 @@
package expr
type (
exprPath struct {
path string
i int
isLast bool
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 (p *exprPath) String() string {
return p.path
}
func (p *exprPath) More() bool {
return p.start < len(p.path)
}
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 {
var rest string
if p.end+1 < len(p.path) {
rest = p.path[p.end:]
}
// @todo this is fugly but it'll do the trick for now
// Clean it up please :)
if len(rest) > 0 && (rest[0] == '.' || rest[0] == ']') {
rest = rest[1:]
}
if len(rest) > 0 && (rest[0] == '.' || rest[0] == ']') {
rest = rest[1:]
}
return rest
}
func (p *exprPath) Next() (err error) {
if !p.More() {
return nil
}
if p.end > 0 {
p.start = p.end + 1
}
var ()
p.start, p.end, p.isLast, err = nxtRange(p.path, p.start)
if err != nil {
return err
}
p.i++
return nil
}
func nxtRange(path string, start int) (startOut, end int, isLast bool, err error) {
startOut = start
for i := start; i < len(path); i++ {
switch path[i] {
// This thing concludes the prev ident
case '.', '[':
if i == len(path)-1 {
return startOut, -1, false, invalidPathErr
}
if i > 0 {
if path[i-1] == ']' {
startOut++
continue
}
}
return startOut, i, false, nil
case ']':
// If we're at the end, that's that
if i == len(path)-1 {
return startOut, i, true, nil
}
if path[i+1] != '.' && path[i+1] != '[' {
return startOut, -1, false, invalidPathErr
} else {
return startOut, i, false, nil
}
}
}
return startOut, len(path), true, nil
}

View File

@@ -0,0 +1,153 @@
package expr
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNxtRange(t *testing.T) {
tcc := []struct {
path string
start int
expEnd int
expErr bool
}{{
path: "a",
start: 0,
expEnd: 1,
}, {
path: "bcd",
start: 0,
expEnd: 3,
}, {
path: "bcd[e]",
start: 0,
expEnd: 3,
}, {
path: "bcd.e",
start: 0,
expEnd: 3,
}, {
path: "bcd.",
start: 0,
expErr: true,
}, {
path: "bcd[",
start: 0,
expErr: true,
}, {
path: "bcd.e",
start: 4,
expEnd: 5,
}}
for _, c := range tcc {
t.Run(c.path[c.start:], func(t *testing.T) {
_, o, _, err := nxtRange(c.path, c.start)
if c.expErr {
require.Error(t, err)
return
}
require.Equal(t, c.expEnd, o)
})
}
}
func TestPath(t *testing.T) {
tcc := []struct {
path string
expBits []string
expRests []string
expErr bool
}{{
path: "a",
expBits: []string{"a"},
expRests: []string{""},
}, {
path: "a.b",
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]",
expBits: []string{"a", "b", "c"},
expRests: []string{"[b][c]", "[c]", ""},
}, {
path: "aa.b[c][d].e.f[g][0]",
expBits: []string{"aa", "b", "c", "d", "e", "f", "g", "0"},
expRests: []string{"b[c][d].e.f[g][0]", "[c][d].e.f[g][0]", "[d].e.f[g][0]", "e.f[g][0]", "f[g][0]", "[g][0]", "[0]", ""},
},
}
for _, c := range tcc {
t.Run(c.path, func(t *testing.T) {
pp := Path(c.path)
var err error
i := -1
for {
i++
err = pp.Next()
require.NoError(t, err)
if !pp.More() {
break
}
require.Equal(t, c.expBits[i], pp.Get())
require.Equal(t, c.expRests[i], pp.Rest())
}
require.Equal(t, len(c.expBits), i)
// for _, b := range c.expBits {
// require.Equal(t, b, pp.Get())
// }
// pp, err = pp.Next()
// require.NoError(t, err)
// require.False(t, pp.More())
})
}
}
func BenchmarkPath(b *testing.B) {
path := "aa.b[c][d].e.f[g][0]"
b.ResetTimer()
for n := 0; n < b.N; n++ {
pp := Path(path)
for {
pp.Next()
if !pp.More() {
break
}
}
}
}

82
server/pkg/expr/values.go Normal file
View File

@@ -0,0 +1,82 @@
package expr
import (
"reflect"
)
type (
Type interface {
Type() string
Cast(interface{}) (TypedValue, error)
}
TypedValue interface {
Type
Assigner
Clone() (TypedValue, error)
Get() interface{}
}
Comparable interface {
Compare(TypedValue) (int, error)
}
typedValueWrap struct {
Value interface{} `json:"@value"`
Type string `json:"@type"`
}
Assigner interface {
Assign(interface{}) error
}
FieldSelector interface {
Has(k string) bool
Select(k string) (TypedValue, error)
}
FieldAssigner interface {
AssignFieldValue(string, TypedValue) error
}
DeepFieldAssigner interface {
AssignFieldValue(Pather, TypedValue) error
}
// FlatStore marks types whose keys must never be path-split (e.g. KV, KVV).
// Assign will use the full key string as-is instead of traversing dot-separated segments.
FlatStore interface {
isFlatStore()
}
Iterator interface {
Each(func(k string, v TypedValue) error) error
}
TypeValueDecoder interface {
Decode(dst reflect.Value) error
}
Dict interface {
Dict() map[string]interface{}
}
Slice interface {
Slice() []interface{}
}
)
func UntypedValue(val interface{}) interface{} {
if tv, is := val.(TypedValue); is {
return tv.Get()
}
return val
}
func Must(v TypedValue, err error) TypedValue {
if err != nil {
panic(err)
}
return v
}

615
server/pkg/expr/vars.go Normal file
View File

@@ -0,0 +1,615 @@
package expr
import (
"context"
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/spf13/cast"
)
func (t *Vars) Len() (out int) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
return len(t.value)
}
func (t *Vars) Select(k string) (out TypedValue, err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
if v, is := t.value[k]; is {
return v, nil
} else {
return nil, errors.NotFound("no such key '%s'", k)
}
}
func (t *Vars) AssignFieldValue(key string, val TypedValue) (err error) {
if t == nil {
return
}
t.mux.Lock()
defer t.mux.Unlock()
if t.value == nil {
t.value = make(map[string]TypedValue)
}
t.value[key] = val
return
}
func (t *Vars) ResolveTypes(res func(typ string) Type) (err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
for k, v := range t.value {
if u, is := v.(*Unresolved); is {
if res(u.Type()) == nil {
return errors.NotFound("failed to resolve unknown or unregistered type %q on %q", u.Type(), k)
}
t.value[k], err = res(u.Type()).Cast(t.value[k])
if err != nil {
return fmt.Errorf("failed to resolve: %w", err)
}
}
if r, is := t.value[k].(resolvableType); is {
if err = r.ResolveTypes(res); err != nil {
return
}
}
}
return
}
// Merge combines the given Vars(es) into Vars
// NOTE: It will return CLONE of the original Vars, if it's called without any parameters
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 {
t.mux.RLock()
defer t.mux.RUnlock()
nn = append([]Iterator{t}, nn...)
}
var (
out = &Vars{value: make(map[string]TypedValue, 4)}
)
for _, i := range nn {
_ = i.Each(func(k string, v TypedValue) error {
out.value[k] = v
return nil
})
}
return out
}
// Copy takes base variables and copies all to dst
func (t *Vars) Copy(dst *Vars, kk ...string) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
if t == nil {
return
}
if dst.value == nil {
dst.value = make(map[string]TypedValue)
}
for _, k := range kk {
dst.value[k] = t.value[k]
}
}
// Has returns true key is present
func (t *Vars) Has(key string) bool {
return t.HasAll(key)
}
// HasAll returns true if all keys are present
func (t *Vars) HasAll(key string, kk ...string) bool {
if t == nil {
return false
}
t.mux.RLock()
defer t.mux.RUnlock()
if t == nil {
return false
}
for _, key = range append([]string{key}, kk...) {
if _, has := t.value[key]; !has {
return false
}
}
return true
}
// HasAny returns true if all keys are present
func (t *Vars) HasAny(key string, kk ...string) bool {
if t == nil {
return false
}
for _, key = range append([]string{key}, kk...) {
if _, has := t.value[key]; has {
return true
}
}
return false
}
var _ gval.Selector = &Vars{}
func (t *Vars) Dict() (out map[string]interface{}) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
out = make(map[string]interface{})
for k, v := range t.value {
switch v := v.(type) {
case Dict:
out[k] = v.Dict()
case Slice:
out[k] = v.Slice()
case TypedValue:
tmp := v.Get()
if d, is := tmp.(Dict); is {
out[k] = d.Dict()
} else {
out[k] = tmp
}
default:
out[k] = v
}
}
return
}
func (t *Vars) Decode(dst interface{}) (err error) {
if t == nil {
return nil
}
t.mux.RLock()
defer t.mux.RUnlock()
dstRef := reflect.ValueOf(dst)
if dstRef.Kind() != reflect.Ptr {
return fmt.Errorf("expecting a pointer, not a value")
}
if dstRef.IsNil() {
return fmt.Errorf("nil pointer passed")
}
dstRef = dstRef.Elem()
for i := 0; i < dstRef.NumField(); i++ {
var (
value TypedValue
has bool
ftyp = dstRef.Type().Field(i)
)
keyName := ftyp.Tag.Get("var")
if keyName == "" {
keyName = strings.ToLower(ftyp.Name[:1]) + ftyp.Name[1:]
}
value, has = t.value[keyName]
if !has {
continue
}
if tvd, is := value.(TypeValueDecoder); is {
if err = tvd.Decode(dstRef.Field(i)); err != nil {
return
}
} else if err = decode(dstRef.Field(i), value); err != nil {
return fmt.Errorf("failed to decode value to field %s: %w", ftyp.Name, err)
}
}
return
}
func (t *Vars) Scan(src any) error { return sql.ParseJSON(src, t) }
func (t *Vars) Value() (driver.Value, error) {
if t != nil {
t.mux.RLock()
defer t.mux.RUnlock()
}
return json.Marshal(t)
}
func (t *Vars) SelectGVal(_ context.Context, k string) (out interface{}, err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
out, err = t.Select(k)
switch c := out.(type) {
case gval.Selector:
return c, err
default:
return UntypedValue(out), err
}
}
// UnmarshalJSON unmarshal JSON value into Vars
func (t *Vars) UnmarshalJSON(in []byte) (err error) {
if t == nil {
return
}
t.mux.Lock()
defer t.mux.Unlock()
var (
aux = make(map[string]*typedValueWrap)
)
if t.value == nil {
t.value = make(map[string]TypedValue)
}
if len(in) == 0 {
return nil
}
if err = json.Unmarshal(in, &aux); err != nil {
return
}
for k, v := range aux {
if t.value[k], err = NewUnresolved(v.Type, v.Value); err != nil {
return
}
}
return
}
func (t *Vars) Each(fn func(k string, v TypedValue) error) (err error) {
if t == nil {
return nil
}
t.mux.RLock()
defer t.mux.RUnlock()
if t.value == nil {
return
}
for k, v := range t.value {
if err = fn(k, v); err != nil {
return
}
}
return
}
// Set or update the specific key value in Vars
func (t *Vars) Set(k string, v interface{}) (err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
if t.value == nil {
t.value = make(map[string]TypedValue)
}
t.value[k], err = Typify(v)
return
}
// MarshalJSON returns JSON encoding of expression
func (t *Vars) MarshalJSON() (out []byte, err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
aux := make(map[string]*typedValueWrap)
for k, v := range t.value {
if v == nil {
continue
}
aux[k] = &typedValueWrap{Type: v.Type()}
if _, is := v.(json.Marshaler); is {
aux[k].Value = v
} else {
aux[k].Value = v.Get()
}
}
return json.Marshal(aux)
}
func decode(dst reflect.Value, src TypedValue) (err error) {
defer func() {
r := recover()
if r == nil {
return
}
switch r := r.(type) {
case error:
err = r
default:
err = fmt.Errorf("%v", r)
}
}()
if dst.Kind() == reflect.Interface && reflect.ValueOf(src).Type().Implements(dst.Type()) {
dst.Set(reflect.ValueOf(src))
return
}
if reflect.ValueOf(src).Type().ConvertibleTo(dst.Type()) {
dst.Set(reflect.ValueOf(src))
return
}
raw := UntypedValue(src)
// Optimistically try to decode source to destination by comparing (internal) value type for destination
if reflect.ValueOf(raw).Type().ConvertibleTo(dst.Type()) {
dst.Set(reflect.ValueOf(raw))
return
}
var (
vBool bool
vInt64 int64
vUint64 uint64
vFloat64 float64
vString string
)
switch dst.Kind() {
case reflect.Bool:
if vBool, err = cast.ToBoolE(raw); err == nil {
dst.SetBool(vBool)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if vInt64, err = cast.ToInt64E(raw); err == nil {
dst.SetInt(vInt64)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if vUint64, err = cast.ToUint64E(raw); err == nil {
dst.SetUint(vUint64)
}
case reflect.Float32, reflect.Float64:
if vFloat64, err = cast.ToFloat64E(raw); err == nil {
dst.SetFloat(vFloat64)
}
case reflect.String:
if vString, err = cast.ToStringE(raw); err == nil {
dst.SetString(vString)
}
case reflect.Map:
dst.Set(reflect.ValueOf(src.Get()))
//case reflect.Interface:
// dst.Set(reflect.ValueOf(raw))
default:
return fmt.Errorf("failed to cast %T to %s", raw, dst.Kind())
}
if err != nil {
return fmt.Errorf("failed to cast %T to %s: %w", raw, dst.Kind(), err)
}
return nil
}
func CastToMeta(val interface{}) (out map[string]any, err error) {
val = UntypedValue(val)
if val == nil {
return make(map[string]any), nil
}
switch c := val.(type) {
case *Vars:
c.mux.RLock()
defer c.mux.RUnlock()
out = make(map[string]any)
for k, v := range c.value {
out[k] = v.Get()
}
return
case map[string]string:
out = make(map[string]any)
for k, v := range c {
out[k] = v
}
return
case map[string][]string:
out = make(map[string]any)
for k, v := range c {
out[k] = v
}
return
case KV:
out = make(map[string]any)
for k, v := range c.value {
out[k] = v
}
return
case KVV:
out = make(map[string]any)
for k, v := range c.value {
out[k] = v
}
return
case map[string]any:
return c, nil
}
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
func CastToVars(val interface{}) (out map[string]TypedValue, err error) {
val = UntypedValue(val)
if val == nil {
return make(map[string]TypedValue), nil
}
switch c := val.(type) {
case *Vars:
c.mux.RLock()
defer c.mux.RUnlock()
return c.value, nil
case map[string]TypedValue:
return c, nil
case map[string]interface{}:
out = make(map[string]TypedValue)
for k, v := range c {
out[k], err = Typify(v)
if err != nil {
return
}
}
return
}
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
// Filter take keys returns Vars with only those key value pair
func (t *Vars) Filter(keys ...string) (out TypedValue, err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
if t.value == nil {
return
}
vars := EmptyVars()
for _, k := range keys {
_, has := t.value[k]
if has {
vars.value[k] = t.value[k]
}
}
return vars, nil
}
// Delete take keys returns Vars without those key value pair
func (t *Vars) Delete(keys ...string) (out TypedValue, err error) {
if t == nil {
return
}
t.mux.RLock()
defer t.mux.RUnlock()
if t.value == nil {
return
}
// get cloned Vars
out, err = t.Merge()
if err != nil {
return
}
vars := out.(*Vars)
// Delete key from t.value if exist
for _, k := range keys {
delete(vars.value, k)
}
return vars, nil
}

View File

@@ -0,0 +1,397 @@
package expr
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
// extract typed-value
func Example_set_vars() {
var (
p = map[string]interface{}{
"vars": &Vars{},
"key": "foo",
"value": &String{value: "foo"},
}
)
eval(`toJSON(set(vars, key, value))`, p)
// output:
// {"foo":{"@value":"foo","@type":"String"}}
}
func Example_merge_vars() {
var (
p = map[string]interface{}{
"vars": &Vars{},
"foo": &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}},
"bar": &Vars{value: map[string]TypedValue{
"k2": &String{value: "v2"},
}},
}
)
eval(`toJSON(merge(foo, bar))`, p)
// output:
// {"k1":{"@value":"v1","@type":"String"},"k2":{"@value":"v2","@type":"String"}}
}
func Example_filter_vars() {
var (
p = map[string]interface{}{
"vars": &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k5": &String{value: "v5"},
}},
"key1": "k1",
"key2": "k3",
"key3": "k5",
}
)
eval(`toJSON(filter(vars, key1, key2, key3))`, p)
// output:
// {"k1":{"@value":"v1","@type":"String"},"k5":{"@value":"v5","@type":"String"}}
}
func Example_omit_vars() {
var (
p = map[string]interface{}{
"vars": &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
"k3": &String{value: "v3"},
}},
"key1": "k1",
"key2": "k3",
}
)
eval(`toJSON(omit(vars, key1, key2))`, p)
// output:
// {"k2":{"@value":"v2","@type":"String"}}
}
func TestVars_Decode(t *testing.T) {
t.Run("mix", func(t *testing.T) {
var (
req = require.New(t)
dst = &struct {
Int int64
Uint64 uint64
String string `var:"STRING"`
RawString string `var:"rawString"`
Bool bool
Unexisting byte
}{}
vars, _ = NewVars(map[string]interface{}{
"int": Must(NewInteger(42)),
"STRING": Must(NewString("foo")),
"bool": Must(NewBoolean(true)),
"missing": Must(NewBoolean(true)),
})
)
req.NoError(vars.Decode(dst))
req.Equal(int64(42), dst.Int)
req.Equal("foo", dst.String)
req.Equal(true, dst.Bool)
req.Empty(dst.Unexisting)
})
t.Run("interfaces", func(t *testing.T) {
var (
req = require.New(t)
dst = &struct {
IString interface{} `var:"iString"`
IInteger interface{} `var:"iInteger"`
IBool interface{} `var:"iBool"`
}{}
vars, _ = NewVars(map[string]interface{}{
"iString": Must(NewString("foo")),
"iInteger": Must(NewInteger(42)),
"iBool": Must(NewBoolean(true)),
})
)
req.NoError(vars.Decode(dst))
})
t.Run("vars-in-vars", func(t *testing.T) {
var (
req = require.New(t)
dst = &struct {
Vars *Vars `var:"vars"`
}{}
vars, _ = NewVars(map[string]interface{}{
"vars": map[string]interface{}{"foo": Must(NewString("bar"))},
})
)
req.NoError(vars.Decode(dst))
})
t.Run("int-uint", func(t *testing.T) {
var (
req = require.New(t)
dst = &struct {
Int int64
Uint64 uint64
}{}
vars, _ = NewVars(map[string]TypedValue{
"uint64": Must(NewAny("42")),
"int": Must(NewAny("42")),
})
)
dst.Uint64 = 0
dst.Int = 0
req.NoError(vars.Decode(dst))
req.Equal(uint64(42), dst.Uint64)
req.Equal(int64(42), dst.Int)
})
}
func TestVars_Assign(t *testing.T) {
var (
req = require.New(t)
vars = &Vars{}
)
req.NoError(Assign(vars, "foo", &String{value: "foo"}))
req.NoError(Assign(vars, "vars", &Vars{}))
req.NoError(Assign(vars, "vars.foo", &String{value: "foo"}))
}
func TestVars_Set(t *testing.T) {
var (
req = require.New(t)
vars = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
}}
)
out, err := set(vars, "k1", &String{value: "v11"})
req.NoError(err)
req.Equal(&String{value: "v11"}, out.(*Vars).GetValue()["k1"])
// Making sure empty vars updates without error
vars = &Vars{}
out, err = set(vars, "k2", &Array{value: []TypedValue{&String{value: "foo"}, &String{value: "bar"}}})
req.NoError(err)
req.Equal(&Array{value: []TypedValue{&String{value: "foo"}, &String{value: "bar"}}}, out.(*Vars).GetValue()["k2"])
}
func TestVars_MergeVars(t *testing.T) {
var (
req = require.New(t)
vars Vars
foo = Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}}
bar = Vars{value: map[string]TypedValue{
"k2": &String{value: "v2"},
}}
expected = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
}}
)
out := vars.MustMerge(&foo, &bar)
req.Equal(expected, out)
}
func TestVars_Merge(t *testing.T) {
var (
req = require.New(t)
vars = &Vars{}
foo = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}}
bar = &Vars{value: map[string]TypedValue{
"k2": &String{value: "v2"},
}}
expected = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
}}
)
out, err := merge(vars, foo, bar)
req.NoError(err)
req.Equal(expected, out)
}
func TestVars_Filter(t *testing.T) {
var (
req = require.New(t)
vars = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
}}
expected = &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}}
)
out, err := filter(vars, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
}
func TestVars_Omit(t *testing.T) {
var (
req = require.New(t)
vars = Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
"k2": &String{value: "v2"},
"k3": &String{value: "v3"},
}}
expected = &Vars{value: map[string]TypedValue{
"k2": &String{value: "v2"},
}}
)
out, err := omit(&vars, "k1", "k3")
req.NoError(err)
req.Equal(expected, out)
}
func TestVarsTypeResolving(t *testing.T) {
var (
req = require.New(t)
v = &Vars{}
)
req.NoError(v.Set("unr", &Unresolved{value: "wrapped any", typ: "Any"}))
req.NoError(v.Set("any", &Any{value: "raw any"}))
req.NoError(v.ResolveTypes(func(typ string) Type { return Any{} }))
req.Equal("wrapped any", v.value["unr"].Get())
req.Equal("raw any", v.value["any"].Get())
}
func TestVars_UnmarshalJSON(t *testing.T) {
cases := []struct {
name string
json string
vars map[string]interface{}
}{
{"empty", "", make(map[string]interface{})},
{"object", "{}", make(map[string]interface{})},
{"string", `{"a":{"@value":"b"}}`, map[string]interface{}{"a": &Unresolved{value: "b"}}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var (
r = require.New(t)
unmarshaled = &Vars{}
aux, _ = NewVars(c.vars)
)
r.NoError(unmarshaled.UnmarshalJSON([]byte(c.json)))
r.Equal(aux, unmarshaled)
})
}
}
func TestVars_MarshalJSON(t *testing.T) {
cases := []struct {
name string
json string
vars map[string]interface{}
}{
{"empty", "{}", nil},
{"string", `{"a":{"@value":"b","@type":"String"}}`, map[string]interface{}{"a": &String{value: "b"}}},
{"array",
`{"arr":{"@value":[{"@value":"foo","@type":"String"},{"@value":"bar","@type":"String"}],"@type":"Array"}}`,
map[string]interface{}{"arr": &Array{value: []TypedValue{&String{value: "foo"}, &String{value: "bar"}}}}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var (
r = require.New(t)
aux, _ = NewVars(c.vars)
)
j, err := json.Marshal(aux)
r.NoError(err)
r.Equal(c.json, string(j))
})
}
}
func TestVars_Dict(t *testing.T) {
cases := []struct {
name string
input *Vars
expected map[string]any
}{
{
name: "empty",
input: nil,
expected: nil,
},
{
name: "string",
input: &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}},
expected: map[string]any{"k1": "v1"},
},
{
name: "string",
input: &Vars{value: map[string]TypedValue{
"k1": &String{value: "v1"},
}},
expected: map[string]any{"k1": "v1"},
},
{
name: "array",
input: &Vars{value: map[string]TypedValue{
"arr": &Array{value: []TypedValue{&String{value: "foo"}}},
}},
expected: map[string]any{
"arr": []any{"foo"},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var (
r = require.New(t)
out = c.input.Dict()
)
r.Equal(c.expected, out)
})
}
}