280 lines
5.1 KiB
Go
280 lines
5.1 KiB
Go
// Copyright (C) 2017 Marius Schellenberger
|
|
|
|
// Package filter implements simple struct field filtering
|
|
package filter
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const any = '*'
|
|
|
|
type Filter interface {
|
|
Len() int
|
|
Index(int) interface{}
|
|
Remove(int, int)
|
|
}
|
|
|
|
type match struct {
|
|
fields []string
|
|
value string
|
|
}
|
|
|
|
type Logic int
|
|
|
|
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:
|
|
// <struct field>:*<part of struct value>
|
|
// <struct field>:<part of struct value>*
|
|
// <struct field>:*<middl of struct value>*
|
|
// Nested structs:
|
|
// <struct field>.<struct field>:<struct value>
|
|
// Multiple matches:
|
|
// <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, 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 {
|
|
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) 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
|
|
}
|
|
ql := len(q.m)
|
|
if ql == 0 {
|
|
return
|
|
}
|
|
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 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' {
|
|
oks[i] = true
|
|
continue
|
|
}
|
|
if !b && qv.value[0] == 'f' {
|
|
oks[i] = 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]) {
|
|
oks[i] = true
|
|
continue
|
|
}
|
|
}
|
|
fallthrough
|
|
case vl >= 2:
|
|
switch {
|
|
case qv.value[0] == any:
|
|
if strings.HasSuffix(s, qv.value[1:]) {
|
|
oks[i] = true
|
|
continue
|
|
}
|
|
case qv.value[vl-1] == any:
|
|
if strings.HasPrefix(s, qv.value[:vl-1]) {
|
|
oks[i] = true
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if s == qv.value {
|
|
oks[i] = true
|
|
}
|
|
}
|
|
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{} {
|
|
v := reflect.ValueOf(i)
|
|
if v.Kind() != reflect.Slice {
|
|
return nil
|
|
}
|
|
ret := reflect.MakeSlice(v.Type(), 0, 0)
|
|
for i := 0; i < v.Len(); i++ {
|
|
val := v.Index(i)
|
|
if q.match(val) {
|
|
ret = reflect.Append(ret, val)
|
|
}
|
|
}
|
|
return ret.Interface()
|
|
}
|
|
|
|
func (q Query) Filter(f Filter) {
|
|
count := 0
|
|
i := 0
|
|
for ; i < f.Len(); i++ {
|
|
if q.match(reflect.ValueOf(f.Index(i))) {
|
|
if count > 0 {
|
|
f.Remove(i-count, i)
|
|
i -= count
|
|
}
|
|
count = 0
|
|
} else {
|
|
count++
|
|
}
|
|
}
|
|
if count > 0 {
|
|
f.Remove(i-count, i)
|
|
}
|
|
}
|