A text/template based package which triggers actions from rules defined in an xml file.
- 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.
$ go get github.com/myntra/roulette
From examples/rules.xml
<roulette>
<!--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.-->
<ruleset name="personRules" dataKey="MyData" resultKey="result" filterTypes="types.Person,types.Company"
filterStrict="false" prioritiesCount="all" workflow="promotioncycle">
<rule name="personFilter1" priority="3">
<r>with .MyData</r>
<r>
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
</r>
<r>end</r>
</rule>
<rule name="personFilter2" priority="2">
<r>with .MyData</r>
<r>
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
</r>
<r>end</r>
</rule>
<rule name="personFilter3" priority="1">
<r>with .MyData</r>
<r>
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 |
</r>
<r>end</r>
</rule>
</ruleset>
<ruleset name="personRules2" dataKey="MyData" resultKey="result" filterTypes="types.Person,types.Company"
filterStrict="false" prioritiesCount="all" workflow="demotioncycle">
<rule name="personFilter1" priority="1">
<r>with .MyData</r>
<r>
eq .types.Company.Name "Myntra" | .types.Person.SetSalary 30000
</r>
<r>end</r>
</rule>
</ruleset>
</roulette>
From examples/...
simple
...
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
...
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
...
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
...
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)
}
}
...
roulette
is the root tag of the xml. It could contain a list of ruleset
tags.
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.
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.
Valid text/template
expression. The delimeters can be changed from the default <r></r>
using the parse api.
-
Write valid
text/template
control structures within the<rule>...</rule>
tag. -
Namespace rules by custom types. e.g:
<ruleset filterTypes="Person,Company">...</ruleset>
-
Set
priority
of rules within namespacefilterTypes
. -
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
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.
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.
roulette.ResultCallback
: An implementation of the roulette.Result
interface which callbacks the provided function with result.Put
value.
roulette.ResultQueue
: An implementation of the roulette.Result
interface which puts the value received from result.Put
on a channel.
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.
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
.
parser,err := NewTextTemplateParser(data, nil,"")
// or parser, err := roulette.NewSimpleParser(data,nil,"")
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(t1,t2)
An implementation of the roulette.SimpleExecute
interface. which accepts a parser initialized with roulette.ResultCallback
.
config := roulette.TextTemplateParserConfig{}
parser, err := roulette.NewParser(readFile("../rules.xml"), config)
if err != nil {
log.Fatal(err)
}
executor := roulette.NewSimpleExecutor(parser)
executor.Execute(...)
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.
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.
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 are also available.
The roulette.png
image is sourced from https://thenounproject.com/term/roulette/143243/ with a CC license.