added match logic and query error checking
This commit is contained in:
parent
e309b478f5
commit
5a8e1e6cf6
4 changed files with 315 additions and 70 deletions
46
README.md
46
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
|
||||
```
|
||||
|
|
|
|||
11
cmd/main.go
11
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)
|
||||
}
|
||||
|
|
|
|||
184
filter.go
184
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: <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
|
||||
|
|
|
|||
156
filter_test.go
156
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue