https://github.com/myntra/roulette
A text/template based rules engine
https://github.com/myntra/roulette
business-rules business-rules-engine rules rules-engine
Last synced: 12 months ago
JSON representation
A text/template based rules engine
- Host: GitHub
- URL: https://github.com/myntra/roulette
- Owner: myntra
- License: mit
- Created: 2017-04-26T22:41:23.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2017-05-13T07:36:49.000Z (almost 9 years ago)
- Last Synced: 2025-03-28T08:04:07.167Z (about 1 year ago)
- Topics: business-rules, business-rules-engine, rules, rules-engine
- Language: Go
- Size: 2.43 MB
- Stars: 43
- Watchers: 13
- Forks: 11
- Open Issues: 1
-
Metadata Files:
- Readme: Readme.md
- License: LICENSE
Awesome Lists containing this project
README

Roulette
A text/template based package which triggers actions from rules defined in an xml file.
---
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Overview](#overview)
- [Guide](#guide)
- [Roulette XML file:](#roulette-xml-file)
- [Tags and Attributes](#tags-and-attributes)
- [Roulette](#roulette)
- [Ruleset](#ruleset)
- [Rule](#rule)
- [Rule Expressions](#rule-expressions)
- [Defining Rules in XML](#defining-rules-in-xml)
- [Parsers](#parsers)
- [TextTemplateParser](#texttemplateparser)
- [Results](#results)
- [ResultCallback](#resultcallback)
- [ResultQueue](#resultqueue)
- [Executors](#executors)
- [SimpleExecutor](#simpleexecutor)
- [SimpleExecutor with Callback](#simpleexecutor-with-callback)
- [QueueExecutor](#queueexecutor)
- [Builtin Functions](#builtin-functions)
- [Attributions](#attributions)
## Features
- Builtin functions for writing simple rule expressions.
- Supports injecting custom functions.
- Can namespace a set of rules for custom `types`.
- Allows setting priority of a `rule`.
This pacakge is used for firing business actions based on a textual decision tree. It uses the powerful control structures in `text/template` and xml parsing from `encoding/xml` to build the tree from a `roulette` xml file.
## Installation
```
$ go get github.com/myntra/roulette
```
## Usage
### Overview
From `examples/rules.xml`
```xml
with .MyData
le .types.Person.Vacations 5 |
and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |
eq .types.Person.Position "SSE" |
.types.Person.SetAge 25
end
with .MyData
le .types.Person.Vacations 5 |
and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |
eq .types.Person.Position "SSE" |
.result.Put .types.Person
end
with .MyData
le .types.Person.Vacations 5 |
and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |
eq .types.Person.Position "SSE" |
eq .types.Company.Name "Myntra" |
.result.Put .types.Company |
end
with .MyData
eq .types.Company.Name "Myntra" | .types.Person.SetSalary 30000
end
```
From `examples/...`
`simple`
```go
...
p := types.Person{ID: 1, Age: 20, Experience: 7, Vacations: 5, Position: "SSE"}
c := types.Company{Name: "Myntra"}
config := roulette.TextTemplateParserConfig{}
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(&p, &c, []string{"hello"}, false, 4, 1.23)
if p.Age != 25 {
log.Fatal("Expected Age to be 25")
}
...
```
`workflows`
```go
...
p := types.Person{ID: 1, Age: 20, Experience: 7, Vacations: 5, Position: "SSE"}
c := types.Company{Name: "Myntra"}
config := roulette.TextTemplateParserConfig{
WorkflowPattern: "demotion*",
}
// set the workflow pattern
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(&p, &c, []string{"hello"}, false, 4, 1.23)
if p.Salary != 30000 {
log.Fatal("Expected Salary to be 30000")
}
if p.Age != 20 {
log.Fatal("Expected Age to be 20")
}
...
```
`callback`
```go
...
count := 0
callback := func(vals interface{}) {
fmt.Println(vals)
count++
}
config := roulette.TextTemplateParserConfig{
Result: roulette.NewResultCallback(callback),
}
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(testValuesCallback...)
if count != 2 {
log.Fatalf("Expected 2 callbacks, got %d", count)
}
...
```
`queue`
```go
...
in := make(chan interface{})
out := make(chan interface{})
config := roulette.TextTemplateParserConfig{
Result: roulette.NewResultQueue(),
}
// get rule results on a queue
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewQueueExecutor(parser)
executor.Execute(in, out)
//writer
go func(in chan interface{}, values []interface{}) {
for _, v := range values {
in <- v
}
}(in, testValuesQueue)
expectedResults := 2
read:
for {
select {
case v := <-out:
expectedResults--
fmt.Println(v)
switch tv := v.(type) {
case types.Person:
// do something
if !(tv.ID == 4 || tv.ID == 3) {
log.Fatal("Unexpected Result", tv)
}
}
if expectedResults == 0 {
break read
}
if expectedResults < 0 {
log.Fatalf("received %d more results", -1*expectedResults)
}
case <-time.After(time.Second * 5):
log.Fatalf("received %d less results", expectedResults)
}
}
...
```
## Guide
### Roulette XML file:
#### Tags and Attributes
##### Roulette
`roulette` is the root tag of the xml. It could contain a list of `ruleset` tags.
##### Ruleset
`ruleset`: a types namespaced tag with `rule` children. The attributes `filterTypes` and `dataKey` are **required**. To match `ruleset` , atleast one of the types from this list should be an input for the executor.
`Attributes`:
- `filterTypes`: "T1,T2,T3..."(required) allow one or all of the types for the rules group. * pointer filterting is not done.
- `filterStrict`: true or false. rules group executed only when all types are present.
- `prioritiesCount`: "1" or "2" or "3"..."all". if 1 then execution stops after "n" top priority rules are executed. "all" executes all the rules
- `dataKey`: "string" (required) root key from which user data can be accessed.
- `resultKey`: "string" key from which result.put function can be accessed. default value is "result".
- `workflow`: "string" to group rulesets to the same workflow. The parser can then be created with a wildcard pattern to filter out rilesets. "*", "?" glob pattern matching is expected.
##### Rule
The tag which holds the `rule expression`. The attributes `name` and `priority` are **optional**. The default value of `priority` is 0. There is no guarantee for order of execution if `priority` is not set.
`Attributes`:
- `name`: name of the rule.
- `priority`: priority rank of the rule within the ruleset.
##### Rule Expressions
Valid `text/template` expression. The delimeters can be changed from the default `` using the parse api.
#### Defining Rules in XML
- Write valid `text/template` control structures within the `...` tag.
- Namespace rules by custom types. e.g:
`...`
- Set `priority` of rules within namespace `filterTypes`.
- Add custom functions to the parser using the method `parser.AddFuncs`. The function must have the signature:
`func(arg1,...,argN,prevVal ...bool)bool`
to allow rule execution status propagation.
- Methods to be invoked from the rules file must also be of the above signature.
- Invalid/Malformed rules are skipped and the error is logged.
- The pipe `|` operator takes a previously evaluated value and passes it to the next function as the last argument.
- For more information on go templating: [text/template](https://golang.org/pkg/text/template/)
### Parsers
#### TextTemplateParser
Right now the package provides a single parser: `TextTemplateParser`. As the name suggests the parser is able to read xml wrapped over a valid `text/template` expression and executes it.
### Results
Types which implements the `roulette.Result`. If a parser is initalised with a `Result` type, rule expressions with `result.Put` become valid. `result.Put` function accepts an `interface{}` type as a parameter.
#### ResultCallback
`roulette.ResultCallback`: An implementation of the `roulette.Result` interface which callbacks the provided function with `result.Put` value.
#### ResultQueue
`roulette.ResultQueue`: An implementation of the `roulette.Result` interface which puts the value received from `result.Put` on a channel.
### Executors
An executor takes a parser and tests an incoming values against the rulesets. Executors implement the `roulette.SimpleExecute` and `roulette.QueueExecute` interfaces. The result is then caught by a struct which implements the `roulette.Result` interface.
#### SimpleExecutor
A simple implementation of the `roulette.SimpleExecute` interface which has a `parser` with `nil` `Result` set. This is mainly used to directly modify the input object. The executor ignores rule expressions which contain `result.Put`.
```go
parser,err := NewTextTemplateParser(data, nil,"")
// or parser, err := roulette.NewSimpleParser(data,nil,"")
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(t1,t2)
```
##### SimpleExecutor with Callback
An implementation of the `roulette.SimpleExecute` interface. which accepts a parser initialized with `roulette.ResultCallback`.
```go
config := roulette.TextTemplateParserConfig{}
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(...)
```
#### QueueExecutor
An implementation of the `roulette.QueueExecute` interface. which accepts the `roulette.ResultQueue` type. The `Execute` method expects an input and an output channel to write values and read results respectively.
```go
in := make(chan interface{})
out := make(chan interface{})
config := roulette.TextTemplateParserConfig{
Result: roulette.NewResultQueue(),
}
// get rule results on a queue
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewQueueExecutor(parser)
executor.Execute(in, out)
```
For concrete examples of the above please see the `examples` directory.
## Builtin Functions
Apart from the built-in functions from `text/template`, the following functions are available.
Default functions reside in `funcmap.go`. They have been sourced and modified from `src/text/template/funcs.go` to make them work with pipelines and keep the expression uncluttered.
The idea is to keep the templating language readable and easy to write.
| Function | Usage |
| ------------- |:-------------:|
| in | `val >= minVal && val <= maxval`, e.g. `in 2 1 4` => `true` |
| gt | `> op`, e.g. `gt 1 2` |
| ge | `>= op`, e.g. `ge 1 2` |
| lt | `<= op`, e.g. `lt 1 2` |
| le | `< op`, e.g. `le 1 2` |
| eq | `== op`, e.g. `eq "hello" "world"` |
| ne | `!= op`, e.g.`ne "hello" "world"` |
| not | `!op` , e.g.`not 1`|
| and | `op1 && op2`, e.g. `and (expr1) (expr2)`|
| or | `op1 // op2`, e.g. `or (expr1) (expr2)`|
| result.Put | `result.Put Value` where `result` is the defined `resultKey`|
- pipe operator | : `Usage: the output of fn1 is the last argument of fn2`, e.g. `fn1 1 2| fn2 1 2 `
The functions from the excellent package [sprig](http://masterminds.github.io/sprig/) are also available.
## Attributions
The `roulette.png` image is sourced from https://thenounproject.com/term/roulette/143243/ with a CC license.