diff --git a/README.md b/README.md index f3738f9..334ef6f 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,18 @@ var data = Filter{ } ``` +### Filter logic + +Supported logical matching: + +* AND - all of the queries have to match +* OR - only one query has to match +* NOT - none of the queries have to match + ### Explicit match ``` -filter.NewQuery("Bool:true").Filter(&data) +filter.NewQuery("Bool:true", filter.OR).Filter(&data) data = [{str1 42 true {s1 {1}}} {str2 42 true {s2 {2}}}] ``` @@ -46,7 +54,7 @@ data = [{str1 42 true {s1 {1}}} {str2 42 true {s2 {2}}}] ### Wildcard match ``` -filter.NewQuery("String:s*").Filter(&data) +filter.NewQuery("String:s*", filter.OR).Filter(&data) data = [{str1 42 true {s1 {1}}} {str3 42 false {s3 {3}}}] ``` @@ -54,7 +62,7 @@ data = [{str1 42 true {s1 {1}}} {str3 42 false {s3 {3}}}] ### Nested struct match ``` -filter.NewQuery("Struct.Struct.Int:3").Filter(&data) +filter.NewQuery("Struct.Struct.Int:3", filter.OR).Filter(&data) data = [{str3 42 false {s3 {3}}}] ``` @@ -62,7 +70,7 @@ data = [{str3 42 false {s3 {3}}}] ### Multiple matches ``` -filter.NewQuery("Struct.String:*2 Bool:true").Filter(&data) +filter.NewQuery("Struct.String:*2 Bool:true", filter.AND).Filter(&data) data = [{2str 42 true {s2 {2}}}] ``` @@ -71,16 +79,22 @@ data = [{2str 42 true {s2 {2}}}] ``` go test -cpu 1,2,4 --bench . -BenchmarkFilterSimple 2000000 899 ns/op 432 B/op 12 allocs/op -BenchmarkFilterSimple-2 2000000 896 ns/op 432 B/op 12 allocs/op -BenchmarkFilterSimple-4 1000000 1439 ns/op 432 B/op 12 allocs/op -BenchmarkGenericFilterSimple 500000 2438 ns/op 1200 B/op 24 allocs/op -BenchmarkGenericFilterSimple-2 500000 2406 ns/op 1200 B/op 24 allocs/op -BenchmarkGenericFilterSimple-4 500000 2645 ns/op 1200 B/op 24 allocs/op -BenchmarkFilterComplex 500000 2929 ns/op 672 B/op 42 allocs/op -BenchmarkFilterComplex-2 500000 2918 ns/op 672 B/op 42 allocs/op -BenchmarkFilterComplex-4 500000 5574 ns/op 672 B/op 42 allocs/op -BenchmarkGenericFilterComplex 200000 7343 ns/op 1680 B/op 84 allocs/op -BenchmarkGenericFilterComplex-2 200000 7400 ns/op 1680 B/op 84 allocs/op -BenchmarkGenericFilterComplex-4 200000 11979 ns/op 1680 B/op 84 allocs/op +BenchmarkFilterSimple 2000000 877 ns/op 432 B/op 12 allocs/op +BenchmarkFilterSimple-2 2000000 865 ns/op 432 B/op 12 allocs/op +BenchmarkFilterSimple-4 1000000 1713 ns/op 432 B/op 12 allocs/op +BenchmarkFilterComplex 500000 3032 ns/op 672 B/op 48 allocs/op +BenchmarkFilterComplex-2 500000 3100 ns/op 672 B/op 48 allocs/op +BenchmarkFilterComplex-4 500000 6235 ns/op 672 B/op 48 allocs/op +BenchmarkFilterComplexOr 500000 2649 ns/op 816 B/op 39 allocs/op +BenchmarkFilterComplexOr-2 500000 2659 ns/op 816 B/op 39 allocs/op +BenchmarkFilterComplexOr-4 500000 5548 ns/op 816 B/op 39 allocs/op +BenchmarkGenericFilterSimple 500000 2340 ns/op 1200 B/op 24 allocs/op +BenchmarkGenericFilterSimple-2 1000000 2307 ns/op 1200 B/op 24 allocs/op +BenchmarkGenericFilterSimple-4 1000000 3615 ns/op 1200 B/op 24 allocs/op +BenchmarkGenericFilterComplex 200000 7073 ns/op 1680 B/op 96 allocs/op +BenchmarkGenericFilterComplex-2 200000 7598 ns/op 1680 B/op 96 allocs/op +BenchmarkGenericFilterComplex-4 200000 10735 ns/op 1680 B/op 96 allocs/op +BenchmarkGenericFilterComplexOr 200000 5406 ns/op 2456 B/op 61 allocs/op +BenchmarkGenericFilterComplexOr-2 300000 5300 ns/op 2456 B/op 61 allocs/op +BenchmarkGenericFilterComplexOr-4 200000 7707 ns/op 2456 B/op 61 allocs/op ``` diff --git a/cmd/main.go b/cmd/main.go index ea88442..2e9d9a9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -36,16 +36,19 @@ var data = Filter{ {"str1", 42, true, Struct1{"s1", Struct2{1}}}, {"2str", 42, true, Struct1{"s2", Struct2{2}}}, {"str3", 42, false, Struct1{"s3", Struct2{3}}}, + {":", 42, false, Struct1{"s3", Struct2{3}}}, } func main() { - f := filter.NewQuery("String:s*").GenericFilter(data) - if f != nil { - for _, v := range f.(Filter) { + f, _ := filter.NewQuery("String:s*", filter.AND) + d := f.GenericFilter(data) + if d != nil { + for _, v := range d.(Filter) { fmt.Println("Generic:", v) } } - filter.NewQuery("String:s*").Filter(&data) + f, _ = filter.NewQuery("Struct.Struct.Int:1 Struct.Struct.Int:2 String::", filter.OR) + f.Filter(&data) for _, v := range data { fmt.Println("Interface:", v) } diff --git a/filter.go b/filter.go index 8647ecf..71235f0 100644 --- a/filter.go +++ b/filter.go @@ -4,6 +4,7 @@ package filter import ( + "errors" "reflect" "strconv" "strings" @@ -22,10 +23,21 @@ type match struct { value string } -// Query implements the filter query -type Query []match +type Logic int -// NewQuery parses the filter query. +const ( + AND Logic = iota + OR + NOT +) + +// Query implements the filter query +type Query struct { + m []match + l Logic +} + +// NewQuery parses the filter query and match logic // Examples: // Explicit match: : // Wildcard match: @@ -38,34 +50,79 @@ type Query []match // : :* // Supported data types are: string, int, bool // Bool does not support the wildcard operator -func NewQuery(s string) (q Query) { +func NewQuery(s string, l Logic) (q *Query, err error) { + if l > 2 || l < 0 { + l = AND + } + q = &Query{l: l} + qm := make(map[string]struct{}) fields := strings.Split(s, " ") for _, v := range fields { - m := strings.Split(v, ":") - if len(m) == 2 { - q = append(q, match{strings.Split(m[0], "."), m[1]}) + if v == "" { + continue + } + m := strings.SplitN(v, ":", 2) + if len(m) != 2 { + err = errors.New("invalid match: " + v) + return + } + if l == AND { + if _, ok := qm[m[0]]; ok { + err = errors.New("field for AND matching used more than once: " + m[0]) + return + } + qm[m[0]] = struct{}{} + } + q.m = append(q.m, match{strings.Split(m[0], "."), m[1]}) + } + return +} + +func (q *Query) check(oks []bool) (b bool) { + b = true + switch q.l { + case AND: + for _, ok := range oks { + if !ok { + b = false + } + } + case NOT: + for _, ok := range oks { + if ok { + b = false + } } } return } -func (q Query) match(v reflect.Value) (ok bool) { +func (q *Query) match(v reflect.Value) bool { + if q.l == OR { + return q.orMatch(v) + } + return q.allMatch(v) +} + +func (q *Query) allMatch(v reflect.Value) (ok bool) { if v.Kind() != reflect.Struct { return } - if len(q) == 0 { + ql := len(q.m) + if ql == 0 { return } - ok = true - for _, qv := range q { + oks := make([]bool, ql) + for i, qv := range q.m { s := "" fl := len(qv.fields) + vl := len(qv.value) if fl == 0 { continue } val := v.FieldByName(qv.fields[0]) - for i := 1; i < fl; i++ { - val = val.FieldByName(qv.fields[i]) + for j := 1; j < fl; j++ { + val = val.FieldByName(qv.fields[j]) } switch val.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, @@ -74,50 +131,121 @@ func (q Query) match(v reflect.Value) (ok bool) { case reflect.String: s = val.String() case reflect.Bool: - if val.Bool() { - s = "true" - } else { - s = "false" + if vl == 0 { + continue } - if s != qv.value { - ok = false + b := val.Bool() + if b && qv.value[0] == 't' { + oks[i] = true + continue + } + if !b && qv.value[0] == 'f' { + oks[i] = true } continue default: - ok = false continue } - vl := len(qv.value) switch { case vl >= 3: if qv.value[0] == any && qv.value[vl-1] == any { if strings.Contains(s, qv.value[1:vl-1]) { + oks[i] = true continue } } fallthrough case vl >= 2: - if qv.value[0] == any { + switch { + case qv.value[0] == any: if strings.HasSuffix(s, qv.value[1:]) { + oks[i] = true continue } - } - if qv.value[vl-1] == any { + case qv.value[vl-1] == any: if strings.HasPrefix(s, qv.value[:vl-1]) { + oks[i] = true continue } } } - if s != qv.value { - ok = false + if s == qv.value { + oks[i] = true } } - return + return q.check(oks) +} + +func (q *Query) orMatch(v reflect.Value) bool { + if v.Kind() != reflect.Struct { + return false + } + ql := len(q.m) + if ql == 0 { + return false + } + for _, qv := range q.m { + s := "" + fl := len(qv.fields) + vl := len(qv.value) + if fl == 0 { + continue + } + val := v.FieldByName(qv.fields[0]) + for j := 1; j < fl; j++ { + val = val.FieldByName(qv.fields[j]) + } + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + s = strconv.Itoa(int(val.Int())) + case reflect.String: + s = val.String() + case reflect.Bool: + if vl == 0 { + continue + } + b := val.Bool() + if b && qv.value[0] == 't' { + return true + } + if !b && qv.value[0] == 'f' { + return true + } + continue + default: + continue + } + switch { + case vl >= 3: + if qv.value[0] == any && qv.value[vl-1] == any { + if strings.Contains(s, qv.value[1:vl-1]) { + return true + } + } + fallthrough + case vl >= 2: + switch { + case qv.value[0] == any: + if strings.HasSuffix(s, qv.value[1:]) { + return true + } + case qv.value[vl-1] == any: + if strings.HasPrefix(s, qv.value[:vl-1]) { + return true + } + } + } + if s == qv.value { + return true + } + } + return false } // GenericFilter takes any slice of structs value and returns the same slice type containing only the filter matches. // Returns nil if interface{} is not a slice. -func (q Query) GenericFilter(i interface{}) interface{} { +func (q *Query) GenericFilter(i interface{}) interface{} { v := reflect.ValueOf(i) if v.Kind() != reflect.Slice { return nil diff --git a/filter_test.go b/filter_test.go index dfad7e1..c779075 100644 --- a/filter_test.go +++ b/filter_test.go @@ -54,9 +54,25 @@ var ( } ) +func TestNewQuery(t *testing.T) { + _, err := NewQuery("String:1 String:2", AND) + if err == nil { + t.Error("multiple AND fields") + } + + _, err = NewQuery("String:1 sdcs Int:1", AND) + if err == nil { + t.Error("invalid match") + } +} + func TestFilter(t *testing.T) { f1 := makefilter(data) - NewQuery("String:*st* Bool:true").Filter(&f1) + q1, err := NewQuery("String:*st* Bool:true", AND) + if err != nil { + t.Error(err) + } + q1.Filter(&f1) if len(f1) != 2 { t.Error("f1 len is:", len(f1)) } @@ -66,22 +82,47 @@ func TestFilter(t *testing.T) { if f1[1] != data[1] { t.Error("f1[1] != data[1]") } + f2 := makefilter(data) - NewQuery("Struct.Struct.Int:3").Filter(&f2) - if len(f2) != 1 { + q2, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", OR) + if err != nil { + t.Error(err) + } + q2.Filter(&f2) + if len(f2) != 2 { t.Error("f2 len is:", len(f2)) } - if f2[0] != data[2] { - t.Error("f1[0] != data[2]") + if f2[0] != data[1] { + t.Error("f2[0] != data[1]") + } + if f2[1] != data[2] { + t.Error("f2[1] != data[2]") + } + + f3 := makefilter(data) + q3, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", NOT) + if err != nil { + t.Error(err) + } + q3.Filter(&f3) + if len(f3) != 1 { + t.Error("f3 len is:", len(f3)) + } + if f3[0] != data[0] { + t.Error("f3[0] != data[0]") } } func TestGenericFilter(t *testing.T) { - i1 := NewQuery("String:*st* Bool:true").GenericFilter(data) - if i1 == nil { - t.Error("i1 is nil") + q1, err := NewQuery("String:*st* Bool:true", AND) + if err != nil { + t.Error(err) } - f1 := i1.(filter) + a1 := q1.GenericFilter(data) + if a1 == nil { + t.Error("a1 is nil") + } + f1 := a1.(filter) if len(f1) != 2 { t.Error("f1 len is:", len(f1)) } @@ -91,22 +132,73 @@ func TestGenericFilter(t *testing.T) { if f1[1] != data[1] { t.Error("f1[1] != data[1]") } - i2 := NewQuery("Struct.Struct.Int:3").GenericFilter(data) - if i2 == nil { - t.Error("i2 is nil") + + q2, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", OR) + if err != nil { + t.Error(err) } - f2 := i2.(filter) - if len(f2) != 1 { + a2 := q2.GenericFilter(data) + if a2 == nil { + t.Error("a2 is nil") + } + f2 := a2.(filter) + if len(f2) != 2 { t.Error("f2 len is:", len(f2)) } - if f2[0] != data[2] { - t.Error("f1[0] != data[2]") + if f2[0] != data[1] { + t.Error("f2[0] != data[1]") + } + if f2[1] != data[2] { + t.Error("f2[1] != data[2]") + } + + q3, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", NOT) + if err != nil { + t.Error(err) + } + a3 := q3.GenericFilter(data) + if a3 == nil { + t.Error("a3 is nil") + } + f3 := a3.(filter) + if len(f3) != 1 { + t.Error("f3 len is:", len(f3)) + } + if f3[0] != data[0] { + t.Error("f3[0] != data[0]") } } func BenchmarkFilterSimple(b *testing.B) { b.ReportAllocs() - q := NewQuery("Bool:true") + q, err := NewQuery("Bool:true", OR) + if err != nil { + b.Error(err) + } + fd := makefilter(benchData) + for i := 0; i < b.N; i++ { + q.Filter(&fd) + } +} + +func BenchmarkFilterComplex(b *testing.B) { + b.ReportAllocs() + q, err := NewQuery("String:*st* Bool:false Struct.Struct.Int:1", AND) + if err != nil { + b.Error(err) + } + fd := makefilter(benchData) + for i := 0; i < b.N; i++ { + q.Filter(&fd) + } +} + +func BenchmarkFilterComplexOr(b *testing.B) { + b.ReportAllocs() + q, err := NewQuery("Int:47 Bool:false Struct.String:s1*", OR) + if err != nil { + b.Error(err) + } fd := makefilter(benchData) for i := 0; i < b.N; i++ { q.Filter(&fd) @@ -115,24 +207,32 @@ func BenchmarkFilterSimple(b *testing.B) { func BenchmarkGenericFilterSimple(b *testing.B) { b.ReportAllocs() - q := NewQuery("Bool:true") + q, err := NewQuery("Bool:true", OR) + if err != nil { + b.Error(err) + } for i := 0; i < b.N; i++ { q.GenericFilter(benchData) } } -func BenchmarkFilterComplex(b *testing.B) { - b.ReportAllocs() - q := NewQuery("String:*st* Bool:false Struct.Struct.Int:1") - fd := makefilter(benchData) - for i := 0; i < b.N; i++ { - q.Filter(&fd) - } -} - func BenchmarkGenericFilterComplex(b *testing.B) { b.ReportAllocs() - q := NewQuery("String:*st* Bool:false Struct.Struct.Int:1") + q, err := NewQuery("String:*st* Bool:false Struct.Struct.Int:1", AND) + if err != nil { + b.Error(err) + } + for i := 0; i < b.N; i++ { + q.GenericFilter(benchData) + } +} + +func BenchmarkGenericFilterComplexOr(b *testing.B) { + b.ReportAllocs() + q, err := NewQuery("Int:47 Bool:false Struct.String:s1*", OR) + if err != nil { + b.Error(err) + } for i := 0; i < b.N; i++ { q.GenericFilter(benchData) }