Skip to content

Commit 4d0207e

Browse files
committed
remove ability to create engines without json
1 parent a009285 commit 4d0207e

File tree

3 files changed

+61
-131
lines changed

3 files changed

+61
-131
lines changed

README.md

+4-20
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,16 @@ This version includes a couple more features including, AND and OR composites an
1212

1313
```go
1414
// Create a new instance of an engine with some default comparators
15-
e := NewEngine()
15+
e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"or","rules":[{"comparator":"always-false","path":"user.name","value":"Trevor"},{"comparator":"eq","path":"user.name","value":"Trevor"}]}]}`))
16+
if err != nil {
17+
panic(err)
18+
}
1619

1720
// Add a new, custom comparator
1821
e = e.AddComparator("always-false", func(a, b interface{}) bool {
1922
return false
2023
})
2124

22-
// Create composites, with rules for the engine to evaluate
23-
e.Composites = []Composite{
24-
Composite{
25-
Operator: OperatorOr,
26-
Rules: []Rule{
27-
Rule{
28-
Comparator: "always-false",
29-
Path: "user.name",
30-
Value: "Trevor",
31-
},
32-
Rule{
33-
Comparator: "eq",
34-
Path: "user.name",
35-
Value: "Trevor",
36-
},
37-
},
38-
},
39-
}
40-
4125
// Give some properties, this map can be deeper and supports interfaces
4226
props := map[string]interface{}{
4327
"user": map[string]interface{}{

rule.go

+13-17
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ var defaultComparators = map[string]Comparator{
3131
// performed, the path is the path into a map, delimited by '.', and
3232
// the value is the value that we expect to match the value at the
3333
// path
34-
type Rule struct {
34+
type rule struct {
3535
Comparator string `json:"comparator"`
3636
Path string `json:"path"`
3737
Value interface{} `json:"value"`
3838
}
3939

40-
func (r *Rule) MarshalJSON() ([]byte, error) {
40+
// MarshalJSON is important because it will put maps back into arrays, we used maps
41+
// to speed up one of
42+
func (r *rule) MarshalJSON() ([]byte, error) {
4143
type unmappedRule struct {
4244
Comparator string `json:"comparator"`
4345
Path string `json:"path"`
@@ -62,7 +64,9 @@ func (r *Rule) MarshalJSON() ([]byte, error) {
6264
return json.Marshal(umr)
6365
}
6466

65-
func (r *Rule) UnmarshalJSON(data []byte) error {
67+
// UnmarshalJSON is important because it will convert arrays in a rule set to a map
68+
// to provide faster lookups
69+
func (r *rule) UnmarshalJSON(data []byte) error {
6670
type mapRule struct {
6771
Comparator string `json:"comparator"`
6872
Path string `json:"path"`
@@ -85,7 +89,7 @@ func (r *Rule) UnmarshalJSON(data []byte) error {
8589
mr.Value = m
8690
}
8791

88-
*r = Rule{
92+
*r = rule{
8993
Comparator: mr.Comparator,
9094
Path: mr.Path,
9195
Value: mr.Value,
@@ -97,26 +101,18 @@ func (r *Rule) UnmarshalJSON(data []byte) error {
97101
// Composite is a group of rules that are joined by a logical operator
98102
// AND or OR. If the operator is AND all of the rules must be true,
99103
// if the operator is OR, one of the rules must be true.
100-
type Composite struct {
104+
type composite struct {
101105
Operator string `json:"operator"`
102-
Rules []Rule `json:"rules"`
106+
Rules []rule `json:"rules"`
103107
}
104108

105109
// Engine is a group of composites. All of the composites must be
106110
// true for the engine's evaluate function to return true.
107111
type Engine struct {
108-
Composites []Composite `json:"composites"`
112+
Composites []composite `json:"composites"`
109113
comparators map[string]Comparator
110114
}
111115

112-
// NewEngine will create a new engine with the default comparators
113-
func NewEngine() Engine {
114-
e := Engine{
115-
comparators: defaultComparators,
116-
}
117-
return e
118-
}
119-
120116
// NewJSONEngine will create a new engine from it's JSON representation
121117
func NewJSONEngine(raw json.RawMessage) (Engine, error) {
122118
var e Engine
@@ -149,7 +145,7 @@ func (e Engine) Evaluate(props map[string]interface{}) bool {
149145
// Evaluate will ensure all either all of the rules are true, if given
150146
// the AND operator, or that one of the rules is true if given the OR
151147
// operator.
152-
func (c Composite) evaluate(props map[string]interface{}, comps map[string]Comparator) bool {
148+
func (c composite) evaluate(props map[string]interface{}, comps map[string]Comparator) bool {
153149
switch c.Operator {
154150
case OperatorAnd:
155151
for _, r := range c.Rules {
@@ -173,7 +169,7 @@ func (c Composite) evaluate(props map[string]interface{}, comps map[string]Compa
173169
}
174170

175171
// Evaluate will return true if the rule is true, false otherwise
176-
func (r Rule) evaluate(props map[string]interface{}, comps map[string]Comparator) bool {
172+
func (r rule) evaluate(props map[string]interface{}, comps map[string]Comparator) bool {
177173
// Make sure we can get a value from the props
178174
val := pluck(props, r.Path)
179175
if val == nil {

rule_test.go

+44-94
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestRule_evaluate(t *testing.T) {
1414
"first_name": "Trevor",
1515
}
1616
t.Run("basic rule", func(t *testing.T) {
17-
r := Rule{
17+
r := rule{
1818
Comparator: "eq",
1919
Path: "first_name",
2020
Value: "Trevor",
@@ -26,7 +26,7 @@ func TestRule_evaluate(t *testing.T) {
2626
})
2727

2828
t.Run("unknown path", func(t *testing.T) {
29-
r := Rule{
29+
r := rule{
3030
Comparator: "eq",
3131
Path: "email",
3232
Value: "Trevor",
@@ -38,7 +38,7 @@ func TestRule_evaluate(t *testing.T) {
3838
})
3939

4040
t.Run("non comparable types", func(t *testing.T) {
41-
r := Rule{
41+
r := rule{
4242
Comparator: "eq",
4343
Path: "name",
4444
Value: func() {},
@@ -50,7 +50,7 @@ func TestRule_evaluate(t *testing.T) {
5050
})
5151

5252
t.Run("unknown comparator", func(t *testing.T) {
53-
r := Rule{
53+
r := rule{
5454
Comparator: "unknown",
5555
Path: "name",
5656
Value: "Trevor",
@@ -63,7 +63,7 @@ func TestRule_evaluate(t *testing.T) {
6363
}
6464

6565
func BenchmarkRule_evaluate(b *testing.B) {
66-
r := Rule{
66+
r := rule{
6767
Comparator: "unit",
6868
Path: "name",
6969
Value: "Trevor",
@@ -129,15 +129,15 @@ func TestComposite_evaluate(t *testing.T) {
129129
}
130130

131131
t.Run("and", func(t *testing.T) {
132-
c := Composite{
132+
c := composite{
133133
Operator: OperatorAnd,
134-
Rules: []Rule{
135-
Rule{
134+
Rules: []rule{
135+
rule{
136136
Comparator: "eq",
137137
Path: "name",
138138
Value: "Trevor",
139139
},
140-
Rule{
140+
rule{
141141
Comparator: "eq",
142142
Path: "age",
143143
Value: float64(23),
@@ -151,15 +151,15 @@ func TestComposite_evaluate(t *testing.T) {
151151
})
152152

153153
t.Run("or", func(t *testing.T) {
154-
c := Composite{
154+
c := composite{
155155
Operator: OperatorOr,
156-
Rules: []Rule{
157-
Rule{
156+
Rules: []rule{
157+
rule{
158158
Comparator: "eq",
159159
Path: "name",
160160
Value: "John",
161161
},
162-
Rule{
162+
rule{
163163
Comparator: "eq",
164164
Path: "age",
165165
Value: float64(23),
@@ -173,15 +173,15 @@ func TestComposite_evaluate(t *testing.T) {
173173
})
174174

175175
t.Run("unknown operator", func(t *testing.T) {
176-
c := Composite{
176+
c := composite{
177177
Operator: "unknown",
178-
Rules: []Rule{
179-
Rule{
178+
Rules: []rule{
179+
rule{
180180
Comparator: "eq",
181181
Path: "name",
182182
Value: "John",
183183
},
184-
Rule{
184+
rule{
185185
Comparator: "eq",
186186
Path: "age",
187187
Value: float64(23),
@@ -196,10 +196,10 @@ func TestComposite_evaluate(t *testing.T) {
196196
}
197197

198198
func BenchmarkComposite_evaluate(b *testing.B) {
199-
c := Composite{
199+
c := composite{
200200
Operator: "or",
201-
Rules: []Rule{
202-
Rule{
201+
Rules: []rule{
202+
rule{
203203
Comparator: "unit",
204204
Path: "name",
205205
Value: "Trevor",
@@ -226,17 +226,20 @@ func TestAddComparator(t *testing.T) {
226226
comp := func(a, b interface{}) bool {
227227
return false
228228
}
229-
e := NewEngine()
229+
e, err := NewJSONEngine(json.RawMessage(`{}`))
230+
if err != nil {
231+
t.Fatal(err)
232+
}
230233
e = e.AddComparator("always-false", comp)
231234
if e.comparators["always-false"] == nil {
232235
t.Fatal("expected comparator to be added under key always-false")
233236
}
234237

235-
e.Composites = []Composite{
236-
Composite{
238+
e.Composites = []composite{
239+
composite{
237240
Operator: OperatorAnd,
238-
Rules: []Rule{
239-
Rule{
241+
Rules: []rule{
242+
rule{
240243
Comparator: "always-false",
241244
Path: "user.name",
242245
Value: "Trevor",
@@ -300,7 +303,10 @@ func TestEngineEvaluate(t *testing.T) {
300303
"id": float64(1234),
301304
},
302305
}
303-
e := NewEngine()
306+
e, err := NewJSONEngine(json.RawMessage(`{}`))
307+
if err != nil {
308+
t.Fatal(err)
309+
}
304310
res := e.Evaluate(props)
305311
if res != true {
306312
t.Fatal("expected engine to pass")
@@ -319,18 +325,9 @@ func TestEngineEvaluate(t *testing.T) {
319325
},
320326
},
321327
}
322-
e := NewEngine()
323-
e.Composites = []Composite{
324-
Composite{
325-
Operator: OperatorAnd,
326-
Rules: []Rule{
327-
Rule{
328-
Comparator: "contains",
329-
Path: "address.bedroom.furniture",
330-
Value: "tv",
331-
},
332-
},
333-
},
328+
e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"contains","path":"address.bedroom.furniture","value":"tv"}]}]}`))
329+
if err != nil {
330+
t.Fatal(err)
334331
}
335332
res := e.Evaluate(props)
336333
if res != true {
@@ -346,38 +343,9 @@ func TestEngineEvaluate(t *testing.T) {
346343
"id": float64(1234),
347344
},
348345
}
349-
e := NewEngine()
350-
e.Composites = []Composite{
351-
Composite{
352-
Operator: OperatorAnd,
353-
Rules: []Rule{
354-
Rule{
355-
Comparator: "eq",
356-
Path: "user.name",
357-
Value: "Trevor",
358-
},
359-
Rule{
360-
Comparator: "eq",
361-
Path: "user.id",
362-
Value: float64(1234),
363-
},
364-
},
365-
},
366-
Composite{
367-
Operator: OperatorOr,
368-
Rules: []Rule{
369-
Rule{
370-
Comparator: "eq",
371-
Path: "user.name",
372-
Value: "Trevor",
373-
},
374-
Rule{
375-
Comparator: "eq",
376-
Path: "user.id",
377-
Value: float64(7),
378-
},
379-
},
380-
},
346+
e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"eq","path":"user.name","value":"Trevor"},{"comparator":"eq","path":"user.id","value":1234}]},{"operator":"or","rules":[{"comparator":"eq","path":"user.name","value":"Trevor"},{"comparator":"eq","path":"user.id","value":7}]}]}`))
347+
if err != nil {
348+
t.Fatal(err)
381349
}
382350
res := e.Evaluate(props)
383351
if res != true {
@@ -397,18 +365,9 @@ func TestEngineEvaluate(t *testing.T) {
397365
},
398366
},
399367
}
400-
e := NewEngine()
401-
e.Composites = []Composite{
402-
Composite{
403-
Operator: OperatorAnd,
404-
Rules: []Rule{
405-
Rule{
406-
Comparator: "contains",
407-
Path: "user.favorites",
408-
Value: "golang",
409-
},
410-
},
411-
},
368+
e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"contains","path":"user.favorites","value":"golang"}]}]}`))
369+
if err != nil {
370+
t.Fatal(err)
412371
}
413372
res := e.Evaluate(props)
414373
if res != true {
@@ -418,18 +377,9 @@ func TestEngineEvaluate(t *testing.T) {
418377
}
419378

420379
func BenchmarkEngine_Evaluate(b *testing.B) {
421-
e := NewEngine()
422-
e.Composites = []Composite{
423-
Composite{
424-
Operator: "or",
425-
Rules: []Rule{
426-
Rule{
427-
Comparator: "unit",
428-
Path: "name",
429-
Value: "Trevor",
430-
},
431-
},
432-
},
380+
e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"unit","path":"name","value":"Trevor"}]}]}`))
381+
if err != nil {
382+
b.Fatal(err)
433383
}
434384
e.AddComparator("unit", func(a, b interface{}) bool { return true })
435385
props := map[string]interface{}{

0 commit comments

Comments
 (0)