306 lines
5.7 KiB
Go
306 lines
5.7 KiB
Go
// Copyright (C) 2018 Marius Schellenberger
|
|
|
|
// Package filter implements simple struct field filtering
|
|
package filter
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Filter is the filter interface to be implemented for faster non generic filtering
|
|
// Example implementation:
|
|
// type f []struct
|
|
// func (v f) Len() int { return len(v) }
|
|
// func (v f) Index(i int) interface{} { return v[i] }
|
|
// func (v *f) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) }
|
|
type Filter interface {
|
|
Len() int
|
|
Index(int) interface{}
|
|
Remove(int, int)
|
|
}
|
|
|
|
type match struct {
|
|
fields []string
|
|
mf func(string) bool
|
|
bf func(bool) bool
|
|
}
|
|
|
|
type sorter struct {
|
|
enabled bool
|
|
only bool
|
|
desc bool
|
|
fields []string
|
|
}
|
|
|
|
const (
|
|
desc = "desc"
|
|
asc = "asc"
|
|
)
|
|
|
|
type logic int
|
|
|
|
const (
|
|
undefined logic = iota
|
|
and
|
|
or
|
|
not
|
|
)
|
|
|
|
func parseLogic(s string) (l logic, ok bool) {
|
|
ok = true
|
|
switch s {
|
|
case "AND":
|
|
l = and
|
|
case "OR":
|
|
l = or
|
|
case "NOT":
|
|
l = not
|
|
default:
|
|
ok = false
|
|
l = and
|
|
}
|
|
return
|
|
}
|
|
|
|
// Query is the filter query
|
|
type Query struct {
|
|
q string
|
|
m []match
|
|
l logic
|
|
s sorter
|
|
}
|
|
|
|
// NewQuery parses the filter/sort query and match logic
|
|
// If the query does not start with a filter logic (AND, OR, NOT), it defaults to AND.
|
|
// Examples:
|
|
// Explicit match: <struct field>:<struct value>
|
|
// Wildcard match (use ** to match the '*' character):
|
|
// <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:
|
|
// Multiple statemenmts are separated by a single whitespace character: ' '
|
|
// <struct field>:<struct value> <struct field>:<struct value>*
|
|
// Supported data types are: string, int, float, bool
|
|
// Bool does not support the wildcard operator
|
|
// Sorting:
|
|
// The sort statement consists of the literal string 'sort', the field name (nesting like above is supported) and the literal strings 'asc' or 'desc'.
|
|
// Examples:
|
|
// sort:<struct field>:asc
|
|
// sort:<struct field>:desc
|
|
// sort:<struct field>.<struct field>:desc
|
|
func NewQuery(s string) (q *Query, err error) {
|
|
q = &Query{q: s}
|
|
if s == "" {
|
|
err = errEmpty
|
|
return
|
|
}
|
|
qm := make(map[string]struct{})
|
|
fields := clearFields(s)
|
|
if len(fields) == 0 {
|
|
err = errEmpty
|
|
return
|
|
}
|
|
for _, v := range fields {
|
|
if q.l == undefined {
|
|
l, ok := parseLogic(v)
|
|
q.l = l
|
|
if ok {
|
|
continue
|
|
}
|
|
}
|
|
if !q.s.enabled {
|
|
s := strings.SplitN(v, ":", 3)
|
|
if s[0] == "sort" && len(s) >= 2 {
|
|
q.s.fields = strings.Split(s[1], ".")
|
|
if err = checkFields(q.s.fields, v); err != nil {
|
|
return
|
|
}
|
|
if len(s) == 3 {
|
|
switch s[2] {
|
|
case desc:
|
|
q.s.desc = true
|
|
case asc:
|
|
default:
|
|
err = newErr("invalid sort: " + v)
|
|
return
|
|
}
|
|
}
|
|
q.s.enabled = true
|
|
continue
|
|
}
|
|
}
|
|
m := strings.SplitN(v, ":", 2)
|
|
if len(m) != 2 {
|
|
err = newErr("invalid match: " + v)
|
|
return
|
|
}
|
|
if q.l == and {
|
|
if _, ok := qm[m[0]]; ok {
|
|
err = newErr("field for AND matching used more than once: " + m[0])
|
|
return
|
|
}
|
|
qm[m[0]] = struct{}{}
|
|
}
|
|
f := strings.Split(m[0], ".")
|
|
if err = checkFields(f, m[0]); err != nil {
|
|
return
|
|
}
|
|
q.m = append(q.m, match{
|
|
f,
|
|
matchFunc(m[1]),
|
|
boolFunc(m[1]),
|
|
})
|
|
}
|
|
if len(q.m) == 0 && q.s.enabled {
|
|
q.s.only = true
|
|
} else if len(q.m) == 0 {
|
|
err = errEmpty
|
|
}
|
|
if len(q.m) == 1 && q.l != not {
|
|
q.l = or
|
|
}
|
|
return
|
|
}
|
|
|
|
func (q *Query) String() string {
|
|
return q.q
|
|
}
|
|
|
|
func (q *Query) check(oks []bool) (b bool) {
|
|
b = true
|
|
switch q.l {
|
|
case and:
|
|
for _, ok := range oks {
|
|
if !ok {
|
|
b = false
|
|
return
|
|
}
|
|
}
|
|
case or:
|
|
b = false
|
|
case not:
|
|
for _, ok := range oks {
|
|
if ok {
|
|
b = false
|
|
return
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (q *Query) match(v reflect.Value) (ok bool) {
|
|
if v.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
ql := len(q.m)
|
|
if ql == 0 {
|
|
return
|
|
}
|
|
var (
|
|
oks []bool
|
|
match bool
|
|
)
|
|
if q.l != or {
|
|
oks = make([]bool, ql)
|
|
}
|
|
for i, qv := range q.m {
|
|
fl := len(qv.fields)
|
|
if fl == 0 {
|
|
continue
|
|
}
|
|
val := walkFields(v.FieldByName(qv.fields[0]), qv.fields[1:])
|
|
if val.Kind() == reflect.Invalid {
|
|
continue
|
|
}
|
|
s, b, err := getValue(val)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
switch s {
|
|
case "":
|
|
match = qv.bf(b)
|
|
default:
|
|
match = qv.mf(s)
|
|
}
|
|
if q.l == or {
|
|
if match {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
oks[i] = match
|
|
}
|
|
return q.check(oks)
|
|
}
|
|
|
|
// 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.
|
|
// GenericFilter does not support sorting.
|
|
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) sort(f Filter) {
|
|
if f.Len() == 0 {
|
|
return
|
|
}
|
|
l := makeLess(f, q.s.fields)
|
|
if l == nil {
|
|
return
|
|
}
|
|
if q.s.desc {
|
|
l = reverse(l)
|
|
}
|
|
sort.SliceStable(reflect.Indirect(reflect.ValueOf(f)).Interface(), l)
|
|
}
|
|
|
|
// Filter takes a pointer to the Filter interface as an argument.
|
|
// The original slice is modified in the filtering/sorting process.
|
|
func (q Query) Filter(f Filter) error {
|
|
v := reflect.ValueOf(f)
|
|
if v.Kind() != reflect.Ptr {
|
|
return newErr("non-pointer " + v.Type().String())
|
|
}
|
|
|
|
if q.s.enabled && q.s.only {
|
|
q.sort(f)
|
|
return nil
|
|
}
|
|
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)
|
|
}
|
|
if q.s.enabled {
|
|
q.sort(f)
|
|
}
|
|
return nil
|
|
}
|