Skip to content

Commit a009285

Browse files
committed
make use of maps to speed up oneof and noneof
1 parent a69cba1 commit a009285

File tree

5 files changed

+196
-57
lines changed

5 files changed

+196
-57
lines changed

README.md

+35-31
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
[![CircleCI](https://circleci.com/gh/huttotw/grules/tree/master.svg?style=svg)](https://circleci.com/gh/huttotw/grules/tree/master)
22

33
# Introduction
4+
45
This package was created with inspiration from Thomas' [go-ruler](https://github.com/hopkinsth/go-ruler) to run a simple set of rules against an entity.
56

67
This version includes a couple more features including, AND and OR composites and the ability to add custom comparators.
78

89
**Note**: This package only compares two types: `string` and `float64`, this plays nicely with `encoding/json`.
910

1011
# Example
12+
1113
```go
1214
// Create a new instance of an engine with some default comparators
1315
e := NewEngine()
@@ -49,54 +51,56 @@ res := e.Evaluate(props)
4951
```
5052

5153
# Comparators
52-
* `eq` will return true if `a == b`
53-
* `neq` will return true if `a != b`
54-
* `lt` will return true if `a < b`
55-
* `lte` will return true if `a <= b`
56-
* `gt` will return true if `a > b`
57-
* `gte` will return true if `a >= b`
58-
* `contains` will return true if `a` contains `b`
59-
* `oneof` will return true if `a` is one of `b`
54+
55+
- `eq` will return true if `a == b`
56+
- `neq` will return true if `a != b`
57+
- `lt` will return true if `a < b`
58+
- `lte` will return true if `a <= b`
59+
- `gt` will return true if `a > b`
60+
- `gte` will return true if `a >= b`
61+
- `contains` will return true if `a` contains `b`
62+
- `oneof` will return true if `a` is one of `b`
6063

6164
`contains` is different than `oneof` in that `contains` expects the first argument to be a slice, and `oneof` expects the second argument to be a slice.
6265

6366
# Benchmarks
6467

65-
|Benchmark|N|Speed|Used|Allocs|
66-
|---------|----------|-----|------|------|
67-
|BenchmarkEqual-12|1000000000|5.22 ns/op|0 B/op|0 allocs/op|
68-
|BenchmarkNotEqual-12|2000000000|3.77 ns/op|0 B/op|0 allocs/op|
69-
|BenchmarkLessThan-12|2000000000|2.20 ns/op|0 B/op|0 allocs/op|
70-
|BenchmarkLessThanEqual-12|2000000000|1.95 ns/op|0 B/op|0 allocs/op|
71-
|BenchmarkGreaterThan-12|5000000000|1.95 ns/op|0 B/op|0 allocs/op|
72-
|BenchmarkGreaterThanEqual-12|2000000000|1.97 ns/op|0 B/op|0 allocs/op|
73-
|BenchmarkContains-12|1000000000|5.66 ns/op|0 B/op|0 allocs/op|
74-
|BenchmarkContainsLong50000-12|30000|157679 ns/op|0 B/op|0 allocs/op|
75-
|BenchmarkNotContains-12|500000000|11.5 ns/op|0 B/op|0 allocs/op|
76-
|BenchmarkNotContainsLong50000-12|30000|157437 ns/op|0 B/op|0 allocs/op|
77-
|BenchmarkOneOf-12|500000000|11.0 ns/op|0 B/op|0 allocs/op|
78-
|BenchmarkNoneOf-12|500000000|10.7 ns/op|0 B/op|0 allocs/op|
79-
|BenchmarkPluckShallow-12|100000000|42.4 ns/op|16 B/op|1 allocs/op|
80-
|BenchmarkPluckDeep-12|30000000|174 ns/op|112 B/op|1 allocs/op|
81-
|BenchmarkRule_evaluate-12|100000000|51.7 ns/op|16 B/op|1 allocs/op|
82-
|BenchmarkComposite_evaluate-12|100000000|58.9 ns/op|16 B/op|1 allocs/op|
83-
|BenchmarkEngine_Evaluate-12|100000000|69.9 ns/op|16 B/op|1 allocs/op|
68+
| Benchmark | N | Speed | Used | Allocs |
69+
| -------------------------------- | ---------- | ------------ | -------- | ----------- |
70+
| BenchmarkEqual-12 | 1000000000 | 5.22 ns/op | 0 B/op | 0 allocs/op |
71+
| BenchmarkNotEqual-12 | 2000000000 | 3.77 ns/op | 0 B/op | 0 allocs/op |
72+
| BenchmarkLessThan-12 | 2000000000 | 2.20 ns/op | 0 B/op | 0 allocs/op |
73+
| BenchmarkLessThanEqual-12 | 2000000000 | 1.95 ns/op | 0 B/op | 0 allocs/op |
74+
| BenchmarkGreaterThan-12 | 5000000000 | 1.95 ns/op | 0 B/op | 0 allocs/op |
75+
| BenchmarkGreaterThanEqual-12 | 2000000000 | 1.97 ns/op | 0 B/op | 0 allocs/op |
76+
| BenchmarkContains-12 | 1000000000 | 5.66 ns/op | 0 B/op | 0 allocs/op |
77+
| BenchmarkContainsLong50000-12 | 30000 | 157679 ns/op | 0 B/op | 0 allocs/op |
78+
| BenchmarkNotContains-12 | 500000000 | 11.5 ns/op | 0 B/op | 0 allocs/op |
79+
| BenchmarkNotContainsLong50000-12 | 30000 | 157437 ns/op | 0 B/op | 0 allocs/op |
80+
| BenchmarkOneOf-12 | 500000000 | 0.53 ns/op | 0 B/op | 0 allocs/op |
81+
| BenchmarkNoneOf-12 | 500000000 | 0.53 ns/op | 0 B/op | 0 allocs/op |
82+
| BenchmarkPluckShallow-12 | 100000000 | 42.4 ns/op | 16 B/op | 1 allocs/op |
83+
| BenchmarkPluckDeep-12 | 30000000 | 174 ns/op | 112 B/op | 1 allocs/op |
84+
| BenchmarkRule_evaluate-12 | 100000000 | 51.7 ns/op | 16 B/op | 1 allocs/op |
85+
| BenchmarkComposite_evaluate-12 | 100000000 | 58.9 ns/op | 16 B/op | 1 allocs/op |
86+
| BenchmarkEngine_Evaluate-12 | 100000000 | 69.9 ns/op | 16 B/op | 1 allocs/op |
8487

8588
To run benchmarks:
89+
8690
```
8791
go test -run none -bench . -benchtime 3s -benchmem
8892
```
8993

9094
All benchmarks were run on:
91-
92-
MacOS High Sierra 2.6Ghz Intel Core i7 16 GB 2400 MHz DDR4
95+
96+
MacOS High Sierra 2.6Ghz Intel Core i7 16 GB 2400 MHz DDR4
9397

9498
# License
9599

96-
Copyright &copy; 2018 Trevor Hutto
100+
Copyright &copy; 2019 Trevor Hutto
97101

98102
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
99103

100104
http://www.apache.org/licenses/LICENSE-2.0
101105

102-
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
106+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

comparators.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,30 @@ func notContains(a, b interface{}) bool {
225225

226226
// oneOf will return true if b contains a
227227
func oneOf(a, b interface{}) bool {
228-
return contains(b, a)
228+
m, ok := b.(map[interface{}]struct{})
229+
if !ok {
230+
return false
231+
}
232+
233+
_, found := m[a]
234+
if found {
235+
return true
236+
}
237+
238+
return false
229239
}
230240

231241
// noneOf will return true if b does not contain a
232242
func noneOf(a, b interface{}) bool {
233-
return notContains(b, a)
243+
m, ok := b.(map[interface{}]struct{})
244+
if !ok {
245+
return false
246+
}
247+
248+
_, found := m[a]
249+
if !found {
250+
return true
251+
}
252+
253+
return false
234254
}

comparators_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,12 @@ func BenchmarkOneOf(b *testing.B) {
245245

246246
func TestOneOf(t *testing.T) {
247247
cases := []testCase{
248-
testCase{args: []interface{}{"a", []interface{}{"a", "b"}}, expected: true},
249-
testCase{args: []interface{}{"c", []interface{}{"a", "b"}}, expected: false},
250-
testCase{args: []interface{}{float64(1), []interface{}{"a", "b"}}, expected: false},
251-
testCase{args: []interface{}{float64(1), []interface{}{float64(1), float64(2)}}, expected: true},
252-
testCase{args: []interface{}{float64(3), []interface{}{float64(1), float64(2)}}, expected: false},
253-
testCase{args: []interface{}{float64(1.01), []interface{}{float64(1.01), float64(1.02)}}, expected: true},
248+
testCase{args: []interface{}{"a", map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: true},
249+
testCase{args: []interface{}{"c", map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: false},
250+
testCase{args: []interface{}{float64(1), map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: false},
251+
testCase{args: []interface{}{float64(1), map[interface{}]struct{}{float64(1): struct{}{}, float64(2): struct{}{}}}, expected: true},
252+
testCase{args: []interface{}{float64(3), map[interface{}]struct{}{float64(1): struct{}{}, float64(2): struct{}{}}}, expected: false},
253+
testCase{args: []interface{}{float64(1.01), map[interface{}]struct{}{1.01: struct{}{}, 1.02: struct{}{}}}, expected: true},
254254
}
255255
for i, c := range cases {
256256
res := oneOf(c.args[0], c.args[1])
@@ -268,13 +268,13 @@ func BenchmarkNoneOf(b *testing.B) {
268268

269269
func TestNoneOf(t *testing.T) {
270270
cases := []testCase{
271-
testCase{args: []interface{}{"a", []interface{}{"a", "b"}}, expected: false},
272-
testCase{args: []interface{}{"c", []interface{}{"a", "b"}}, expected: true},
273-
testCase{args: []interface{}{float64(1), []interface{}{"a", "b"}}, expected: true},
274-
testCase{args: []interface{}{float64(1), []interface{}{float64(1), float64(2)}}, expected: false},
275-
testCase{args: []interface{}{float64(3), []interface{}{float64(1), float64(2)}}, expected: true},
276-
testCase{args: []interface{}{float64(1.01), []interface{}{float64(1.01), float64(1.02)}}, expected: false},
277-
testCase{args: []interface{}{float64(1.03), []interface{}{float64(1.01), float64(1.02)}}, expected: true},
271+
testCase{args: []interface{}{"a", map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: false},
272+
testCase{args: []interface{}{"c", map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: true},
273+
testCase{args: []interface{}{float64(1), map[interface{}]struct{}{"a": struct{}{}, "b": struct{}{}}}, expected: true},
274+
testCase{args: []interface{}{float64(1), map[interface{}]struct{}{float64(1): struct{}{}, float64(2): struct{}{}}}, expected: false},
275+
testCase{args: []interface{}{float64(3), map[interface{}]struct{}{float64(1): struct{}{}, float64(2): struct{}{}}}, expected: true},
276+
testCase{args: []interface{}{float64(1.01), map[interface{}]struct{}{1.01: struct{}{}, 1.02: struct{}{}}}, expected: false},
277+
testCase{args: []interface{}{float64(1.03), map[interface{}]struct{}{1.01: struct{}{}, 1.02: struct{}{}}}, expected: true},
278278
}
279279

280280
for i, c := range cases {

rule.go

+57
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,63 @@ type Rule struct {
3737
Value interface{} `json:"value"`
3838
}
3939

40+
func (r *Rule) MarshalJSON() ([]byte, error) {
41+
type unmappedRule struct {
42+
Comparator string `json:"comparator"`
43+
Path string `json:"path"`
44+
Value interface{} `json:"value"`
45+
}
46+
47+
switch t := r.Value.(type) {
48+
case map[interface{}]struct{}:
49+
var s []interface{}
50+
for k := range t {
51+
s = append(s, k)
52+
}
53+
r.Value = s
54+
}
55+
56+
umr := unmappedRule{
57+
Comparator: r.Comparator,
58+
Path: r.Path,
59+
Value: r.Value,
60+
}
61+
62+
return json.Marshal(umr)
63+
}
64+
65+
func (r *Rule) UnmarshalJSON(data []byte) error {
66+
type mapRule struct {
67+
Comparator string `json:"comparator"`
68+
Path string `json:"path"`
69+
Value interface{} `json:"value"`
70+
}
71+
72+
var mr mapRule
73+
err := json.Unmarshal(data, &mr)
74+
if err != nil {
75+
return err
76+
}
77+
78+
switch t := mr.Value.(type) {
79+
case []interface{}:
80+
var m = make(map[interface{}]struct{})
81+
for _, v := range t {
82+
m[v] = struct{}{}
83+
}
84+
85+
mr.Value = m
86+
}
87+
88+
*r = Rule{
89+
Comparator: mr.Comparator,
90+
Path: mr.Path,
91+
Value: mr.Value,
92+
}
93+
94+
return nil
95+
}
96+
4097
// Composite is a group of rules that are joined by a logical operator
4198
// AND or OR. If the operator is AND all of the rules must be true,
4299
// if the operator is OR, one of the rules must be true.

rule_test.go

+69-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package grules
22

33
import (
4+
"encoding/json"
5+
"reflect"
46
"testing"
57
)
68

@@ -81,6 +83,42 @@ func BenchmarkRule_evaluate(b *testing.B) {
8183
}
8284
}
8385

86+
func TestRule_MarshalJSON(t *testing.T) {
87+
t.Run("simple engine", func(t *testing.T) {
88+
j := []byte(`{"composites":[{"operator":"and","rules":[{"comparator":"eq","path":"first_name","value":"Trevor"}]}]}`)
89+
e, err := NewJSONEngine(j)
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
94+
b, err := json.Marshal(e)
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
99+
if string(b) != string(j) {
100+
t.Fatal("expected json to be same")
101+
}
102+
})
103+
104+
t.Run("list to map", func(t *testing.T) {
105+
j := []byte(`{"composites":[{"operator":"and","rules":[{"comparator":"oneof","path":"first_name","value":["Trevor"]}]}]}`)
106+
e, err := NewJSONEngine(j)
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
111+
b, err := json.Marshal(e)
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
116+
if string(b) != string(j) {
117+
t.Fatal("expected json to be same")
118+
}
119+
})
120+
}
121+
84122
func TestComposite_evaluate(t *testing.T) {
85123
comparators := map[string]Comparator{
86124
"eq": equal,
@@ -220,17 +258,37 @@ func TestAddComparator(t *testing.T) {
220258
}
221259

222260
func TestNewJSONEngine(t *testing.T) {
223-
j := []byte(`{"composites":[{"operator":"and","rules":[{"comparator":"eq","path":"first_name","value":"Trevor"}]}]}`)
224-
e, err := NewJSONEngine(j)
225-
if err != nil {
226-
t.Fatal(err)
227-
}
228-
if len(e.Composites) != 1 {
229-
t.Fatal("expected 1 composite")
230-
}
231-
if len(e.Composites[0].Rules) != 1 {
232-
t.Fatal("expected 1 rule in first composite")
233-
}
261+
t.Run("simple engine", func(t *testing.T) {
262+
j := []byte(`{"composites":[{"operator":"and","rules":[{"comparator":"eq","path":"first_name","value":"Trevor"}]}]}`)
263+
e, err := NewJSONEngine(j)
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
if len(e.Composites) != 1 {
268+
t.Fatal("expected 1 composite")
269+
}
270+
if len(e.Composites[0].Rules) != 1 {
271+
t.Fatal("expected 1 rule in first composite")
272+
}
273+
})
274+
275+
t.Run("list to map", func(t *testing.T) {
276+
j := []byte(`{"composites":[{"operator":"and","rules":[{"comparator":"oneof","path":"first_name","value":["Trevor"]}]}]}`)
277+
e, err := NewJSONEngine(j)
278+
if err != nil {
279+
t.Fatal(err)
280+
}
281+
if len(e.Composites) != 1 {
282+
t.Fatal("expected 1 composite")
283+
}
284+
if len(e.Composites[0].Rules) != 1 {
285+
t.Fatal("expected 1 rule in first composite")
286+
}
287+
288+
if reflect.TypeOf(e.Composites[0].Rules[0].Value).Kind() != reflect.Map {
289+
t.Fatal("expected list to be transformed to map")
290+
}
291+
})
234292
}
235293

236294
func TestEngineEvaluate(t *testing.T) {

0 commit comments

Comments
 (0)