diff --git a/README.md b/README.md index 2af0b1e..b403119 100644 --- a/README.md +++ b/README.md @@ -12,32 +12,16 @@ This version includes a couple more features including, AND and OR composites an ```go // Create a new instance of an engine with some default comparators -e := NewEngine() +e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"or","rules":[{"comparator":"always-false","path":"user.name","value":"Trevor"},{"comparator":"eq","path":"user.name","value":"Trevor"}]}]}`)) +if err != nil { + panic(err) +} // Add a new, custom comparator e = e.AddComparator("always-false", func(a, b interface{}) bool { return false }) -// Create composites, with rules for the engine to evaluate -e.Composites = []Composite{ - Composite{ - Operator: OperatorOr, - Rules: []Rule{ - Rule{ - Comparator: "always-false", - Path: "user.name", - Value: "Trevor", - }, - Rule{ - Comparator: "eq", - Path: "user.name", - Value: "Trevor", - }, - }, - }, -} - // Give some properties, this map can be deeper and supports interfaces props := map[string]interface{}{ "user": map[string]interface{}{ diff --git a/rule.go b/rule.go index 32b0873..dfe2a98 100644 --- a/rule.go +++ b/rule.go @@ -31,13 +31,15 @@ var defaultComparators = map[string]Comparator{ // performed, the path is the path into a map, delimited by '.', and // the value is the value that we expect to match the value at the // path -type Rule struct { +type rule struct { Comparator string `json:"comparator"` Path string `json:"path"` Value interface{} `json:"value"` } -func (r *Rule) MarshalJSON() ([]byte, error) { +// MarshalJSON is important because it will put maps back into arrays, we used maps +// to speed up one of +func (r *rule) MarshalJSON() ([]byte, error) { type unmappedRule struct { Comparator string `json:"comparator"` Path string `json:"path"` @@ -62,7 +64,9 @@ func (r *Rule) MarshalJSON() ([]byte, error) { return json.Marshal(umr) } -func (r *Rule) UnmarshalJSON(data []byte) error { +// UnmarshalJSON is important because it will convert arrays in a rule set to a map +// to provide faster lookups +func (r *rule) UnmarshalJSON(data []byte) error { type mapRule struct { Comparator string `json:"comparator"` Path string `json:"path"` @@ -85,7 +89,7 @@ func (r *Rule) UnmarshalJSON(data []byte) error { mr.Value = m } - *r = Rule{ + *r = rule{ Comparator: mr.Comparator, Path: mr.Path, Value: mr.Value, @@ -97,26 +101,18 @@ func (r *Rule) UnmarshalJSON(data []byte) error { // Composite is a group of rules that are joined by a logical operator // AND or OR. If the operator is AND all of the rules must be true, // if the operator is OR, one of the rules must be true. -type Composite struct { +type composite struct { Operator string `json:"operator"` - Rules []Rule `json:"rules"` + Rules []rule `json:"rules"` } // Engine is a group of composites. All of the composites must be // true for the engine's evaluate function to return true. type Engine struct { - Composites []Composite `json:"composites"` + Composites []composite `json:"composites"` comparators map[string]Comparator } -// NewEngine will create a new engine with the default comparators -func NewEngine() Engine { - e := Engine{ - comparators: defaultComparators, - } - return e -} - // NewJSONEngine will create a new engine from it's JSON representation func NewJSONEngine(raw json.RawMessage) (Engine, error) { var e Engine @@ -149,7 +145,7 @@ func (e Engine) Evaluate(props map[string]interface{}) bool { // Evaluate will ensure all either all of the rules are true, if given // the AND operator, or that one of the rules is true if given the OR // operator. -func (c Composite) evaluate(props map[string]interface{}, comps map[string]Comparator) bool { +func (c composite) evaluate(props map[string]interface{}, comps map[string]Comparator) bool { switch c.Operator { case OperatorAnd: for _, r := range c.Rules { @@ -173,7 +169,7 @@ func (c Composite) evaluate(props map[string]interface{}, comps map[string]Compa } // Evaluate will return true if the rule is true, false otherwise -func (r Rule) evaluate(props map[string]interface{}, comps map[string]Comparator) bool { +func (r rule) evaluate(props map[string]interface{}, comps map[string]Comparator) bool { // Make sure we can get a value from the props val := pluck(props, r.Path) if val == nil { diff --git a/rule_test.go b/rule_test.go index b3eb663..53a5639 100644 --- a/rule_test.go +++ b/rule_test.go @@ -14,7 +14,7 @@ func TestRule_evaluate(t *testing.T) { "first_name": "Trevor", } t.Run("basic rule", func(t *testing.T) { - r := Rule{ + r := rule{ Comparator: "eq", Path: "first_name", Value: "Trevor", @@ -26,7 +26,7 @@ func TestRule_evaluate(t *testing.T) { }) t.Run("unknown path", func(t *testing.T) { - r := Rule{ + r := rule{ Comparator: "eq", Path: "email", Value: "Trevor", @@ -38,7 +38,7 @@ func TestRule_evaluate(t *testing.T) { }) t.Run("non comparable types", func(t *testing.T) { - r := Rule{ + r := rule{ Comparator: "eq", Path: "name", Value: func() {}, @@ -50,7 +50,7 @@ func TestRule_evaluate(t *testing.T) { }) t.Run("unknown comparator", func(t *testing.T) { - r := Rule{ + r := rule{ Comparator: "unknown", Path: "name", Value: "Trevor", @@ -63,7 +63,7 @@ func TestRule_evaluate(t *testing.T) { } func BenchmarkRule_evaluate(b *testing.B) { - r := Rule{ + r := rule{ Comparator: "unit", Path: "name", Value: "Trevor", @@ -129,15 +129,15 @@ func TestComposite_evaluate(t *testing.T) { } t.Run("and", func(t *testing.T) { - c := Composite{ + c := composite{ Operator: OperatorAnd, - Rules: []Rule{ - Rule{ + Rules: []rule{ + rule{ Comparator: "eq", Path: "name", Value: "Trevor", }, - Rule{ + rule{ Comparator: "eq", Path: "age", Value: float64(23), @@ -151,15 +151,15 @@ func TestComposite_evaluate(t *testing.T) { }) t.Run("or", func(t *testing.T) { - c := Composite{ + c := composite{ Operator: OperatorOr, - Rules: []Rule{ - Rule{ + Rules: []rule{ + rule{ Comparator: "eq", Path: "name", Value: "John", }, - Rule{ + rule{ Comparator: "eq", Path: "age", Value: float64(23), @@ -173,15 +173,15 @@ func TestComposite_evaluate(t *testing.T) { }) t.Run("unknown operator", func(t *testing.T) { - c := Composite{ + c := composite{ Operator: "unknown", - Rules: []Rule{ - Rule{ + Rules: []rule{ + rule{ Comparator: "eq", Path: "name", Value: "John", }, - Rule{ + rule{ Comparator: "eq", Path: "age", Value: float64(23), @@ -196,10 +196,10 @@ func TestComposite_evaluate(t *testing.T) { } func BenchmarkComposite_evaluate(b *testing.B) { - c := Composite{ + c := composite{ Operator: "or", - Rules: []Rule{ - Rule{ + Rules: []rule{ + rule{ Comparator: "unit", Path: "name", Value: "Trevor", @@ -226,17 +226,20 @@ func TestAddComparator(t *testing.T) { comp := func(a, b interface{}) bool { return false } - e := NewEngine() + e, err := NewJSONEngine(json.RawMessage(`{}`)) + if err != nil { + t.Fatal(err) + } e = e.AddComparator("always-false", comp) if e.comparators["always-false"] == nil { t.Fatal("expected comparator to be added under key always-false") } - e.Composites = []Composite{ - Composite{ + e.Composites = []composite{ + composite{ Operator: OperatorAnd, - Rules: []Rule{ - Rule{ + Rules: []rule{ + rule{ Comparator: "always-false", Path: "user.name", Value: "Trevor", @@ -300,7 +303,10 @@ func TestEngineEvaluate(t *testing.T) { "id": float64(1234), }, } - e := NewEngine() + e, err := NewJSONEngine(json.RawMessage(`{}`)) + if err != nil { + t.Fatal(err) + } res := e.Evaluate(props) if res != true { t.Fatal("expected engine to pass") @@ -319,18 +325,9 @@ func TestEngineEvaluate(t *testing.T) { }, }, } - e := NewEngine() - e.Composites = []Composite{ - Composite{ - Operator: OperatorAnd, - Rules: []Rule{ - Rule{ - Comparator: "contains", - Path: "address.bedroom.furniture", - Value: "tv", - }, - }, - }, + e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"contains","path":"address.bedroom.furniture","value":"tv"}]}]}`)) + if err != nil { + t.Fatal(err) } res := e.Evaluate(props) if res != true { @@ -346,38 +343,9 @@ func TestEngineEvaluate(t *testing.T) { "id": float64(1234), }, } - e := NewEngine() - e.Composites = []Composite{ - Composite{ - Operator: OperatorAnd, - Rules: []Rule{ - Rule{ - Comparator: "eq", - Path: "user.name", - Value: "Trevor", - }, - Rule{ - Comparator: "eq", - Path: "user.id", - Value: float64(1234), - }, - }, - }, - Composite{ - Operator: OperatorOr, - Rules: []Rule{ - Rule{ - Comparator: "eq", - Path: "user.name", - Value: "Trevor", - }, - Rule{ - Comparator: "eq", - Path: "user.id", - Value: float64(7), - }, - }, - }, + 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}]}]}`)) + if err != nil { + t.Fatal(err) } res := e.Evaluate(props) if res != true { @@ -397,18 +365,9 @@ func TestEngineEvaluate(t *testing.T) { }, }, } - e := NewEngine() - e.Composites = []Composite{ - Composite{ - Operator: OperatorAnd, - Rules: []Rule{ - Rule{ - Comparator: "contains", - Path: "user.favorites", - Value: "golang", - }, - }, - }, + e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"contains","path":"user.favorites","value":"golang"}]}]}`)) + if err != nil { + t.Fatal(err) } res := e.Evaluate(props) if res != true { @@ -418,18 +377,9 @@ func TestEngineEvaluate(t *testing.T) { } func BenchmarkEngine_Evaluate(b *testing.B) { - e := NewEngine() - e.Composites = []Composite{ - Composite{ - Operator: "or", - Rules: []Rule{ - Rule{ - Comparator: "unit", - Path: "name", - Value: "Trevor", - }, - }, - }, + e, err := NewJSONEngine(json.RawMessage(`{"composites":[{"operator":"and","rules":[{"comparator":"unit","path":"name","value":"Trevor"}]}]}`)) + if err != nil { + b.Fatal(err) } e.AddComparator("unit", func(a, b interface{}) bool { return true }) props := map[string]interface{}{