Skip to content
This repository has been archived by the owner on Feb 11, 2025. It is now read-only.

Commit

Permalink
Refactor and implement dumbql:"-" (omit) struct tag
Browse files Browse the repository at this point in the history
  • Loading branch information
tomakado committed Feb 11, 2025
1 parent 0b5bfae commit c2a063f
Show file tree
Hide file tree
Showing 7 changed files with 932 additions and 714 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Simple (dumb) query language and parser for Go.
- One-of/In expressions (`occupation = [designer, "ux analyst"]`)
- Schema validation
- Drop-in usage with [squirrel](https://github.com/Masterminds/squirrel) query builder or SQL drivers directly
- Struct matching with `dumbql` struct tag

## Examples

Expand Down Expand Up @@ -143,6 +144,7 @@ import (
"fmt"

"github.com/defer-panic/dumbql"
"github.com/defer-panic/dumbql/match"
"github.com/defer-panic/dumbql/query"
)

Expand Down Expand Up @@ -196,7 +198,7 @@ func main() {
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

filtered := make([]User, 0, len(users))

Expand Down
73 changes: 73 additions & 0 deletions match/match_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package match_test

import (
"fmt"

"github.com/defer-panic/dumbql/match"
"github.com/defer-panic/dumbql/query"
)

type MatchUser struct {
ID int64 `dumbql:"id"`
Name string `dumbql:"name"`
Age int64 `dumbql:"age"`
Score float64 `dumbql:"score"`
Location string `dumbql:"location"`
Role string `dumbql:"role"`
}

func Example() {
users := []MatchUser{
{
ID: 1,
Name: "John Doe",
Age: 30,
Score: 4.5,
Location: "New York",
Role: "admin",
},
{
ID: 2,
Name: "Jane Smith",
Age: 25,
Score: 3.8,
Location: "Los Angeles",
Role: "user",
},
{
ID: 3,
Name: "Bob Johnson",
Age: 35,
Score: 4.2,
Location: "Chicago",
Role: "user",
},
// This one will be dropped:
{
ID: 4,
Name: "Alice Smith",
Age: 25,
Score: 3.8,
Location: "Los Angeles",
Role: "admin",
},
}

q := `(age >= 30 and score > 4.0) or (location:"Los Angeles" and role:"user")`
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)

matcher := &match.StructMatcher{}

filtered := make([]MatchUser, 0, len(users))

for _, user := range users {
if expr.Match(&user, matcher) {
filtered = append(filtered, user)
}
}

fmt.Println(filtered)
// Output:
// [{1 John Doe 30 4.5 New York admin} {2 Jane Smith 25 3.8 Los Angeles user} {3 Bob Johnson 35 4.2 Chicago user}]
}
67 changes: 67 additions & 0 deletions match/struct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package match

import (
"reflect"

"github.com/defer-panic/dumbql/query"
)

// StructMatcher is a basic implementation of the Matcher interface for evaluating query expressions against structs.
// It supports struct tags using the `dumbql` tag name, which allows you to specify a custom field name.
type StructMatcher struct{}

func (m *StructMatcher) MatchAnd(target any, left, right query.Expr) bool {
return left.Match(target, m) && right.Match(target, m)
}

func (m *StructMatcher) MatchOr(target any, left, right query.Expr) bool {
return left.Match(target, m) || right.Match(target, m)
}

func (m *StructMatcher) MatchNot(target any, expr query.Expr) bool {
return !expr.Match(target, m)
}

// MatchField matches a field in the target struct using the provided value and operator. It supports struct tags using
// the `dumbql` tag name, which allows you to specify a custom field name. If struct tag is not provided, it will use
// the field name as is.
func (m *StructMatcher) MatchField(target any, field string, value query.Valuer, op query.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)

tag := f.Tag.Get("dumbql")
if tag == "-" {
return true // Field marked with dumbql:"-" always match (in other words does not affect the result)
}

fname := f.Name
if tag != "" {
fname = tag
}

if fname == field {
v := reflect.ValueOf(target)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}

return m.MatchValue(v.Field(i).Interface(), value, op)
}
}

return true
}

func (m *StructMatcher) MatchValue(target any, value query.Valuer, op query.FieldOperator) bool {
return value.Match(target, op)
}
85 changes: 66 additions & 19 deletions match_example_test.go → match/struct_example_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package dumbql_test
package match_test

import (
"fmt"

"github.com/defer-panic/dumbql/match"
"github.com/defer-panic/dumbql/query"
)

Expand All @@ -15,7 +16,7 @@ type User struct {
Role string `dumbql:"role"`
}

