From 389ee2d25ca9fa58586d0abe30619b07cbc58451 Mon Sep 17 00:00:00 2001 From: murasame Date: Wed, 22 Jan 2025 15:47:30 +0800 Subject: [PATCH] feature: support foreach items record callback --- internal/core/condition.go | 73 +++++++++++++++++++++++++++ internal/core/rule_visitor.go | 80 ++++++++++++++++++------------ internal/core/rule_visitor_test.go | 22 +++++--- typedef/visit.go | 13 +++++ 4 files changed, 149 insertions(+), 39 deletions(-) diff --git a/internal/core/condition.go b/internal/core/condition.go index b51791d..321eadd 100644 --- a/internal/core/condition.go +++ b/internal/core/condition.go @@ -77,3 +77,76 @@ func (a *arishemCondition) Operator() string { func (a *arishemCondition) Error() error { return a.error } + +type arishemForeachItem struct { + Index int + ItemVal interface{} + Pass bool +} + +func (a *arishemForeachItem) Idx() int { + return a.Index +} + +func (a *arishemForeachItem) ItemValue() interface{} { + return a.ItemVal +} + +func (a *arishemForeachItem) Passed() bool { + return a.Pass +} + +func newArishemForeachItem(idx int, itemVal interface{}, pass bool) typedef.ForeachItem { + return &arishemForeachItem{ + Index: idx, + ItemVal: itemVal, + Pass: pass, + } +} + +type arishemForeachCondition struct { + *arishemCondition + foreachOperator string + foreachLogic string + leftItems []typedef.ForeachItem +} + +func newArishemForeachCondition( + passed bool, + left interface{}, + leftExpr string, + right interface{}, + rightExpr string, + operator string, + error error, + foreachOperator string, + foreachLogic string, + foreachItems []typedef.ForeachItem, +) typedef.JudgeNode { + return &arishemForeachCondition{ + arishemCondition: &arishemCondition{ + passed: passed, + left: left, + leftExpr: leftExpr, + right: right, + rightExpr: rightExpr, + operator: operator, + error: error, + }, + foreachOperator: foreachOperator, + foreachLogic: foreachLogic, + leftItems: foreachItems, + } +} + +func (a *arishemForeachCondition) ForeachLogic() string { + return a.foreachLogic +} + +func (a *arishemForeachCondition) ForeachOperator() string { + return a.foreachOperator +} + +func (a *arishemForeachCondition) ForeachItems() []typedef.ForeachItem { + return a.leftItems +} diff --git a/internal/core/rule_visitor.go b/internal/core/rule_visitor.go index c64e628..b8f79dd 100644 --- a/internal/core/rule_visitor.go +++ b/internal/core/rule_visitor.go @@ -30,6 +30,8 @@ import ( "strings" ) +const foreachKey = "FOREACH" + type simpleVisitTarget struct { name string } @@ -242,48 +244,29 @@ func (a *arishemRuleVisitor) VisitFullCondition(ctx *parser.FullConditionContext left := a.Visit(lhsExpr) right := a.Visit(rhsExpr) - pass := false + var pass = false var err error - - const foreachKey = "FOREACH" + var cdtInfo typedef.JudgeNode if !strings.HasPrefix(opr, foreachKey) { pass, err = evaluateLR(a, left, right, opr) + cdtInfo = newArishemCondition(pass, left, lhsExpr.GetText(), right, rhsExpr.GetText(), opr, err) } else { - // execute the foreach operator - // check the left type whether array or not - var lArray []interface{} - lArray, err = tools.ConvToSliceUnifyType(left) - if err == nil { - // parse the operator, arishem grammar guarantee that here are at least two space - const space = " " - firstSpace := strings.Index(opr, space) - lastSpace := strings.LastIndex(opr, space) - foreachOpr := opr[firstSpace+1 : lastSpace] - if strings.HasPrefix(foreachOpr, foreachKey) { - err = errors.New("nested foreach operation is not supported") - } else { - logicOpr := toStdLogic(opr[lastSpace+1:]) - for _, lVal := range lArray { - pass, err = evaluateLR(a, lVal, right, foreachOpr) - if err != nil { - break - } - if logicOpr == logicAnd && pass == false { - break - } - if logicOpr == logicOr && pass == true { - break - } - } - } - } + // parse operator and logic + const space = " " + firstSpace := strings.Index(opr, space) + lastSpace := strings.LastIndex(opr, space) + foreachOpr := opr[firstSpace+1 : lastSpace] + logicOpr := toStdLogic(opr[lastSpace+1:]) + // execute the foreach condition + var foreachItems []typedef.ForeachItem + foreachItems, pass, err = a.visitForeachConditionInner(left, right, foreachOpr, logicOpr) + cdtInfo = newArishemForeachCondition(pass, left, lhsExpr.GetText(), right, rhsExpr.GetText(), opr, err, foreachOpr, logicOpr, foreachItems) } if err != nil { a.errorCallback(node, err.Error()) } // put it into cache - cdtInfo := newArishemCondition(pass, left, lhsExpr.GetText(), right, rhsExpr.GetText(), opr, err) a.dataCtx.Set(ctx.GetAltNumber(), cdtInfo) // observers call back for _, obsr := range a.observers { @@ -292,6 +275,37 @@ func (a *arishemRuleVisitor) VisitFullCondition(ctx *parser.FullConditionContext return pass } +// execute the foreach operator judgement +func (a *arishemRuleVisitor) visitForeachConditionInner(left, right interface{}, foreachOpr, logicOpr string) ([]typedef.ForeachItem, bool, error) { + if strings.HasPrefix(foreachOpr, foreachKey) { + return nil, false, errors.New("nested foreach operation is not supported") + } + // check the left type whether array or not + lArray, err := tools.ConvToSliceUnifyType(left) + if err != nil { + return nil, false, errors.New("foreach operation is supported only when left is slice/array type") + } + // parse the operator, arishem grammar guarantee that here are at least two space + items := make([]typedef.ForeachItem, 0, len(lArray)/2+1) + pass := false + for i, lVal := range lArray { + pass, err = evaluateLR(a, lVal, right, foreachOpr) + + items = append(items, newArishemForeachItem(i, lVal, pass)) + + if err != nil { + break + } + if logicOpr == logicAnd && pass == false { + break + } + if logicOpr == logicOr && pass == true { + break + } + } + return items, pass, err +} + func (a *arishemRuleVisitor) VisitNullCondition(_ *parser.NullConditionContext) interface{} { return false } @@ -706,7 +720,7 @@ func evaluateLR(ori *arishemRuleVisitor, left, right interface{}, opr string) (b return false, errors.New("sub condition right type is not sub condition expression") } pass, err = evaluateSubCond(ori, rSubCond.name, lData, rSubCond.tree) - // if err is not nil, pass will have to be false + // if err is not nil, Pass will have to be false if err == nil && (strings.HasPrefix(opr, "!") || strings.HasPrefix(opr, "NOT")) { // revert the judge result pass = !pass diff --git a/internal/core/rule_visitor_test.go b/internal/core/rule_visitor_test.go index b56c8f1..836ed12 100644 --- a/internal/core/rule_visitor_test.go +++ b/internal/core/rule_visitor_test.go @@ -22,6 +22,7 @@ import ( "github.com/bytedance/arishem/internal/parser" "github.com/bytedance/arishem/typedef" "github.com/bytedance/gopkg/util/logger" + "github.com/bytedance/sonic" "github.com/stretchr/testify/assert" "testing" ) @@ -63,9 +64,18 @@ func (t *testArisVisitObserver) HashCode() string { func (t *testArisVisitObserver) OnJudgeNodeVisitEnd(ctx context.Context, info typedef.JudgeNode, vt typedef.VisitTarget) { if info != nil { - logger.Infof("left: %v, leftExpr: %v, right: %v, rightExpr: %v, operator: %v, target: %v, err: %v", - info.Left(), info.LeftExpr(), info.Right(), info.RightExpr(), info.Operator(), vt.Identifier(), info.Error(), - ) + foreachNode, ok := info.(typedef.ForeachJudgeNode) + if !ok { + logger.Infof("left: %v, leftExpr: %v, right: %v, rightExpr: %v, operator: %v, target: %v, err: %v", + info.Left(), info.LeftExpr(), info.Right(), info.RightExpr(), info.Operator(), vt.Identifier(), info.Error(), + ) + } else { + itemsStr, _ := sonic.MarshalString(foreachNode.ForeachItems()) + logger.Infof("[ForeachJudgeNode] left: %v, leftExpr: %v, right: %v, rightExpr: %v, operator: %v, target: %v, err: %v, foreachOperator: %v, foreachLogic: %v, foreachItems: %v", + foreachNode.Left(), info.LeftExpr(), foreachNode.Right(), info.RightExpr(), foreachNode.Operator(), vt.Identifier(), foreachNode.Error(), + foreachNode.ForeachOperator(), foreachNode.ForeachLogic(), itemsStr, + ) + } } } @@ -216,7 +226,7 @@ func TestConditionVisit(t *testing.T) { assert.Empty(t, errMsg) }, }, { - "CONDITION: [pass] logic math operator by LrMath", + "CONDITION: [Pass] logic math operator by LrMath", `{"is_latest": true}`, `{"OpLogic":"AND","Conditions":[{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"==","Lhs":{"Const":{"NumConst":1.1232}},"Rhs":{"Const":{"NumConst":1.1232}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"!=","Lhs":{"Const":{"NumConst":10}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":">","Lhs":{"Const":{"NumConst":10}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"<","Lhs":{"Const":{"NumConst":-1}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":">=","Lhs":{"Const":{"NumConst":8}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"<=","Lhs":{"Const":{"NumConst":8}},"Rhs":{"Const":{"NumConst":8}}}}}]}`, func(pass bool, errMsg []string) { @@ -224,7 +234,7 @@ func TestConditionVisit(t *testing.T) { assert.Empty(t, errMsg) }, }, { - "CONDITION: [not pass] logic math operator by LrMath", + "CONDITION: [not Pass] logic math operator by LrMath", `{"is_latest": true}`, `{"OpLogic":"AND","Conditions":[{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"!=","Lhs":{"Const":{"NumConst":8}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"!=","Lhs":{"Const":{"NumConst":10}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":">","Lhs":{"Const":{"NumConst":10}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"<","Lhs":{"Const":{"NumConst":-1}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":">=","Lhs":{"Const":{"NumConst":8}},"Rhs":{"Const":{"NumConst":8}}}}},{"Operator":"==","Lhs":{"VarExpr":"is_latest"},"Rhs":{"MathExpr":{"OpMath":"<=","Lhs":{"Const":{"NumConst":8}},"Rhs":{"Const":{"NumConst":8}}}}}]}`, func(pass bool, errMsg []string) { @@ -484,7 +494,7 @@ func TestForeach(t *testing.T) { }, { "SUB CONDITION: judge with FOREACH VALID sub condition operator: PASS", `{"item_info":{"item_list":[{"name":"name@1","price":100},{"name":"name@2","price":102.13},{"name":"name@3","price":200},{"name":"name@4","price":100},{"name":"name@5","price":101},{"name":"name@6","price":303.1234}]}}`, - `{"OpLogic":"&&","Conditions":[{"Operator":"FOREACH SUB_COND &&","Lhs":{"VarExpr":"item_info.item_list"},"Rhs":{"SubCondExpr":{"CondName":"MyCond"}}}]}`, + `{"OpLogic":"&&","Conditions":[{"Operator":"FOREACH SUB_COND ||","Lhs":{"VarExpr":"item_info.item_list"},"Rhs":{"SubCondExpr":{"CondName":"MyCond"}}}]}`, func(_ string) (antlr.ParseTree, error) { return parser.ParseArishemCondition(`{"OpLogic":"&&","Conditions":[{"Operator":">","Lhs":{"VarExpr":"price"},"Rhs":{"Const":{"NumConst":10}}},{"Operator":"STRING_CONTAINS","Lhs":{"VarExpr":"name"},"Rhs":{"Const":{"StrConst":"name@"}}}]}`) }, diff --git a/typedef/visit.go b/typedef/visit.go index fb1d6c4..744036b 100644 --- a/typedef/visit.go +++ b/typedef/visit.go @@ -34,6 +34,19 @@ type JudgeNode interface { Error() error } +type ForeachItem interface { + Idx() int + ItemValue() interface{} + Passed() bool +} + +type ForeachJudgeNode interface { + JudgeNode + ForeachOperator() string + ForeachLogic() string + ForeachItems() []ForeachItem +} + // ActionAim is the interface that defines the aim of action. type ActionAim interface { ActionName() string