{"id":21862895,"url":"https://github.com/myntra/roulette","last_synced_at":"2025-04-14T19:43:03.546Z","repository":{"id":57542413,"uuid":"89532920","full_name":"myntra/roulette","owner":"myntra","description":"A text/template based rules engine","archived":false,"fork":false,"pushed_at":"2017-05-13T07:36:49.000Z","size":2548,"stargazers_count":43,"open_issues_count":1,"forks_count":11,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-03-28T08:04:07.167Z","etag":null,"topics":["business-rules","business-rules-engine","rules","rules-engine"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/myntra.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-26T22:41:23.000Z","updated_at":"2024-10-27T13:40:43.000Z","dependencies_parsed_at":"2022-09-26T18:31:36.073Z","dependency_job_id":null,"html_url":"https://github.com/myntra/roulette","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/myntra%2Froulette","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/myntra%2Froulette/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/myntra%2Froulette/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/myntra%2Froulette/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/myntra","download_url":"https://codeload.github.com/myntra/roulette/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248949238,"owners_count":21188037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["business-rules","business-rules-engine","rules","rules-engine"],"created_at":"2024-11-28T03:17:56.654Z","updated_at":"2025-04-14T19:43:03.496Z","avatar_url":"https://github.com/myntra.png","language":"Go","readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://cdn.rawgit.com/myntra/roulette/master/images/roulette.png\" height=\"118\" width=\"130\" /\u003e\n\n  \u003ch3 align=\"center\"\u003eRoulette\u003c/h3\u003e\n  \u003cp align=\"center\"\u003eA text/template based package which triggers actions from rules defined in an xml file.\u003c/p\u003e\n  \u003cp align=\"center\"\u003e\n  \t\u003cimg src=\"http://badges.github.io/stability-badges/dist/experimental.svg\"/\u003e\n    \u003ca href='https://coveralls.io/github/myntra/roulette?branch=master'\u003e\u003cimg src='https://coveralls.io/repos/github/myntra/roulette/badge.svg?branch=master' alt='Coverage Status' /\u003e\u003c/a\u003e\n    \u003ca href=\"https://travis-ci.org/myntra/roulette\"\u003e\u003cimg src=\"https://travis-ci.org/myntra/roulette.svg?branch=master\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://godoc.org/github.com/myntra/roulette\"\u003e\u003cimg src=\"https://godoc.org/github.com/myntra/roulette?status.svg\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://goreportcard.com/report/github.com/myntra/roulette\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/myntra/roulette\"\u003e\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/p\u003e\n\n---\n\u003c!-- TOC --\u003e\n\n- [Features](#features)\n- [Installation](#installation)\n- [Usage](#usage)\n    - [Overview](#overview)\n- [Guide](#guide)\n    - [Roulette XML file:](#roulette-xml-file)\n        - [Tags and Attributes](#tags-and-attributes)\n            - [Roulette](#roulette)\n            - [Ruleset](#ruleset)\n            - [Rule](#rule)\n            - [Rule Expressions](#rule-expressions)\n        - [Defining Rules in XML](#defining-rules-in-xml)\n    - [Parsers](#parsers)\n        - [TextTemplateParser](#texttemplateparser)\n    - [Results](#results)\n        - [ResultCallback](#resultcallback)\n        - [ResultQueue](#resultqueue)\n    - [Executors](#executors)\n        - [SimpleExecutor](#simpleexecutor)\n            - [SimpleExecutor with Callback](#simpleexecutor-with-callback)\n        - [QueueExecutor](#queueexecutor)\n- [Builtin Functions](#builtin-functions)\n- [Attributions](#attributions)\n\n\u003c!-- /TOC --\u003e\n\n## Features\n\n- Builtin functions for writing simple rule expressions. \n- Supports injecting custom functions.\n- Can namespace a set of rules for custom `types`.\n- Allows setting priority of a `rule`.\n\n\nThis 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.\n\n## Installation\n```\n$ go get github.com/myntra/roulette\n```\n\n## Usage\n### Overview\n\nFrom `examples/rules.xml`\n\n```xml\n\u003croulette\u003e\n    \u003c!--filterTypes=\"T1,T2,T3...\"(required) allow one or all of the types for the rules group. * pointer filterting is not done .--\u003e\n    \u003c!--filterStrict=true or false. rules group executed only when all types are present --\u003e\n    \u003c!--prioritiesCount= \"1\" or \"2\" or \"3\"...\"all\". if 1 then execution stops after \"n\" top priority rules are executed. \"all\" executes all the rules.--\u003e\n    \u003c!--dataKey=\"string\" (required) root key from which user data can be accessed. --\u003e\n    \u003c!--resultKey=\"string\" key from which result.put function can be accessed. default value is \"result\".--\u003e\n    \u003c!--workflow: \"string\" to group rulesets to the same workflow.--\u003e\n\n    \u003cruleset name=\"personRules\" dataKey=\"MyData\" resultKey=\"result\" filterTypes=\"types.Person,types.Company\" \n        filterStrict=\"false\" prioritiesCount=\"all\"  workflow=\"promotioncycle\"\u003e\n\n        \u003crule name=\"personFilter1\" priority=\"3\"\u003e\n                \u003cr\u003ewith .MyData\u003c/r\u003e\n                    \u003cr\u003e\n                       le .types.Person.Vacations 5 |\n                       and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |\n                       eq .types.Person.Position \"SSE\" |\n                       .types.Person.SetAge 25\n                    \u003c/r\u003e\n                \u003cr\u003eend\u003c/r\u003e\n        \u003c/rule\u003e\n\n        \u003crule name=\"personFilter2\" priority=\"2\"\u003e\n                \u003cr\u003ewith .MyData\u003c/r\u003e\n                    \u003cr\u003e\n                       le .types.Person.Vacations 5 |\n                       and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |\n                       eq .types.Person.Position \"SSE\"  |\n                       .result.Put .types.Person\n                    \u003c/r\u003e\n                \u003cr\u003eend\u003c/r\u003e\n        \u003c/rule\u003e\n\n        \u003crule name=\"personFilter3\" priority=\"1\"\u003e\n            \u003cr\u003ewith .MyData\u003c/r\u003e\n                    \u003cr\u003e\n                    le .types.Person.Vacations 5 |\n                    and (gt .types.Person.Experience 6) (in .types.Person.Age 15 30) |\n                    eq .types.Person.Position \"SSE\"  |\n                    eq .types.Company.Name \"Myntra\" |\n                     .result.Put .types.Company |\n                    \u003c/r\u003e\n            \u003cr\u003eend\u003c/r\u003e\n        \u003c/rule\u003e\n    \u003c/ruleset\u003e\n\n    \u003cruleset name=\"personRules2\" dataKey=\"MyData\" resultKey=\"result\" filterTypes=\"types.Person,types.Company\" \n    filterStrict=\"false\" prioritiesCount=\"all\" workflow=\"demotioncycle\"\u003e\n    \u003crule name=\"personFilter1\" priority=\"1\"\u003e\n        \u003cr\u003ewith .MyData\u003c/r\u003e\n            \u003cr\u003e\n                eq .types.Company.Name \"Myntra\" | .types.Person.SetSalary 30000\n            \u003c/r\u003e\n        \u003cr\u003eend\u003c/r\u003e\n    \u003c/rule\u003e\n    \u003c/ruleset\u003e    \n\u003c/roulette\u003e\n```\n\nFrom `examples/...`\n\n`simple`\n\n```go\n...\n   \tp := types.Person{ID: 1, Age: 20, Experience: 7, Vacations: 5, Position: \"SSE\"}\n\tc := types.Company{Name: \"Myntra\"}\n\n\tconfig := roulette.TextTemplateParserConfig{}\n\n\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texecutor := roulette.NewSimpleExecutor(parser)\n\texecutor.Execute(\u0026p, \u0026c, []string{\"hello\"}, false, 4, 1.23)\n\n\tif p.Age != 25 {\n\t\tlog.Fatal(\"Expected Age to be 25\")\n\t}\n\n\n  ...\n```\n\n`workflows`\n\n```go\n...\n\n\tp := types.Person{ID: 1, Age: 20, Experience: 7, Vacations: 5, Position: \"SSE\"}\n\tc := types.Company{Name: \"Myntra\"}\n\n\tconfig := roulette.TextTemplateParserConfig{\n\t\tWorkflowPattern: \"demotion*\",\n\t}\n\t// set the workflow pattern\n\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texecutor := roulette.NewSimpleExecutor(parser)\n\texecutor.Execute(\u0026p, \u0026c, []string{\"hello\"}, false, 4, 1.23)\n\n\tif p.Salary != 30000 {\n\t\tlog.Fatal(\"Expected Salary to be 30000\")\n\t}\n\n\tif p.Age != 20 {\n\t\tlog.Fatal(\"Expected Age to be 20\")\n\t}\n...\n```\n\n\n`callback`\n\n```go\n...\n    count := 0\n\tcallback := func(vals interface{}) {\n\t\tfmt.Println(vals)\n\t\tcount++\n\t}\n\n\tconfig := roulette.TextTemplateParserConfig{\n\t\tResult: roulette.NewResultCallback(callback),\n\t}\n\n\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texecutor := roulette.NewSimpleExecutor(parser)\n\texecutor.Execute(testValuesCallback...)\n\tif count != 2 {\n\t\tlog.Fatalf(\"Expected 2 callbacks, got %d\", count)\n\t}\n...\n```\n`queue`\n\n```go\n...\nin := make(chan interface{})\n\tout := make(chan interface{})\n\n\tconfig := roulette.TextTemplateParserConfig{\n\t\tResult: roulette.NewResultQueue(),\n\t}\n\n\t// get rule results on a queue\n\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texecutor := roulette.NewQueueExecutor(parser)\n\texecutor.Execute(in, out)\n\n\t//writer\n\tgo func(in chan interface{}, values []interface{}) {\n\n\t\tfor _, v := range values {\n\t\t\tin \u003c- v\n\t\t}\n\n\t}(in, testValuesQueue)\n\n\texpectedResults := 2\n\nread:\n\tfor {\n\t\tselect {\n\t\tcase v := \u003c-out:\n\t\t\texpectedResults--\n\t\t\tfmt.Println(v)\n\t\t\tswitch tv := v.(type) {\n\t\t\tcase types.Person:\n\t\t\t\t// do something\n\t\t\t\tif !(tv.ID == 4 || tv.ID == 3) {\n\t\t\t\t\tlog.Fatal(\"Unexpected Result\", tv)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif expectedResults == 0 {\n\t\t\t\tbreak read\n\t\t\t}\n\n\t\t\tif expectedResults \u003c 0 {\n\t\t\t\tlog.Fatalf(\"received  %d more results\", -1*expectedResults)\n\t\t\t}\n\n\t\tcase \u003c-time.After(time.Second * 5):\n\t\t\tlog.Fatalf(\"received  %d less results\", expectedResults)\n\t\t}\n\t}\n...\n```\n\n## Guide\n### Roulette XML file: \n\n#### Tags and Attributes\n\n##### Roulette\n  \n  `roulette` is the root tag of the xml. It could contain a list of `ruleset` tags. \n\n##### Ruleset\n\n`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. \n\n`Attributes`: \n\n- `filterTypes`: \"T1,T2,T3...\"(required) allow one or all of the types for the rules group. * pointer filterting is not done.\n\n- `filterStrict`: true or false. rules group executed only when all types are present.\n\n- `prioritiesCount`: \"1\" or \"2\" or \"3\"...\"all\". if 1 then execution stops after \"n\" top priority rules are executed. \"all\" executes all the rules\n\n- `dataKey`: \"string\" (required) root key from which user data can be accessed.\n\n- `resultKey`: \"string\" key from which result.put function can be accessed. default value is \"result\".\n- `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.\n\n\n##### Rule\n\nThe 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.\n\n`Attributes`:\n\n- `name`: name of the rule.\n\n- `priority`: priority rank of the rule within the ruleset. \n\n\n##### Rule Expressions\n\nValid `text/template` expression. The delimeters can be changed from the default `\u003cr\u003e\u003c/r\u003e` using the parse api.\n\n#### Defining Rules in XML\n\n- Write valid `text/template` control structures within the `\u003crule\u003e...\u003c/rule\u003e` tag.\n- Namespace rules by custom types. e.g: \n\n\t`\u003cruleset filterTypes=\"Person,Company\"\u003e...\u003c/ruleset\u003e`\n\n- Set `priority` of rules within namespace `filterTypes`.\n- Add custom functions to the parser using the method `parser.AddFuncs`. The function must have the signature:\n\t\n\t`func(arg1,...,argN,prevVal ...bool)bool`\n \n  to allow rule execution status propagation.\n- Methods to be invoked from the rules file must also be of the above signature.\n- Invalid/Malformed rules are skipped and the error is logged.\n- The pipe `|` operator takes a previously evaluated value and passes it to the next function as the last argument.\n- For more information on go templating: [text/template](https://golang.org/pkg/text/template/)\n\n\n### Parsers\n\n#### TextTemplateParser\nRight 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.\n\n\n### Results\n\nTypes 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.\n\n#### ResultCallback\n\n`roulette.ResultCallback`: An implementation of the `roulette.Result` interface which callbacks the provided function with `result.Put` value.\n\n#### ResultQueue \n\n`roulette.ResultQueue`: An implementation of the `roulette.Result` interface which puts the value received from `result.Put` on a channel.\n\n### Executors\n\nAn 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. \n\n#### SimpleExecutor\n\nA 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`.\n\n```go\n\nparser,err := NewTextTemplateParser(data, nil,\"\")\n// or parser, err := roulette.NewSimpleParser(data,nil,\"\")\nexecutor := roulette.NewSimpleExecutor(parser)\nexecutor.Execute(t1,t2)\n```\n\n##### SimpleExecutor with Callback\n\nAn implementation of the `roulette.SimpleExecute` interface. which accepts a parser initialized with `roulette.ResultCallback`.\n\n```go\n    config := roulette.TextTemplateParserConfig{}\n\n\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texecutor := roulette.NewSimpleExecutor(parser)\n\texecutor.Execute(...)\n```\n\n#### QueueExecutor \n\nAn 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. \n\n```go\n\nin := make(chan interface{})\n\t\tout := make(chan interface{})\n\n\t\tconfig := roulette.TextTemplateParserConfig{\n\t\t\tResult: roulette.NewResultQueue(),\n\t\t}\n\n\t\t// get rule results on a queue\n\t\tparser, err := roulette.NewParser(readFile(\"../rules.xml\"), config)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\texecutor := roulette.NewQueueExecutor(parser)\n\t\texecutor.Execute(in, out)\n```\n\nFor concrete examples of the above please see the `examples` directory. \n\n\n## Builtin Functions\n\nApart from the built-in functions from `text/template`, the following functions are available.\n\nDefault 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.\nThe idea is to keep the templating language readable and easy to write.\n\n| Function      | Usage         |\n| ------------- |:-------------:|\n| in           |  `val \u003e= minVal \u0026\u0026 val \u003c= maxval`, e.g. `in 2 1 4` =\u003e `true` | \n| gt           |  `\u003e op`, e.g. `gt 1 2` |\n| ge           |  `\u003e= op`, e.g. `ge 1 2` |\n| lt           |  `\u003c= op`, e.g. `lt 1 2` |\n| le           |  `\u003c op`, e.g. `le 1 2` |\n| eq           |  `== op`, e.g. `eq \"hello\" \"world\"` |\n| ne           |  `!= op`, e.g.`ne \"hello\" \"world\"` |\n| not\t\t   | `!op` , e.g.`not 1`|\n| and \t\t   | `op1 \u0026\u0026 op2`, e.g. `and (expr1) (expr2)`|\n| or \t\t   | `op1 // op2`, e.g. `or (expr1) (expr2)`|\n| result.Put   | `result.Put Value` where `result` is the defined `resultKey`|\n \n\n\n - pipe operator | : `Usage: the output of fn1 is the last argument of fn2`, e.g. `fn1 1 2| fn2 1 2 `\n\nThe functions from the excellent package [sprig](http://masterminds.github.io/sprig/) are also available.\n\n## Attributions\nThe `roulette.png` image is sourced from https://thenounproject.com/term/roulette/143243/ with a CC license.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmyntra%2Froulette","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmyntra%2Froulette","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmyntra%2Froulette/lists"}