Categorygithub.com/nimbit-software/gojson-rules-engine

# README

json-rules-engine

GO-(json-rules-engine)

This projects strives to be a drop in replacement for the original json-rules-engine project, but written in Go. A big thanks on the team from cache-control for the current project With the goal of offering better performance and through go's concurrency model.

Project Status

The project is still in its initial stages and is not yet ready for production use, but we are working to get it there since we want to use it in production.

A rules engine expressed in JSON

Synopsis

json-rules-engine is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.

Features

  • Rules expressed in simple, easy to read JSON
  • Full support for ALL and ANY boolean operators, including recursive nesting
  • Fast by default
  • Early stopping evaluation (short-circuiting)
  • Lightweight & extendable; w/few dependencies

Installation

$ go get github.com/nimbit-software/gojson-rules-engine

Contributions welcome!!!

TODOS

  • Better mascot. I am not a designer so the mascot is just a gopher with a hat
  • Better error handling. Error handling can always be better. I never want it to panic but handle error gracefully
  • More examples.
  • More operators. I want to add more operators to the engine
  • More documentation. I want to add more documentation to the engine
  • Move the "rules-engine" package to the top-level for better documentation visibility.
  • Reduce the use of any type and create more strongly-typed methods.
  • Separate methods with different input types for clarity.
  • Optimize for speed by creating a more strongly-typed Node structure.
  • Implement a two-pass approach for unmarshaling and creating strongly-typed nodes.
  • Add more unit tests to increase code coverage.
  • Operator decorators for the rules engine.
  • Create condition validation function
  • Add condition sharing
  • convert all rules to Json

ValueNode

The ValueNode is a strongly-typed node that can be used to represent any value in the rules engine. It is used to represent facts values and condition values in the rules engine.

const (
	Null DataType = iota
	Bool
	Number
	String
	Array
	Object
)

type ValueNode struct {
	Type   DataType
	Bool   bool
	Number float64
	String string
	Array  []ValueNode
	Object map[string]ValueNode
}

Operators

The engine comes with the following default operators: Either the operator itself or an alias can be used.

OperatorAliasData typeDescriptionExample
equaleq,=string, number booleanStrict equality{ "fact": "age", "operator": "equal", "value": 21 }
notEqualne,!=string, number booleanStrict inequality{ "fact": "age", "operator": "notEqual", "value": 21 }
inin,containsarrayValue is in array{ "fact": "age", "operator": "in", "value": [21, 22, 23] }
notInnin,notContainsarrayValue is not in array{ "fact": "age", "operator": "notIn", "value": [21, 22, 23] }
lessThanlt,<numberLess than{ "fact": "age", "operator": "lessThan", "value": 21 }
lessThanInclusivelte,<=numberLess than or equal{ "fact": "age", "operator": "lessThanInclusive", "value": 21 }
greaterThangt,>numberGreater than{ "fact": "age", "operator": "greaterThan", "value": 21 }
greaterThanInclusivegte,>=numberGreater than or equal{ "fact": "age", "operator": "greaterThanInclusive", "value": 21 }
startsWithstringString starts with{ "fact": "name", "operator": "startsWith", "value": "B" }
endsWithstringString ends with{ "fact": "name", "operator": "endsWith", "value": "b" }
includesstringString includes{ "fact": "name", "operator": "includes", "value": "op" }

Additional operators can be added via the AddOperator method.

	
// func NewOperator(name string, cb func(factValue, jsonValue interface{}) bool, factValueValidator func(factValue interface{}) bool) (*Operator, error)
    o, _ := NewOperator("startsWith", func(a, b interface{}) bool {
        aString, okA := a.(string)
        bString, okB := b.(string)
        return okA && okB && strings.HasPrefix(aString, bString)
    }, nil)umberValidator), nil)

    engine.AddOperator(o, nil)

Facts shared or calculated facts can be added to the engine via the AddFact or AddCalculatedFact method.

Calculated facts are facts that are calculated at runtime ONCE and then reused in the rules engine.

err := engine.AddCalculatedFact("personalFoulLimit", func(a *rulesEngine.Almanac, params ...interface{}) *rulesEngine.ValueNode {
    return &rulesEngine.ValueNode{Type: rulesEngine.Number, Number: 50}
}, nil)

// or

err := engine.AddFact("test.fact", &rulesEngine.ValueNode{Type: rulesEngine.Number, Number: 50}, nil)


Examples

Basic Example

This example demonstrates an engine for detecting whether a basketball player has fouled out (a player who commits five personal fouls over the course of a 40-minute game, or six in a 48-minute game, fouls out).

package main
import (
    "context"
    "encoding/json"
    "fmt"
    rulesEngine "github.com/nimbit-software/gojson-rules-engine/cmd"
    "os"
)

