Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nil operation on EQ/NEQ/IN/NOT_IN #112

Merged
merged 8 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 107 additions & 41 deletions filter/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,18 @@ func toInt64(i interface{}) int64 {
}

func binEval(op ItemType, lhs, rhs interface{}) bool {
if _, ok := rhs.(*Regex); !ok {
if _, ok := rhs.(*Regex); ok {
if _, isStr := lhs.(string); !isStr {
log.Warnf("non-string(type %s) can not match with regexp", reflect.TypeOf(lhs))
return false
}
} else { // rhs are all literals
tl := reflect.TypeOf(lhs).String()
tr := reflect.TypeOf(rhs).String()
switch op {
case GTE, GT, LT, LTE, EQ, NEQ: // type conflict detecting on comparison expr
if tl != tr {
if _, ok := rhs.(*NilLiteral); !ok && // any type can compare to nil/null
tl != tr {
log.Warnf("type conflict %+#v(%s) <> %+#v(%s)", lhs, reflect.TypeOf(lhs), rhs, reflect.TypeOf(rhs))
return false
}
Expand All @@ -170,15 +176,25 @@ func binEval(op ItemType, lhs, rhs interface{}) bool {
return almostEqual(lv, f)
}

case *NilLiteral:
if _, ok := rhs.(*NilLiteral); !ok { // nil compared to non-nil always false
log.Warnf("rhs %v not nil", rhs)
return false
}

return lv.String() == Nil
coanor marked this conversation as resolved.
Show resolved Hide resolved

default: // NOTE: interface{} EQ/NEQ, see: https://stackoverflow.com/a/34246225/342348
switch reg := rhs.(type) {
switch rv := rhs.(type) {
case *Regex:
ok, err := regexp.MatchString(reg.Regex, lhs.(string))
log.Debugf("lhs: %v, rhs: %v", lhs, rhs)
ok, err := regexp.MatchString(rv.Regex, lhs.(string))
if err != nil {
log.Error(err)
}

return ok

default:
return lhs == rhs
}
Expand All @@ -191,6 +207,12 @@ func binEval(op ItemType, lhs, rhs interface{}) bool {
return !rhs.(*Regex).Re.MatchString(lhs.(string))

case NEQ:
_, lok := lhs.(*NilLiteral)
_, rok := rhs.(*NilLiteral)
if lok && rok {
return false
}

return lhs != rhs

case GTE, GT, LT, LTE: // rhs/lhs should be number or string
Expand Down Expand Up @@ -306,6 +328,10 @@ func (e *BinaryExpr) singleEval(data KVs) bool {
}
case *Regex:
arr = append(arr, x)
case *NilLiteral:
arr = append(arr, x)
case *BoolLiteral:
arr = append(arr, x.Val)
default:
log.Warnf("unsupported node list with type `%s'", reflect.TypeOf(elem).String())
}
Expand All @@ -314,64 +340,104 @@ func (e *BinaryExpr) singleEval(data KVs) bool {
case *Regex:
lit = rhs

case *NilLiteral:
lit = rhs

case *BoolLiteral:
lit = rhs.Val

default:

log.Errorf("invalid RHS, got type `%s'", reflect.TypeOf(e.RHS).String())
return false
}

// first: fetch left-handle-symbol and OP on right-handle-symbol
switch lhs := e.LHS.(type) {
var lhs *Identifier
switch left := e.LHS.(type) { // Left part can be string/bool/number/nil literal and identifier
case *NilLiteral:
return binEval(e.Op, nilVal, lit)

case *NumberLiteral:
if left.IsInt {
return binEval(e.Op, left.Int, lit)
} else {
return binEval(e.Op, left.Float, lit)
}

case *BoolLiteral:
return binEval(e.Op, left.Val, lit)

case *StringLiteral:
return binEval(e.Op, left.Val, lit)

case *Identifier:
name := lhs.Name

switch e.Op {
case MATCH, NOT_MATCH:
for _, item := range e.RHS.(NodeList) {
if v, ok := data.Get(name); ok {
switch x := v.(type) {
case string:
if binEval(e.Op, x, item) {
return true
}
default:
continue
}
}
}
return false
lhs = left // we get detailed lhs value later...

case IN:
for _, item := range arr {
if v, ok := data.Get(name); ok {
if binEval(EQ, v, item) {
default:
log.Errorf("unknown LHS type, expect Identifier, got `%s'", reflect.TypeOf(e.LHS).String())
return false
}

name := lhs.Name

switch e.Op {
case MATCH, NOT_MATCH:
for _, item := range e.RHS.(NodeList) {
if v, ok := data.Get(name); ok {
switch x := v.(type) {
case string:
if binEval(e.Op, x, item) {
return true
}
default:
continue
}
}
return false
}
return false

case NOT_IN:
for _, item := range arr {
if v, ok := data.Get(name); ok {
if binEval(EQ, v, item) {
return false
}
case IN:
for _, item := range arr {
if v, ok := data.Get(name); ok {
if binEval(EQ, v, item) {
return true
}
} else {
return binEval(EQ, item, nilVal)
}
}
return false

return true

case GTE, GT, LT, LTE, NEQ, EQ:

case NOT_IN:
for _, item := range arr {
if v, ok := data.Get(name); ok {
if binEval(e.Op, v, lit) {
return true
if binEval(EQ, v, item) {
return false
}
} else {
if binEval(EQ, item, nilVal) {
return false
}
}
}

return true

case GTE, GT, LT, LTE, NEQ, EQ:
if v, ok := data.Get(name); ok {
if binEval(e.Op, v, lit) {
return true
}
} else { // not exist in data
return binEval(e.Op, lit, nilVal)
}
default:
log.Errorf("unknown LHS type, expect Identifier, got `%s'", reflect.TypeOf(e.LHS).String())
log.Warnf("unsupported operation %s on single-eval expr", e.Op)
}

return false
}

var (
nilVal = &NilLiteral{}
)
136 changes: 131 additions & 5 deletions filter/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package filter
import (
"testing"

tu "github.com/GuanceCloud/cliutils/testutil"
"github.com/stretchr/testify/assert"
)

func TestExprConditions(t *testing.T) {
Expand Down Expand Up @@ -116,13 +116,139 @@ func TestExprConditions(t *testing.T) {
// fields: map[string]interface{}{"host": "123abc"},
// pass: false,
// },

{
in: "{ abc = NULL && abc = null && abc = NIL && abc = nil }",
fields: map[string]any{"xyz": int64(123)},
pass: true,
},

{
in: "{ abc in [ NULL, 123, 'hello'] }",
fields: map[string]any{"xyz": int64(123)},
pass: true,
},

{
in: "{ abc notin [ NULL, 123, 'hello'] }",
fields: map[string]any{"xyz": int64(123)},
pass: false,
},

{
in: "{ abc not_in [ 123, 'hello'] }",
fields: map[string]any{"xyz": int64(123)},
pass: true,
},

{
in: "{ xyz != NULL and abc = nil }",
fields: map[string]any{"xyz": int64(123)},
pass: true,
},

{
in: "{ xyz in [ null, 123 ] and abc = nil }",
fields: map[string]any{"xyz": int64(123)},
pass: true,
},

{
in: "{ xyz = nil }",
pass: true,
},
{
in: "{ xyz != nil }",
pass: false,
},

{
in: "{ nil = nil }", // nil literal
pass: true,
},

{
in: "{ 1 = 1 }", // int literal
pass: true,
},

{
in: " {a = b}", // a,b both nil, but b is not literal or regex
pass: false,
},

{
in: "{ true = true }", // boolean literal
pass: true,
},

{
in: "{ 'hello' = 'hello'}", // string literal
pass: true,
},

{
in: "{ 1.0 = 1.0 }", // float literal
pass: true,
},
{
in: "{ 'abc' = 'ABC' }",
pass: false,
},

{
in: "{ re('ABC') = nil }", // regexp can not be lhs
pass: false,
},

{
in: "{ nil = re('ABC') }",
pass: false,
},

{
in: "{ abc = re(`nginx_*`)}", // abc is nil
fields: map[string]interface{}{"host": "abcdef"},
pass: false,
},

{
in: "{ false = re(`nginx_*`)}",
fields: map[string]interface{}{"host": "abcdef"},
pass: false,
},

{
in: "{ 123 = re(`nginx_*`)}",
fields: map[string]interface{}{"host": "abcdef"},
pass: false,
},

{
in: "{ 3.14 = re(`nginx_*`)}",
fields: map[string]interface{}{"host": "abcdef"},
pass: false,
},

// bool in list
{
in: "{ xyz in [ false, true, 123,'abc' ] }",
fields: map[string]any{"xyz": false},
pass: true,
},

{
in: "{ abc in [ false ] }",
fields: map[string]any{"xyz": false},
pass: false,
},
}

for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
conditions, _ := GetConds(tc.in)

tu.Equals(t, tc.pass, conditions.Eval(newtf(tc.tags, tc.fields)) >= 0)
assert.Equalf(t, tc.pass, conditions.Eval(newtf(tc.tags, tc.fields)) >= 0, "conditions: %s", conditions)

t.Logf("[ok] %s => %v, source: %s, tags: %+#v, fields: %+#v", tc.in, tc.pass, tc.source, tc.tags, tc.fields)
})
Expand Down Expand Up @@ -253,7 +379,7 @@ func TestConditions(t *testing.T) {

for _, tc := range cases {
t.Run(tc.in.String(), func(t *testing.T) {
tu.Equals(t, tc.pass, tc.in.Eval(newtf(tc.tags, tc.fields)) >= 0)
assert.Equal(t, tc.pass, tc.in.Eval(newtf(tc.tags, tc.fields)) >= 0)
t.Logf("[ok] %s => %v, tags: %+#v, fields: %+#v", tc.in, tc.pass, tc.tags, tc.fields)
})
}
Expand Down Expand Up @@ -345,7 +471,7 @@ func TestBinEval(t *testing.T) {
}

for _, tc := range cases {
tu.Equals(t, tc.pass, binEval(tc.op, tc.lhs, tc.rhs))
assert.Equal(t, tc.pass, binEval(tc.op, tc.lhs, tc.rhs))
t.Logf("[ok] %v %s %v => %v", tc.lhs, tc.op, tc.rhs, tc.pass)
}
}
Expand Down Expand Up @@ -419,7 +545,7 @@ func TestEval(t *testing.T) {
for _, tc := range cases {
t.Run(tc.cond.String(), func(t *testing.T) {
t.Logf("[ok] %s => %v", tc.cond, tc.pass)
tu.Equals(t, tc.pass, tc.cond.Eval(newtf(tc.tags, tc.fields)))
assert.Equal(t, tc.pass, tc.cond.Eval(newtf(tc.tags, tc.fields)))
})
}
}
Expand Down
Loading
Loading