func Example_simpleMatching() {
func ExampleStructMatcher_MatchField_simpleMatching() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -31,14 +32,14 @@ func Example_simpleMatching() {
expr := ast.(query.Expr)

// Create a matcher
matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}
result := expr.Match(user, matcher)

fmt.Printf("%s: %v\n", q, result)
// Output: name = "John Doe": true
}

func Example_complexMatching() {
func ExampleStructMatcher_MatchField_complexMatching() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -54,14 +55,14 @@ func Example_complexMatching() {
expr := ast.(query.Expr)

// Create a matcher
matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}
result := expr.Match(user, matcher)

fmt.Printf("%s: %v\n", q, result)
// Output: age >= 25 and location:["New York", "Los Angeles"] and score > 4.0: true
}

func Example_numericComparisons() {
func ExampleStructMatcher_MatchField_numericComparisons() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -81,7 +82,7 @@ func Example_numericComparisons() {
`score < 5.0`,
}

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
Expand All @@ -97,7 +98,7 @@ func Example_numericComparisons() {
// Query 'score < 5.0' match result: true
}

func Example_stringOperations() {
func ExampleStructMatcher_MatchField_stringOperations() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -115,7 +116,7 @@ func Example_stringOperations() {
`role:admin`,
}

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
Expand All @@ -130,7 +131,7 @@ func Example_stringOperations() {
// Query 'role:admin' match result: true
}

func Example_notExpressions() {
func ExampleStructMatcher_MatchField_notExpressions() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -147,7 +148,7 @@ func Example_notExpressions() {
`not (role:"user" and score < 3.0)`,
}

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
Expand All @@ -161,7 +162,7 @@ func Example_notExpressions() {
// Query 'not (role:"user" and score < 3.0)' match result: true
}

func Example_multiMatch() {
func ExampleStructMatcher_MatchField_multiMatch() {
users := []User{
{
ID: 1,
Expand Down Expand Up @@ -202,7 +203,7 @@ func Example_multiMatch() {
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

filtered := make([]User, 0, len(users))

Expand All @@ -217,7 +218,7 @@ func Example_multiMatch() {
// [{1 John Doe 30 4.5 New York admin} {2 Jane Smith 25 3.8 Los Angeles user} {3 Bob Johnson 35 4.2 Chicago user}]
}

func Example_oneOfExpression() {
func ExampleStructMatcher_MatchField_oneOfExpression() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -234,7 +235,7 @@ func Example_oneOfExpression() {
`age:[25, 30, 35]`,
}

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
Expand All @@ -248,7 +249,7 @@ func Example_oneOfExpression() {
// Query 'age:[25, 30, 35]' match result: true
}

func Example_edgeCases() {
func ExampleStructMatcher_MatchField_edgeCases() {
user := &User{
ID: 1,
Name: "John Doe",
Expand All @@ -271,19 +272,65 @@ func Example_edgeCases() {
`(age > 20 and age < 40) and (score >= 4.0 or role:"admin")`,
}

matcher := &query.DefaultMatcher{}
matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)
result := expr.Match(user, matcher)
fmt.Printf("Query '%s' match result: %v\n", q, result)
}

// Output:
// Query 'nonexistent:"value"' match result: false
// Query 'nonexistent:"value"' match result: true
// Query 'age:"not a number"' match result: false
// Query 'name:""' match result: false
// Query 'score:0' match result: false
// Query '(age > 20 and age < 40) and (score >= 4.0 or role:"admin")' match result: true
}

func ExampleStructMatcher_MatchField_structTagOmit() {
type User struct {
ID int64 `dumbql:"id"`
Name string `dumbql:"name"`
Password string `dumbql:"-"` // Omitted from querying
Internal bool `dumbql:"-"` // Omitted from querying
Score float64 `dumbql:"score"`
}

user := &User{
ID: 1,
Name: "John",
Password: "secret123",
Internal: true,
Score: 4.5,
}

// Test various queries including omitted fields
queries := []string{
// Query against visible field
`id:1`,
// Query against omitted field - always matches
`password:"wrong_password"`,
// Query against omitted boolean field - always matches
`internal:false`,
// Combined visible and omitted fields
`id:1 and password:"wrong_password"`,
// Complex query with omitted fields
`(id:1 or score > 4.0) and (password:"wrong" or internal:false)`,
}

matcher := &match.StructMatcher{}

for _, q := range queries {
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)
result := expr.Match(user, matcher)
fmt.Printf("Query '%s' match result: %v\n", q, result)
}
// Output:
// Query 'id:1' match result: true
// Query 'password:"wrong_password"' match result: true
// Query 'internal:false' match result: true
// Query 'id:1 and password:"wrong_password"' match result: true
// Query '(id:1 or score > 4.0) and (password:"wrong" or internal:false)' match result: true
}
Loading

0 comments on commit c2a063f

Please sign in to comment.