Skip to content

Commit

Permalink
remove ability to create engines without json
Browse files Browse the repository at this point in the history
  • Loading branch information
huttotw committed Mar 25, 2019
1 parent a009285 commit 4d0207e
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 131 deletions.
24 changes: 4 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand Down
30 changes: 13 additions & 17 deletions rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
138 changes: 44 additions & 94 deletions rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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() {},
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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{}{
Expand Down

0 comments on commit 4d0207e

Please sign in to comment.