From 61051e998ed5299bd0243057b2a11f5ce5a63c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Sun, 21 Aug 2022 16:37:18 +0200 Subject: [PATCH] Define tester & evaluator interfaces for asserting/computing attrs --- pkg/dal/runner.go | 15 +++ pkg/dal/runner_gval.go | 219 +++++++++++++++++++++++++++++++++++++++++ pkg/dal/runner_test.go | 58 +++++++++++ 3 files changed, 292 insertions(+) create mode 100644 pkg/dal/runner.go create mode 100644 pkg/dal/runner_gval.go create mode 100644 pkg/dal/runner_test.go diff --git a/pkg/dal/runner.go b/pkg/dal/runner.go new file mode 100644 index 000000000..f5e16ee17 --- /dev/null +++ b/pkg/dal/runner.go @@ -0,0 +1,15 @@ +package dal + +import ( + "context" +) + +type ( + tester interface { + Test(ctx context.Context, params any) bool + } + + evaluator interface { + Eval(ctx context.Context, params any) any + } +) diff --git a/pkg/dal/runner_gval.go b/pkg/dal/runner_gval.go new file mode 100644 index 000000000..72031766f --- /dev/null +++ b/pkg/dal/runner_gval.go @@ -0,0 +1,219 @@ +package dal + +import ( + "context" + "fmt" + "strings" + + "github.com/PaesslerAG/gval" + "github.com/cortezaproject/corteza-server/pkg/ql" +) + +type ( + runnerGval struct { + eval gval.Evaluable + } + converterGval struct { + parser *ql.Parser + } + exprHandlerGval struct { + Handler func(...string) string + } +) + +var ( + refToGvalExp = map[string]*exprHandlerGval{ + // keywords + "null": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s == null", args[0]) + }, + }, + "nnull": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s != null", args[0]) + }, + }, + + // operators + "not": { + Handler: func(args ...string) string { + return fmt.Sprintf("(! %s)", args[0]) + }, + }, + + // - bool + "and": { + Handler: func(args ...string) string { + return strings.Join(args, " && ") + }, + }, + "or": { + Handler: func(args ...string) string { + return strings.Join(args, " || ") + }, + }, + + // - comp. + "eq": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s == %s", args[0], args[1]) + }, + }, + "ne": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s != %s", args[0], args[1]) + }, + }, + "lt": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s < %s", args[0], args[1]) + }, + }, + "le": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s <= %s", args[0], args[1]) + }, + }, + "gt": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s > %s", args[0], args[1]) + }, + }, + "ge": { + //Handler: makeGenericCompHandler(">="), + Handler: func(args ...string) string { + return fmt.Sprintf("%s >= %s", args[0], args[1]) + }, + }, + + // - math + "add": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s + %s", args[0], args[1]) + }, + }, + "sub": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s - %s", args[0], args[1]) + }, + }, + "mult": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s * %s", args[0], args[1]) + }, + }, + "div": { + Handler: func(args ...string) string { + return fmt.Sprintf("%s / %s", args[0], args[1]) + }, + }, + + "group": { + Handler: func(args ...string) string { + return fmt.Sprintf("(%s)", args[0]) + }, + }, + } +) + +// newConverterGval initializes a new gval exp. converter +func newConverterGval() converterGval { + return converterGval{ + parser: ql.NewParser(), + } +} + +// newRunnerGval initializes a new gval exp. runner from the provided expression +func newRunnerGval(expr string) (out *runnerGval, err error) { + out = &runnerGval{} + + c := newConverterGval() + n, err := c.Parse(expr) + if err != nil { + return nil, err + } + + expr, err = c.Convert(n) + if err != nil { + return nil, err + } + + // @todo add all those extra functions the expr package uses? + // Potentially not ok since we allow a subset of operations (for compatability) + out.eval, err = gval.Full().NewEvaluable(expr) + return +} + +// newRunnerGvalParsed initializes a new gval exp. runner from the pre-parsed expression +func newRunnerGvalParsed(n *ql.ASTNode) (out *runnerGval, err error) { + out = &runnerGval{} + c := newConverterGval() + + expr, err := c.Convert(n) + if err != nil { + return + } + + // @todo add all those extra functions the expr package uses? + // Potentially not ok since we allow a subset of operations (for compatability) + out.eval, err = gval.Full().NewEvaluable(expr) + return +} + +func (c converterGval) Parse(expr string) (*ql.ASTNode, error) { + return c.parser.Parse(expr) +} + +func (c converterGval) Convert(n *ql.ASTNode) (expr string, err error) { + return c.convert(n) +} + +func (c converterGval) convert(n *ql.ASTNode) (_ string, err error) { + switch { + case n.Symbol != "": + return n.Symbol, nil + case n.Value != nil: + // @todo I don't think this is quite ok, but it works for now so it'll do for now + if n.Value.V.Type() == "String" { + return fmt.Sprintf("\"%v\"", n.Value.V.Get()), nil + } + return fmt.Sprintf("%v", n.Value.V.Get()), nil + } + + args := make([]string, len(n.Args)) + for i, a := range n.Args { + args[i], err = c.convert(a) + if err != nil { + return + } + } + + return c.refHandler(n, args...) +} + +func (c converterGval) refHandler(n *ql.ASTNode, args ...string) (out string, err error) { + if refToGvalExp[n.Ref] == nil { + return "", fmt.Errorf("unknown ref %q", n.Ref) + } + return refToGvalExp[n.Ref].Handler(args...), nil +} + +func (e *runnerGval) Test(ctx context.Context, rows any) bool { + o, err := e.eval(ctx, rows) + if err != nil { + return false + } + + v, ok := o.(bool) + return ok && v +} + +func (e *runnerGval) Eval(ctx context.Context, rows any) any { + o, err := e.eval(ctx, rows) + if err != nil { + return nil + } + + return o +} diff --git a/pkg/dal/runner_test.go b/pkg/dal/runner_test.go new file mode 100644 index 000000000..9e0faa433 --- /dev/null +++ b/pkg/dal/runner_test.go @@ -0,0 +1,58 @@ +package dal + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRowEvaluatorTest(t *testing.T) { + tc := []struct { + name string + expr string + in map[string]ValueGetter + out bool + }{{ + name: "single row ok", + expr: `row.test == 10`, + in: map[string]ValueGetter{ + "row": (&row{}).WithValue("test", 0, 10), + }, + out: true, + }, { + name: "single row nok", + expr: `row.test == 11`, + in: map[string]ValueGetter{ + "row": (&row{}).WithValue("test", 0, 10), + }, + out: false, + }, { + name: "two rows ok", + expr: `local.key == foreign.ref`, + in: map[string]ValueGetter{ + "local": (&row{}).WithValue("key", 0, 10), + "foreign": (&row{}).WithValue("ref", 0, 10), + }, + out: true, + }, { + name: "two rows nok", + expr: `local.key == foreign.ref`, + in: map[string]ValueGetter{ + "local": (&row{}).WithValue("key", 0, 10), + "foreign": (&row{}).WithValue("ref", 0, 11), + }, + out: false, + }} + + ctx := context.Background() + + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + evl, err := newRunnerGval(c.expr) + require.NoError(t, err) + require.Equal(t, c.out, evl.Test(ctx, c.in)) + }) + } + +}