initial commit
This commit is contained in:
commit
e309b478f5
5 changed files with 453 additions and 0 deletions
24
LICENSE
Normal file
24
LICENSE
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Copyright (C) 2017 Marius Schellenberger
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* The names of the authors and/or contributors may not be used to
|
||||
endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
86
README.md
Normal file
86
README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# filter - a simple struct field filter for go
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
// Example data structure
|
||||
type Example struct {
|
||||
String string
|
||||
Int int
|
||||
Bool bool
|
||||
Struct Struct1
|
||||
}
|
||||
|
||||
type Struct1 struct {
|
||||
String string
|
||||
Struct Struct2
|
||||
}
|
||||
|
||||
type Struct2 struct {
|
||||
Int int
|
||||
}
|
||||
|
||||
// Filter implements the filter.Filter interface
|
||||
type Filter []Example
|
||||
|
||||
func (v Filter) Len() int { return len(v) }
|
||||
func (v Filter) Index(i int) interface{} { return v[i] }
|
||||
func (v *Filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) }
|
||||
|
||||
// Example data
|
||||
var data = Filter{
|
||||
{"str1", 42, true, Struct1{"s1", Struct2{1}}},
|
||||
{"2str", 42, true, Struct1{"s2", Struct2{2}}},
|
||||
{"str3", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
}
|
||||
```
|
||||
|
||||
### Explicit match
|
||||
|
||||
```
|
||||
filter.NewQuery("Bool:true").Filter(&data)
|
||||
|
||||
data = [{str1 42 true {s1 {1}}} {str2 42 true {s2 {2}}}]
|
||||
```
|
||||
|
||||
### Wildcard match
|
||||
|
||||
```
|
||||
filter.NewQuery("String:s*").Filter(&data)
|
||||
|
||||
data = [{str1 42 true {s1 {1}}} {str3 42 false {s3 {3}}}]
|
||||
```
|
||||
|
||||
### Nested struct match
|
||||
|
||||
```
|
||||
filter.NewQuery("Struct.Struct.Int:3").Filter(&data)
|
||||
|
||||
data = [{str3 42 false {s3 {3}}}]
|
||||
```
|
||||
|
||||
### Multiple matches
|
||||
|
||||
```
|
||||
filter.NewQuery("Struct.String:*2 Bool:true").Filter(&data)
|
||||
|
||||
data = [{2str 42 true {s2 {2}}}]
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
52
cmd/main.go
Normal file
52
cmd/main.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.giftfish.de/ston1th/filter"
|
||||
)
|
||||
|
||||
// Example data structure
|
||||
type Example struct {
|
||||
String string
|
||||
Int int
|
||||
Bool bool
|
||||
Struct Struct1
|
||||
}
|
||||
|
||||
type Struct1 struct {
|
||||
String string
|
||||
Struct Struct2
|
||||
}
|
||||
|
||||
type Struct2 struct {
|
||||
Int int
|
||||
}
|
||||
|
||||
// Filter implements the filter.Filter interface
|
||||
type Filter []Example
|
||||
|
||||
func (v Filter) Len() int { return len(v) }
|
||||
func (v Filter) Index(i int) interface{} { return v[i] }
|
||||
func (v *Filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) }
|
||||
|
||||
// Example data
|
||||
var data = Filter{
|
||||
{"str1", 42, true, Struct1{"s1", Struct2{1}}},
|
||||
{"2str", 42, true, Struct1{"s2", Struct2{2}}},
|
||||
{"str3", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
}
|
||||
|
||||
func main() {
|
||||
f := filter.NewQuery("String:s*").GenericFilter(data)
|
||||
if f != nil {
|
||||
for _, v := range f.(Filter) {
|
||||
fmt.Println("Generic:", v)
|
||||
}
|
||||
}
|
||||
filter.NewQuery("String:s*").Filter(&data)
|
||||
for _, v := range data {
|
||||
fmt.Println("Interface:", v)
|
||||
}
|
||||
}
|
||||
152
filter.go
Normal file
152
filter.go
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
// Package filter implements simple struct field filtering
|
||||
package filter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const any = '*'
|
||||
|
||||
type Filter interface {
|
||||
Len() int
|
||||
Index(int) interface{}
|
||||
Remove(int, int)
|
||||
}
|
||||
|
||||
type match struct {
|
||||
fields []string
|
||||
value string
|
||||
}
|
||||
|
||||
// Query implements the filter query
|
||||
type Query []match
|
||||
|
||||
// NewQuery parses the filter query.
|
||||
// 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) (q Query) {
|
||||
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]})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q Query) match(v reflect.Value) (ok bool) {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
if len(q) == 0 {
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
for _, qv := range q {
|
||||
s := ""
|
||||
fl := len(qv.fields)
|
||||
if fl == 0 {
|
||||
continue
|
||||
}
|
||||
val := v.FieldByName(qv.fields[0])
|
||||
for i := 1; i < fl; i++ {
|
||||
val = val.FieldByName(qv.fields[i])
|
||||
}
|
||||
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 val.Bool() {
|
||||
s = "true"
|
||||
} else {
|
||||
s = "false"
|
||||
}
|
||||
if s != qv.value {
|
||||
ok = false
|
||||
}
|
||||
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]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case vl >= 2:
|
||||
if qv.value[0] == any {
|
||||
if strings.HasSuffix(s, qv.value[1:]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if qv.value[vl-1] == any {
|
||||
if strings.HasPrefix(s, qv.value[:vl-1]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if s != qv.value {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
139
filter_test.go
Normal file
139
filter_test.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package filter
|
||||
|
||||
import "testing"
|
||||
|
||||
type Example struct {
|
||||
String string
|
||||
Int int
|
||||
Bool bool
|
||||
Struct Struct1
|
||||
}
|
||||
|
||||
type Struct1 struct {
|
||||
String string
|
||||
Struct Struct2
|
||||
}
|
||||
|
||||
type Struct2 struct {
|
||||
Int int
|
||||
}
|
||||
|
||||
type filter []Example
|
||||
|
||||
func makefilter(v filter) (f filter) {
|
||||
f = make(filter, len(v))
|
||||
copy(f, v)
|
||||
return
|
||||
}
|
||||
|
||||
func (v filter) Len() int { return len(v) }
|
||||
func (v filter) Index(i int) interface{} { return v[i] }
|
||||
func (v *filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) }
|
||||
|
||||
var (
|
||||
data = filter{
|
||||
{"str1", 42, true, Struct1{"s1", Struct2{1}}},
|
||||
{"2str", 42, true, Struct1{"s2", Struct2{2}}},
|
||||
{"str3", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
}
|
||||
benchData = filter{
|
||||
{"str1", 41, true, Struct1{"s1", Struct2{1}}},
|
||||
{"2str", 42, true, Struct1{"s2", Struct2{1}}},
|
||||
{"str3", 43, false, Struct1{"s3", Struct2{1}}},
|
||||
{"str4", 44, false, Struct1{"s4", Struct2{1}}},
|
||||
{"str5", 45, false, Struct1{"s5", Struct2{1}}},
|
||||
{"str6", 46, false, Struct1{"s6", Struct2{1}}},
|
||||
{"str7", 47, true, Struct1{"s7", Struct2{1}}},
|
||||
{"str8", 48, true, Struct1{"s8", Struct2{1}}},
|
||||
{"str9", 49, true, Struct1{"s9", Struct2{1}}},
|
||||
{"str10", 50, true, Struct1{"s10", Struct2{1}}},
|
||||
{"str11", 51, false, Struct1{"s11", Struct2{1}}},
|
||||
{"str12", 52, false, Struct1{"s12", Struct2{1}}},
|
||||
}
|
||||
)
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
f1 := makefilter(data)
|
||||
NewQuery("String:*st* Bool:true").Filter(&f1)
|
||||
if len(f1) != 2 {
|
||||
t.Error("f1 len is:", len(f1))
|
||||
}
|
||||
if f1[0] != data[0] {
|
||||
t.Error("f1[0] != data[0]")
|
||||
}
|
||||
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 {
|
||||
t.Error("f2 len is:", len(f2))
|
||||
}
|
||||
if f2[0] != data[2] {
|
||||
t.Error("f1[0] != data[2]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericFilter(t *testing.T) {
|
||||
i1 := NewQuery("String:*st* Bool:true").GenericFilter(data)
|
||||
if i1 == nil {
|
||||
t.Error("i1 is nil")
|
||||
}
|
||||
f1 := i1.(filter)
|
||||
if len(f1) != 2 {
|
||||
t.Error("f1 len is:", len(f1))
|
||||
}
|
||||
if f1[0] != data[0] {
|
||||
t.Error("f1[0] != data[0]")
|
||||
}
|
||||
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")
|
||||
}
|
||||
f2 := i2.(filter)
|
||||
if len(f2) != 1 {
|
||||
t.Error("f2 len is:", len(f2))
|
||||
}
|
||||
if f2[0] != data[2] {
|
||||
t.Error("f1[0] != data[2]")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterSimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q := NewQuery("Bool:true")
|
||||
fd := makefilter(benchData)
|
||||
for i := 0; i < b.N; i++ {
|
||||
q.Filter(&fd)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenericFilterSimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q := NewQuery("Bool:true")
|
||||
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")
|
||||
for i := 0; i < b.N; i++ {
|
||||
q.GenericFilter(benchData)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue