From 7e1e9b2c9b983efe45c50049d1fb29bfc3dfeb43 Mon Sep 17 00:00:00 2001
From: Danis Kirasirov <dbgbbu@gmail.com>
Date: Wed, 21 Feb 2024 20:05:21 +0300
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
 =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20try?=
 =?UTF-8?q?=20=D0=B2=20expr/mongo?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkg/expr/expr.go       | 23 ---------------------
 pkg/expr/expr_test.go  | 45 ------------------------------------------
 pkg/expr/mongo.go      | 31 +++++++++++++++++++++++++++++
 pkg/expr/mongo_test.go |  4 ++++
 4 files changed, 35 insertions(+), 68 deletions(-)

diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go
index 1969b58c..42588c2b 100644
--- a/pkg/expr/expr.go
+++ b/pkg/expr/expr.go
@@ -1,21 +1,12 @@
 package expr
 
 import (
-	"regexp"
-	"strings"
-
-	"git.perx.ru/perxis/perxis-go/pkg/data"
 	exprcompiler "github.com/expr-lang/expr/compiler"
 	"github.com/expr-lang/expr/parser"
 	"github.com/expr-lang/expr/vm"
 	"golang.org/x/net/context"
 )
 
-var (
-	additionalFunctions = []string{"contains", "startsWith", "endsWith", "and", "or", "in", "not"}
-	isExpression        = regexp.MustCompile(`[()}{<>=|&%*+\-\/\]\[\\]`).MatchString
-)
-
 const EnvContextKey = "$context"
 
 func Eval(ctx context.Context, input string, env map[string]interface{}) (interface{}, error) {
@@ -65,17 +56,3 @@ func EvalKV(ctx context.Context, input string, kv ...interface{}) (interface{},
 
 	return Eval(ctx, input, m)
 }
-
-func IsExpression(input string) bool {
-	if isExpression(input) {
-		return true
-	}
-
-	for _, s := range strings.Fields(input) {
-		if data.Contains(s, additionalFunctions) {
-			return true
-		}
-	}
-
-	return false
-}
\ No newline at end of file
diff --git a/pkg/expr/expr_test.go b/pkg/expr/expr_test.go
index 35153da2..b3db1b23 100644
--- a/pkg/expr/expr_test.go
+++ b/pkg/expr/expr_test.go
@@ -2,56 +2,11 @@ package expr
 
 import (
 	"context"
-	"fmt"
 	"testing"
-	"time"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-func TestIsExpression(t *testing.T) {
-	now := time.Now()
-
-	tests := []struct {
-		name string
-		eval string
-		want bool
-	}{
-		{"equal", "i == 3", true},
-		{"in array", "i in [1,2,3]", true},
-		{"contains", "value contains 'some'", true},
-		{"contains with . + () $ {} ^", "value contains 'something with . + () $ {} ^'", true},
-		{"startsWith", "value startsWith 'some'", true},
-		{"startsWith . + () $ {} ^", "value startsWith '. + () $ {} ^'", true},
-		{"endsWith", "value endsWith 'some'", true},
-		{"endsWith . + () $ {} ^", "value endsWith '. + () $ {} ^'", true},
-		{"icontains", "icontains(value, 'some')", true},
-		{"icontains with . + () $ {} ^", "icontains (value, 'something with . + () $ {} ^')", true},
-		{"istartsWith", "istartsWith(value, 'Some')", true},
-		{"istartsWith . + () $ {} ^ . + () $ {} ^", "istartsWith(value, '. + () $ {} ^')", true},
-		{"iendsWith", "iendsWith(value, 'some')", true},
-		{"iendsWith . + () $ {} ^", "iendsWith(value,'. + () $ {} ^')", true},
-		{"or", "i == 2 || i > 10", true},
-		{"search", "search('some') || i > 10", true},
-		{"vars:or", "i == a + 2 || i > a + 10", true},
-		{"near", "near(a, [55.5, 37.5], 1000)", true},
-		{"within", "within(a, 'box', [[54.54, 36.36], [55.55, 37.37]])", true},
-		{"time", "d > Time.Date('2021-08-31')", true},
-		{"time", fmt.Sprintf("d > Time.Time('%s')", now.Format(time.RFC3339)), true},
-		{"in", "In(s, [1,2,3])", true},
-		{"in", "In(s, 1)", true},
-		{"text search or id", "id", false},
-		{"numbers", "3", false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := IsExpression(tt.eval)
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
-
 type testEnvStruct struct {
 	ID   string      `expr:"id"`
 	Size int         `expr:"size"`
diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go
index f0e33578..71f3bdda 100644
--- a/pkg/expr/mongo.go
+++ b/pkg/expr/mongo.go
@@ -358,6 +358,8 @@ func (c *compiler) SliceNode(node *ast.SliceNode) interface{} {
 
 func (c *compiler) CallNode(node *ast.CallNode) interface{} {
 	switch node.Callee.String() {
+	case "try":
+		return c.handleTryNode(node)
 	case "search", "q":
 		val := c.compile(node.Arguments[0])
 		return bson.M{"$text": bson.M{"$search": val}}
@@ -710,3 +712,32 @@ func (c *compiler) handleLenNode(node *ast.BinaryNode) bson.M {
 		panic("invalid comparison operator with len()")
 	}
 }
+
+// handleTryNode получает узел AST с двумя выражениями-строками и пытается выполнить первое
+// в случае успеха - возвращает его
+// в случае ошибки - пытается выполнить второе и вернуть его результат
+func (c *compiler) handleTryNode(node *ast.CallNode) (result interface{}) {
+	if len(node.Arguments) != 2 {
+		panic("try() expects exactly 2 arguments")
+	}
+
+	subcompiler := &compiler{env: c.env, config: c.config, identifierRenameFn: c.identifierRenameFn}
+	subexprCompile := func(subexpr string) interface{} {
+		tree, err := parser.Parse(subexpr)
+		if err != nil {
+			panic(err)
+		}
+
+		subcompiler.tree = tree
+		return subcompiler.compile(tree.Node)
+	}
+
+	defer func() {
+		if r := recover(); r != nil {
+			result = subexprCompile(node.Arguments[1].(*ast.StringNode).Value)
+		}
+	}()
+
+	result = subexprCompile(node.Arguments[0].(*ast.StringNode).Value).(bson.M)
+	return
+}
diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go
index bbe42465..65070bbc 100644
--- a/pkg/expr/mongo_test.go
+++ b/pkg/expr/mongo_test.go
@@ -79,6 +79,10 @@ func TestConvertToMongo(t *testing.T) {
 		{"in", "In(s, 1)", nil, bson.M{"s": bson.M{"$in": []interface{}{1}}}, false},
 		{"text search or id", "id", nil, nil, true},
 		{"struct env", "db_item.id == env_item.id", map[string]interface{}{"env_item": &testEnvStruct{ID: "id1"}}, bson.M{"db_item.id": "id1"}, false},
+		{"try#1", "try('s == 1', 's == 2')", nil, bson.M{"s": 1}, false},
+		{"try#2", "try('some-slug', 'search(\\'some\\') || _id contains \\'some-slug\\'')", nil, bson.M{"$or": bson.A{bson.M{"$text": bson.M{"$search": "some"}}, bson.M{"_id": bson.M{"$regex": "some-slug"}}}}, false},
+		{"try#3", "try(bad-expr, bad-expr)", nil, nil, true},
+		{"try#4", "try('3', 's == 1')", nil, bson.M{"s": 1}, false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-- 
GitLab