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
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:
147
server/pkg/expr/expr.go
Normal file
147
server/pkg/expr/expr.go
Normal 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
|
||||
}
|
||||
93
server/pkg/expr/expr_test.go
Normal file
93
server/pkg/expr/expr_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
56
server/pkg/expr/expr_type_comparators.go
Normal file
56
server/pkg/expr/expr_type_comparators.go
Normal 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
1281
server/pkg/expr/expr_types.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
1211
server/pkg/expr/expr_types.go
Normal file
1211
server/pkg/expr/expr_types.go
Normal file
File diff suppressed because it is too large
Load Diff
99
server/pkg/expr/expr_types.yaml
Normal file
99
server/pkg/expr/expr_types.yaml
Normal 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'
|
||||
734
server/pkg/expr/expr_types_test.go
Normal file
734
server/pkg/expr/expr_types_test.go
Normal 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
311
server/pkg/expr/func_arr.go
Normal 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
|
||||
}
|
||||
706
server/pkg/expr/func_arr_test.go
Normal file
706
server/pkg/expr/func_arr_test.go
Normal 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
|
||||
}
|
||||
136
server/pkg/expr/func_generic.go
Normal file
136
server/pkg/expr/func_generic.go
Normal 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
|
||||
}
|
||||
316
server/pkg/expr/func_generic_test.go
Normal file
316
server/pkg/expr/func_generic_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
21
server/pkg/expr/func_json.go
Normal file
21
server/pkg/expr/func_json.go
Normal 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)
|
||||
}
|
||||
37
server/pkg/expr/func_json_test.go
Normal file
37
server/pkg/expr/func_json_test.go
Normal 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
162
server/pkg/expr/func_num.go
Normal 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)
|
||||
}
|
||||
148
server/pkg/expr/func_num_test.go
Normal file
148
server/pkg/expr/func_num_test.go
Normal 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
257
server/pkg/expr/func_str.go
Normal 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
|
||||
}
|
||||
205
server/pkg/expr/func_str_test.go
Normal file
205
server/pkg/expr/func_str_test.go
Normal 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==
|
||||
}
|
||||
186
server/pkg/expr/func_time.go
Normal file
186
server/pkg/expr/func_time.go
Normal 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
|
||||
}
|
||||
150
server/pkg/expr/func_time_test.go
Normal file
150
server/pkg/expr/func_time_test.go
Normal 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
128
server/pkg/expr/parser.go
Normal 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))
|
||||
}
|
||||
}
|
||||
110
server/pkg/expr/parser_test.go
Normal file
110
server/pkg/expr/parser_test.go
Normal 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
114
server/pkg/expr/path.go
Normal 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
|
||||
}
|
||||
153
server/pkg/expr/path_test.go
Normal file
153
server/pkg/expr/path_test.go
Normal 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
82
server/pkg/expr/values.go
Normal 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
615
server/pkg/expr/vars.go
Normal 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
|
||||
}
|
||||
397
server/pkg/expr/vars_test.go
Normal file
397
server/pkg/expr/vars_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user