func main() {

	rule := []byte(`{
  "conditions": {
    "any": [
      {
        "all": [
          {
            "fact": "gameDuration",
            "operator": "equal",
            "value": 40
          },
          {
            "fact": "personalFoulCount",
            "operator": "greaterThanInclusive",
            "value": 5
          }
        ]
      },
      {
        "all": [
          {
            "fact": "gameDuration",
            "operator": "equal",
            "value": 48
          },
          {
            "fact": "personalFoulCount",
            "operator": "greaterThanInclusive",
            "value": 6
          }
        ]
      }
    ]
  },
  "event": {
    "type": "fouledOut",
    "params": {
      "message": "Player has fouled out!"
    }
  }
}`)

	// CONTEXT FOR EARLY-STOPPING 
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// ENGINE OPTIONS
	ep := &rulesEngine.RuleEngineOptions{
		AllowUndefinedFacts: true,
	}
    // BUILD ENGINE
	engine := rulesEngine.NewEngine(nil, ep)

	// PARSE RULE
	var ruleConfig rulesEngine.RuleConfig
	if err := json.Unmarshal(rule, &ruleConfig); err != nil {
		panic(err)
	}

    // CREATE RULE
	rule, err := rulesEngine.NewRule(&ruleConfig)
	
	// ADD RULE TO ENGINE
	err = engine.AddRule(rule)

	facts := []byte(`{
            "personalFoulCount": 6,
            "gameDuration": 40,
            "name": "John",
            "user": {
                "lastName": "Jones"
            }
        }`)

    // THE ENGINE CAN RUN BOTH A MAP AND A JSON BYTE ARRAY
    res, err := engine.Run(ctx, facts)
	if err != nil {
		panic(err)
	}
    // OR 
	
	factMap := map[string]interface{}{
        "personalFoulCount": 6,
        "gameDuration": 40,
        "name": "John",
        "user": map[string]interface{}{
            "lastName": "Jones",
        },
    }
	
	res, err = engine.RunWithMap(ctx, factMap)
	if err != nil {
		panic(err)
	}
	
}	

More example coming soon

Debugging

To see what the engine is doing under the hood, debug output can be turned on via:

Environment Variables

DEBUG=json-rules-engine

Benchmarking

There is some very basic benchmarking to allow you to test the performance of the engine.

The first test is with a single go routine and the second one with 10 go routines.

go test ./benchmarks -bench=. -run=^$ -benchmem -v 

Current Results

# 1.000 iterations
BenchmarkRuleEngine took 35.2002ms for 1000 itterations
BenchmarkRuleEngineBasic-16                 1000             35200 ns/op            7338 B/op        108 allocs/op
BenchmarkRuleEngineWithPath took 2.9516ms for 1000 iterations
BenchmarkRuleEngineWithPath-16              1000              2952 ns/op            5595 B/op         76 allocs/op

# 10.000 iterations
BenchmarkRuleEngine took 316.1679ms for 10000 itterations
BenchmarkRuleEngineBasic-16                10000             31617 ns/op            6449 B/op        108 allocs/op
BenchmarkRuleEngineWithPath took 19.159ms for 10000 iterations
BenchmarkRuleEngineWithPath-16             10000              1916 ns/op            4930 B/op         77 allocs/op


# 100.000 iterations
BenchmarkRuleEngine took 3.2104305s for 100000 itterations
BenchmarkRuleEngineBasic-16               100000             32109 ns/op            6414 B/op        108 allocs/op
BenchmarkRuleEngineWithPath took 194.997ms for 100000 iterations
BenchmarkRuleEngineWithPath-16            100000              1950 ns/op            4847 B/op         75 allocs/op


License

ISC

# Packages

No description provided by the author
No description provided by the author

# Functions

Debug logs the message if the DEBUG environment variable contains "json-rules-engine".
DefaultOperators returns a slice of default operators.
DefaultRuleEngineOptions returns a default set of options for the rules engine.
EvalEndsWith checks if the string in the first ValueNode ends with the string in the second ValueNode.
EvalEqual checks if two ValueNode instances are equal.
EvalGreaterOrEqual checks if the first ValueNode is greater than or equal to the second.
EvalGreaterThan checks if the first ValueNode is greater than the second.
EvalIn checks if a ValueNode instance is present in an array of ValueNode instances.
EvalIncludes checks if the string in the first ValueNode contains with the string in the second ValueNode.
EvalLessThan checks if the first ValueNode is less than the second.
EvalLessThanOrEqual checks if the first ValueNode is less than or equal to the second.
EvalNotEquals checks if two ValueNode instances are not equal.
EvalNotIn checks if a ValueNode instance is not present in an array of ValueNode instances.
EvalStartsWith checks if the string in the first ValueNode starts with the string in the second ValueNode.
No description provided by the author
isObjectLike checks if the value is an object-like structure.
NewAlmanac creates and returns a new Almanac instance.
NewCalculatedFact creates a new Fact instance with a dynamic calculation method.
NewEngine creates a new Engine instance with the provided rules and options.
No description provided by the author
NewFact creates a new Fact instance with a static value.
No description provided by the author
No description provided by the author
No description provided by the author
NewOperator adds a new operator to the engine.
No description provided by the author
NewRule creates a new Rule instance.
NewRuleResult creates a new RuleResult instance.
NewUndefinedFactError creates a new UndefinedFactError instance.
NewValueFromGjson converts a gjson.Result into a ValueNode.

# Constants

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

Almanac is a struct that manages fact results lookup and caching within a rules engine.
Condition represents an individual condition within a rule in the rules engine.
No description provided by the author
ConditionProperties represents a condition inEvaluator the rule.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
ExecutionContext holds metadata and control flags for rule execution.
Fact represents a fact within the rules engine.
FactMap is a thread-safe map used to store and manage facts in the rules engine.
No description provided by the author
InvalidRuleError represents an error for an invalid rule.
Operator defines a function that compares two ValueNodes and returns a boolean result.
Options defines the optional settings for the Almanac.
Rule represents a rule in the engine.
No description provided by the author
No description provided by the author
RuleProperties represents the properties of a rule.
RuleResult represents the result of a rule evaluation.
TopLevelCondition represents the top-level condition, which can be AllConditions, AnyConditions, NotConditions, or ConditionReference.
UndefinedFactError represents an error for an undefined fact.
ValueNode represents a value used in conditions and comparisons.

# Type aliases

No description provided by the author
No description provided by the author
No description provided by the author
EventHandler represents an event handler function.
No description provided by the author