added match logic and query error checking

This commit is contained in:
ston1th 2017-07-09 15:09:39 +02:00
commit 5a8e1e6cf6
4 changed files with 315 additions and 70 deletions

View file

@ -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
```

View file

@ -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)
}

184
filter.go
View file

@ -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: <struct field>:<struct value>
// Wildcard match:
@ -38,34 +50,79 @@ type Query []match
// <struct field>:<struct value> <struct field>:<struct value>*
// 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

View file

@ -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)
}