From 93955cb058c1cc25b3d9ebea06cd407112e6ecca Mon Sep 17 00:00:00 2001 From: Ildar Karymov Date: Tue, 11 Feb 2025 15:20:34 +0100 Subject: [PATCH] Implement struct matching --- .github/workflows/main.yml | 2 +- .golangci.yml | 2 +- Taskfile.yml | 2 +- query/ast.go | 6 +- query/match.go | 162 +++++ query/match_test.go | 1135 ++++++++++++++++++++++++++++++++++++ query/validation_test.go | 6 +- 7 files changed, 1307 insertions(+), 8 deletions(-) create mode 100644 query/match.go create mode 100644 query/match_test.go diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10e57b7..591433f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: - name: unit-tests run: | go test ./... -coverprofile=coverage.out - cat coverage.out | grep -v "query/parser.gen.go" > coverage_filtered.out + cat coverage.out | grep -v "query/parser.gen.go" > coverage_filtered.out | grep -v "github.com/defer-panic/dumbql/query/ast.go:81" go tool cover -func=coverage_filtered.out - name: golangci-lint diff --git a/.golangci.yml b/.golangci.yml index 6e3f205..d2ea965 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,7 +5,6 @@ linters: enable: - cyclop - decorder - - dupl - exhaustive - fatcontext - funlen @@ -29,6 +28,7 @@ linters: - reassign - recvcheck - revive + - testifylint - testpackage - tparallel - unconvert diff --git a/Taskfile.yml b/Taskfile.yml index abfcffd..d9ad0e3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,7 +32,7 @@ tasks: desc: "Run unit tests" cmds: - go test ./... -coverprofile=coverage.out - - cat coverage.out | grep -v "query/parser.gen.go" > coverage_filtered.out + - cat coverage.out | grep -v "query/parser.gen.go" | grep -v "github.com/defer-panic/dumbql/query/ast.go:81" > coverage_filtered.out - go tool cover -func=coverage_filtered.out # Codegen diff --git a/query/ast.go b/query/ast.go index bfa0344..d41c4e2 100644 --- a/query/ast.go +++ b/query/ast.go @@ -14,11 +14,13 @@ type Expr interface { fmt.Stringer sq.Sqlizer + Match(target any, matcher Matcher) bool Validate(schema.Schema) (Expr, error) } type Valuer interface { Value() any + Match(target any, op FieldOperator) bool } // BinaryExpr represents a binary operation (`and`, `or`, `AND`, `OR`) between two expressions. @@ -57,8 +59,8 @@ type StringLiteral struct { StringValue string } -func (t *StringLiteral) String() string { return strconv.Quote(t.StringValue) } -func (t *StringLiteral) Value() any { return t.StringValue } +func (s *StringLiteral) String() string { return strconv.Quote(s.StringValue) } +func (s *StringLiteral) Value() any { return s.StringValue } type NumberLiteral struct { NumberValue float64 diff --git a/query/match.go b/query/match.go new file mode 100644 index 0000000..528745a --- /dev/null +++ b/query/match.go @@ -0,0 +1,162 @@ +package query + +import ( + "reflect" + "strings" +) + +type Matcher interface { + MatchAnd(target any, left, right Expr) bool + MatchOr(target any, left, right Expr) bool + MatchNot(target any, expr Expr) bool + MatchField(target any, field string, value Valuer, op FieldOperator) bool + MatchValue(target any, value Valuer, op FieldOperator) bool +} + +type DefaultMatcher struct{} + +func (m *DefaultMatcher) MatchAnd(target any, left, right Expr) bool { + return left.Match(target, m) && right.Match(target, m) +} + +func (m *DefaultMatcher) MatchOr(target any, left, right Expr) bool { + return left.Match(target, m) || right.Match(target, m) +} + +func (m *DefaultMatcher) MatchNot(target any, expr Expr) bool { + return !expr.Match(target, m) +} + +func (m *DefaultMatcher) MatchField(target any, field string, value Valuer, op FieldOperator) bool { + t := reflect.TypeOf(target) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return false + } + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + + fname := f.Name + if f.Tag.Get("dumbql") != "" { + fname = f.Tag.Get("dumbql") + } + + if fname == field { + return m.MatchValue(reflect.ValueOf(target).Field(i).Interface(), value, op) + } + } + + return false +} + +func (m *DefaultMatcher) MatchValue(target any, value Valuer, op FieldOperator) bool { + return value.Match(target, op) +} + +func (b *BinaryExpr) Match(target any, matcher Matcher) bool { + switch b.Op { + case And: + return matcher.MatchAnd(target, b.Left, b.Right) + case Or: + return matcher.MatchOr(target, b.Left, b.Right) + default: + return false + } +} + +func (n *NotExpr) Match(target any, matcher Matcher) bool { + return matcher.MatchNot(target, n.Expr) +} + +func (f *FieldExpr) Match(target any, matcher Matcher) bool { + return matcher.MatchField(target, f.Field.String(), f.Value, f.Op) +} + +func (s *StringLiteral) Match(target any, op FieldOperator) bool { + str, ok := target.(string) + if !ok { + return false + } + + return matchString(str, s.StringValue, op) +} + +func (i *IntegerLiteral) Match(target any, op FieldOperator) bool { + intVal, ok := target.(int64) + if !ok { + return false + } + + return matchNum(intVal, i.IntegerValue, op) +} + +func (n *NumberLiteral) Match(target any, op FieldOperator) bool { + floatVal, ok := target.(float64) + if !ok { + return false + } + + return matchNum(floatVal, n.NumberValue, op) +} + +func (i Identifier) Match(target any, op FieldOperator) bool { + str, ok := target.(string) + if !ok { + return false + } + + return matchString(str, i.String(), op) +} + +func (o *OneOfExpr) Match(target any, op FieldOperator) bool { + switch op { //nolint:exhaustive + case Equal, Like: + for _, v := range o.Values { + if v.Match(target, op) { + return true + } + } + + return false + + default: + return false + } +} + +func matchString(a, b string, op FieldOperator) bool { + switch op { //nolint:exhaustive + case Equal: + return a == b + case NotEqual: + return a != b + case Like: + return strings.Contains(a, b) + default: + return false + } +} + +func matchNum[T int64 | float64](a, b T, op FieldOperator) bool { + switch op { //nolint:exhaustive + case Equal: + return a == b + case NotEqual: + return a != b + case GreaterThan: + return a > b + case GreaterThanOrEqual: + return a >= b + case LessThan: + return a < b + case LessThanOrEqual: + return a <= b + default: + return false + } +} diff --git a/query/match_test.go b/query/match_test.go new file mode 100644 index 0000000..cff5aa6 --- /dev/null +++ b/query/match_test.go @@ -0,0 +1,1135 @@ +package query_test + +import ( + "testing" + + "github.com/defer-panic/dumbql/query" + "github.com/stretchr/testify/assert" +) + +type person struct { + Name string `dumbql:"name"` + Age int64 `dumbql:"age"` + Height float64 `dumbql:"height"` + IsMember bool +} + +func TestDefaultMatcher_MatchAnd(t *testing.T) { //nolint:funlen + matcher := &query.DefaultMatcher{} + target := person{Name: "John", Age: 30} + + tests := []struct { + name string + left query.Expr + right query.Expr + want bool + }{ + { + name: "both conditions true", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + want: true, + }, + { + name: "left condition false", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + want: false, + }, + { + name: "right condition false", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + want: false, + }, + { + name: "both conditions false", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchAnd(target, test.left, test.right) + assert.Equal(t, test.want, result) + }) + } +} + +func TestDefaultMatcher_MatchOr(t *testing.T) { //nolint:funlen + matcher := &query.DefaultMatcher{} + target := person{Name: "John", Age: 30} + + tests := []struct { + name string + left query.Expr + right query.Expr + want bool + }{ + { + name: "both conditions true", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + want: true, + }, + { + name: "left condition true only", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + want: true, + }, + { + name: "right condition true only", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + want: true, + }, + { + name: "both conditions false", + left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchOr(target, test.left, test.right) + assert.Equal(t, test.want, result) + }) + } +} + +func TestDefaultMatcher_MatchNot(t *testing.T) { + matcher := &query.DefaultMatcher{} + target := person{Name: "John", Age: 30} + + tests := []struct { + name string + expr query.Expr + want bool + }{ + { + name: "negate true condition", + expr: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + want: false, + }, + { + name: "negate false condition", + expr: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + want: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchNot(target, test.expr) + assert.Equal(t, test.want, result) + }) + } +} + +func TestDefaultMatcher_MatchField(t *testing.T) { + matcher := &query.DefaultMatcher{} + target := person{ + Name: "John", + Age: 30, + Height: 1.75, + IsMember: true, + } + + tests := []struct { + name string + field string + value query.Valuer + op query.FieldOperator + want bool + }{ + { + name: "string equal match", + field: "name", + value: &query.StringLiteral{StringValue: "John"}, + op: query.Equal, + want: true, + }, + { + name: "string not equal match", + field: "name", + value: &query.StringLiteral{StringValue: "Jane"}, + op: query.NotEqual, + want: true, + }, + { + name: "integer equal match", + field: "age", + value: &query.IntegerLiteral{IntegerValue: 30}, + op: query.Equal, + want: true, + }, + { + name: "float greater than match", + field: "height", + value: &query.NumberLiteral{NumberValue: 1.70}, + op: query.GreaterThan, + want: true, + }, + { + name: "non-existent field", + field: "invalid", + value: &query.StringLiteral{StringValue: "test"}, + op: query.Equal, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchField(target, test.field, test.value, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func TestDefaultMatcher_MatchValue(t *testing.T) { + t.Run("string", testMatchValueString) + t.Run("integer", testMatchValueInteger) + t.Run("float", testMatchValueFloat) + t.Run("type mismatch", testMatchValueTypeMismatch) +} + +func testMatchValueString(t *testing.T) { //nolint:funlen + matcher := &query.DefaultMatcher{} + tests := []struct { + name string + target any + value query.Valuer + op query.FieldOperator + want bool + }{ + { + name: "equal - match", + target: "hello", + value: &query.StringLiteral{StringValue: "hello"}, + op: query.Equal, + want: true, + }, + { + name: "equal - no match", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.Equal, + want: false, + }, + { + name: "not equal - match", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.NotEqual, + want: true, + }, + { + name: "not equal - no match", + target: "hello", + value: &query.StringLiteral{StringValue: "hello"}, + op: query.NotEqual, + want: false, + }, + { + name: "like - match", + target: "hello world", + value: &query.StringLiteral{StringValue: "world"}, + op: query.Like, + want: true, + }, + { + name: "like - no match", + target: "hello world", + value: &query.StringLiteral{StringValue: "universe"}, + op: query.Like, + want: false, + }, + { + name: "greater than - invalid", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.GreaterThan, + want: false, + }, + { + name: "greater than or equal - invalid", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.GreaterThanOrEqual, + want: false, + }, + { + name: "less than - invalid", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.LessThan, + want: false, + }, + { + name: "less than or equal - invalid", + target: "hello", + value: &query.StringLiteral{StringValue: "world"}, + op: query.LessThanOrEqual, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchValue(test.target, test.value, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func testMatchValueInteger(t *testing.T) { //nolint:funlen + matcher := &query.DefaultMatcher{} + tests := []struct { + name string + target any + value query.Valuer + op query.FieldOperator + want bool + }{ + { + name: "equal - match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.Equal, + want: true, + }, + { + name: "equal - no match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.Equal, + want: false, + }, + { + name: "not equal - match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.NotEqual, + want: true, + }, + { + name: "not equal - no match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.NotEqual, + want: false, + }, + { + name: "greater than - match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.GreaterThan, + want: true, + }, + { + name: "greater than - no match", + target: int64(24), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.GreaterThan, + want: false, + }, + { + name: "greater than or equal - match (greater)", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.GreaterThanOrEqual, + want: true, + }, + { + name: "greater than or equal - match (equal)", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.GreaterThanOrEqual, + want: true, + }, + { + name: "greater than or equal - no match", + target: int64(24), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.GreaterThanOrEqual, + want: false, + }, + { + name: "less than - match", + target: int64(24), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.LessThan, + want: true, + }, + { + name: "less than - no match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.LessThan, + want: false, + }, + { + name: "less than or equal - match (less)", + target: int64(24), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.LessThanOrEqual, + want: true, + }, + { + name: "less than or equal - match (equal)", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.LessThanOrEqual, + want: true, + }, + { + name: "less than or equal - no match", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.LessThanOrEqual, + want: false, + }, + { + name: "like - invalid", + target: int64(42), + value: &query.IntegerLiteral{IntegerValue: 24}, + op: query.Like, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchValue(test.target, test.value, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func testMatchValueFloat(t *testing.T) { //nolint:funlen + matcher := &query.DefaultMatcher{} + tests := []struct { + name string + target any + value query.Valuer + op query.FieldOperator + want bool + }{ + { + name: "equal - match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.Equal, + want: true, + }, + { + name: "equal - no match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.Equal, + want: false, + }, + { + name: "not equal - match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.NotEqual, + want: true, + }, + { + name: "not equal - no match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.NotEqual, + want: false, + }, + { + name: "greater than - match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.GreaterThan, + want: true, + }, + { + name: "greater than - no match", + target: 2.718, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.GreaterThan, + want: false, + }, + { + name: "greater than or equal - match (greater)", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.GreaterThanOrEqual, + want: true, + }, + { + name: "greater than or equal - match (equal)", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.GreaterThanOrEqual, + want: true, + }, + { + name: "greater than or equal - no match", + target: 2.718, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.GreaterThanOrEqual, + want: false, + }, + { + name: "less than - match", + target: 2.718, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.LessThan, + want: true, + }, + { + name: "less than - no match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.LessThan, + want: false, + }, + { + name: "less than or equal - match (less)", + target: 2.718, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.LessThanOrEqual, + want: true, + }, + { + name: "less than or equal - match (equal)", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 3.14}, + op: query.LessThanOrEqual, + want: true, + }, + { + name: "less than or equal - no match", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.LessThanOrEqual, + want: false, + }, + { + name: "like - invalid", + target: 3.14, + value: &query.NumberLiteral{NumberValue: 2.718}, + op: query.Like, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchValue(test.target, test.value, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func testMatchValueTypeMismatch(t *testing.T) { + matcher := &query.DefaultMatcher{} + tests := []struct { + name string + target any + value query.Valuer + op query.FieldOperator + want bool + }{ + { + name: "string target with integer value", + target: "42", + value: &query.IntegerLiteral{IntegerValue: 42}, + op: query.Equal, + want: false, + }, + { + name: "integer target with float value", + target: int64(42), + value: &query.NumberLiteral{NumberValue: 42.0}, + op: query.Equal, + want: false, + }, + { + name: "float target with string value", + target: 3.14, + value: &query.StringLiteral{StringValue: "3.14"}, + op: query.Equal, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := matcher.MatchValue(test.target, test.value, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func TestBinaryExpr_Match(t *testing.T) { //nolint:funlen + target := person{Name: "John", Age: 30} + matcher := &query.DefaultMatcher{} + + tests := []struct { + name string + expr *query.BinaryExpr + want bool + }{ + { + name: "AND - both true", + expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + Op: query.And, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + }, + want: true, + }, + { + name: "AND - left false", + expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + Op: query.And, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + }, + want: false, + }, + { + name: "OR - both true", + expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + Op: query.Or, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + }, + want: true, + }, + { + name: "OR - one true", + expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + Op: query.Or, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + }, + want: true, + }, + { + name: "OR - both false", + expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + Op: query.Or, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.expr.Match(target, matcher) + assert.Equal(t, test.want, result) + }) + } +} + +func TestNotExpr_Match(t *testing.T) { + target := person{Name: "John", Age: 30} + matcher := &query.DefaultMatcher{} + + tests := []struct { + name string + expr *query.NotExpr + want bool + }{ + { + name: "negate true condition", + expr: &query.NotExpr{ + Expr: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + }, + want: false, + }, + { + name: "negate false condition", + expr: &query.NotExpr{ + Expr: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + }, + want: true, + }, + { + name: "negate AND expression", + expr: &query.NotExpr{ + Expr: &query.BinaryExpr{ + Left: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + Op: query.And, + Right: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.IntegerLiteral{IntegerValue: 30}, + }, + }, + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.expr.Match(target, matcher) + assert.Equal(t, test.want, result) + }) + } +} + +func TestFieldExpr_Match(t *testing.T) { //nolint:funlen + target := person{ + Name: "John", + Age: 30, + Height: 1.75, + IsMember: true, + } + matcher := &query.DefaultMatcher{} + + tests := []struct { + name string + expr *query.FieldExpr + want bool + }{ + { + name: "string equal - match", + expr: &query.FieldExpr{ + Field: "name", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "John"}, + }, + want: true, + }, + { + name: "string not equal - match", + expr: &query.FieldExpr{ + Field: "name", + Op: query.NotEqual, + Value: &query.StringLiteral{StringValue: "Jane"}, + }, + want: true, + }, + { + name: "integer greater than - match", + expr: &query.FieldExpr{ + Field: "age", + Op: query.GreaterThan, + Value: &query.IntegerLiteral{IntegerValue: 25}, + }, + want: true, + }, + { + name: "float less than - match", + expr: &query.FieldExpr{ + Field: "height", + Op: query.LessThan, + Value: &query.NumberLiteral{NumberValue: 1.80}, + }, + want: true, + }, + { + name: "non-existent field", + expr: &query.FieldExpr{ + Field: "invalid", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "test"}, + }, + want: false, + }, + { + name: "field without dumbql tag", + expr: &query.FieldExpr{ + Field: "IsMember", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "true"}, + }, + want: false, + }, + { + name: "type mismatch", + expr: &query.FieldExpr{ + Field: "age", + Op: query.Equal, + Value: &query.StringLiteral{StringValue: "30"}, + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.expr.Match(target, matcher) + assert.Equal(t, test.want, result) + }) + } +} + +func TestIdentifier_Match(t *testing.T) { //nolint:funlen + tests := []struct { + name string + id query.Identifier + target any + op query.FieldOperator + want bool + }{ + { + name: "equal - match", + id: query.Identifier("test"), + target: "test", + op: query.Equal, + want: true, + }, + { + name: "equal - no match", + id: query.Identifier("test"), + target: "other", + op: query.Equal, + want: false, + }, + { + name: "not equal - match", + id: query.Identifier("test"), + target: "other", + op: query.NotEqual, + want: true, + }, + { + name: "not equal - no match", + id: query.Identifier("test"), + target: "test", + op: query.NotEqual, + want: false, + }, + { + name: "like - match", + id: query.Identifier("world"), + target: "hello world", + op: query.Like, + want: true, + }, + { + name: "like - no match", + id: query.Identifier("universe"), + target: "hello world", + op: query.Like, + want: false, + }, + { + name: "with non-string target", + id: query.Identifier("42"), + target: 42, + op: query.Equal, + want: false, + }, + { + name: "with invalid operator", + id: query.Identifier("test"), + target: "test", + op: query.GreaterThan, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.id.Match(test.target, test.op) + assert.Equal(t, test.want, result) + }) + } +} + +func TestOneOfExpr_Match(t *testing.T) { //nolint:funlen + tests := []struct { + name string + expr *query.OneOfExpr + target any + op query.FieldOperator + want bool + }{ + { + name: "string equal - match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "apple"}, + &query.StringLiteral{StringValue: "banana"}, + &query.StringLiteral{StringValue: "orange"}, + }, + }, + target: "banana", + op: query.Equal, + want: true, + }, + { + name: "string equal - no match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "apple"}, + &query.StringLiteral{StringValue: "banana"}, + &query.StringLiteral{StringValue: "orange"}, + }, + }, + target: "grape", + op: query.Equal, + want: false, + }, + { + name: "integer equal - match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.IntegerLiteral{IntegerValue: 1}, + &query.IntegerLiteral{IntegerValue: 2}, + &query.IntegerLiteral{IntegerValue: 3}, + }, + }, + target: int64(2), + op: query.Equal, + want: true, + }, + { + name: "integer equal - no match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.IntegerLiteral{IntegerValue: 1}, + &query.IntegerLiteral{IntegerValue: 2}, + &query.IntegerLiteral{IntegerValue: 3}, + }, + }, + target: int64(4), + op: query.Equal, + want: false, + }, + { + name: "float equal - match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.NumberLiteral{NumberValue: 1.1}, + &query.NumberLiteral{NumberValue: 2.2}, + &query.NumberLiteral{NumberValue: 3.3}, + }, + }, + target: 2.2, + op: query.Equal, + want: true, + }, + { + name: "float equal - no match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.NumberLiteral{NumberValue: 1.1}, + &query.NumberLiteral{NumberValue: 2.2}, + &query.NumberLiteral{NumberValue: 3.3}, + }, + }, + target: 4.4, + op: query.Equal, + want: false, + }, + { + name: "mixed types", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "one"}, + &query.IntegerLiteral{IntegerValue: 2}, + &query.NumberLiteral{NumberValue: 3.3}, + }, + }, + target: "one", + op: query.Equal, + want: true, + }, + { + name: "empty values", + expr: &query.OneOfExpr{ + Values: []query.Valuer{}, + }, + target: "test", + op: query.Equal, + want: false, + }, + { + name: "nil values", + expr: &query.OneOfExpr{ + Values: nil, + }, + target: "test", + op: query.Equal, + want: false, + }, + { + name: "string like - match", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "world"}, + &query.StringLiteral{StringValue: "universe"}, + }, + }, + target: "hello world", + op: query.Like, + want: true, + }, + { + name: "invalid operator", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "test"}, + }, + }, + target: "test", + op: query.GreaterThan, + want: false, + }, + { + name: "type mismatch", + expr: &query.OneOfExpr{ + Values: []query.Valuer{ + &query.StringLiteral{StringValue: "42"}, + }, + }, + target: 42, + op: query.Equal, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.expr.Match(test.target, test.op) + assert.Equal(t, test.want, result) + }) + } +} diff --git a/query/validation_test.go b/query/validation_test.go index 1502923..d057e87 100644 --- a/query/validation_test.go +++ b/query/validation_test.go @@ -51,7 +51,7 @@ func TestBinaryExpr_Validate(t *testing.T) { //nolint:funlen require.True(t, isNumberLiteral) require.Equal(t, int64(42), integerLiteral.IntegerValue) - require.Equal(t, math.Pi, numberLiteral.NumberValue) + require.InDelta(t, math.Pi, numberLiteral.NumberValue, 0.01) }) t.Run("negative", func(t *testing.T) { @@ -84,7 +84,7 @@ func TestBinaryExpr_Validate(t *testing.T) { //nolint:funlen numberLiteral, isNumberLiteral := fieldExpr.Value.(*query.NumberLiteral) require.True(t, isNumberLiteral) - require.Equal(t, math.Pi, numberLiteral.NumberValue) + require.InDelta(t, math.Pi, numberLiteral.NumberValue, 0.01) }) t.Run("right rule error", func(t *testing.T) { @@ -289,7 +289,7 @@ func TestFieldExpr_Validate(t *testing.T) { //nolint:funlen require.True(t, isNumberLiteral) require.Equal(t, int64(42), integerLiteral.IntegerValue) - require.Equal(t, math.Pi, numberLiteral.NumberValue) + require.InDelta(t, math.Pi, numberLiteral.NumberValue, 0.01) }) })