nerdErg

1. One Ring - scripting rules engine

One Ring isn’t like other "Rules Engines", it’s meant to be used as a web service (SOA) for multiple applications to gain access to scripted processing of arbitrary parameters.

It centralises processing of common rules (or business rules) for multiple applications that need access to the same rules. Rules are defined using a simple language understood by domain experts.

One Ring is aimed at continuous processing for multiple small applications, not batch processing of billions of entities. It is very light weight and is deployed as a WAR inside a container like Tomcat. You can have multiple One Ring servers running in the same container pointing to different, or the same rule sets.

It has not been optimised for speed, but it’s not exactly slow. The engine can process a 1000 simple 3 rule rule sets in around 200ms or ~0.2ms per rule set. That’s serially called with fact set-up in each call.

2. Features

  • A friendly to quite a few humans DSL

  • REST and SOAP interfaces

  • JSON and XML fact encoding

  • Inbuilt rule testing in the rule set

  • Script rules in simplified or not so simplified Groovy

  • Simple directory of ruleset files which can be version controlled using any version control system like Git, Mercurial, Subversion etc.

  • Live updating of rulesets without interruption

3. Still to come

  • User management and security - at the moment it is assumed you run this server partitioned from external access, however the ability to update and access the servers REST or SOAP functionality could be secured to allow B2B scripted access.

  • Pluggable DSLs - adding Domain Specific Language processing for specific domains should be a simple thing to do.

  • Scriptable data access/lookup services - rules commonly have data tables, and including those tables in the rules directly is ugly and could be abstracted, it’s simple enough to read a file in a One Ring rule script, but accessing other REST services and databases could be simpler.

You can find the source at https://github.com/nerdErg/One-Ring

4. Getting started

Getting One Ring going is pretty simple Download the WAR file and drop it into a web container like Tomcat and point your browser at http://localhost:8080/rulesEngine (change "localhost" to the host you’re Tomcat instance is on).

Check out more details here: One Ring install

5. Rule examples

The following is an example rule set using the simple DSL. This shows three different forms of rules, 1) when, then, otherwise; 2) evaluate; and 3) Plain old Groovy.

example.groovy
ruleset("Means Test") {
    require(['income', 'expenses'])
    rule("nett income") {
        when {
            nett_income = income - expenses
            nett_income < 400.00
        }
        then {
            incomeTest = 'passed'
        }
        otherwise {
            incomeTest = 'failed'
        }
    }

    rule("nett income2") {
        evaluate {
            incomeTest2 = nett_income < 400.00
        }
    }

    rule("nett income3") {
        if (fact.nett_income > 400) {
            fact.incomeTest3 = 'rich bugger'
        } else {
            fact.incomeTest3 = 'poor bugger'
        }
    }

    test(income: 900, expenses: 501) {
        incomeTest 'passed'
        nett_income 399
    }
}

A couple of things to note from this example:

  • The rule set takes a Map of Facts

  • The facts are dynamically typed

  • The require phrase tells the rules engine that certain facts must exist or it will fail

  • When, Then and Evaluate rules assume variables are from the map of facts. If you assign a value it will be returned in the map of facts to the caller. Outside of when, then, otherwise and evaluate you need to directly reference facts.

  • "When" clauses must end in an value that will be judged true (then) or false (otherwise). This uses Groovy Truth, so a blank string evaluates to false.

  • The test phrase takes input test facts, runs them against the rules, and checks the results.

6. Rule set variables

You can define a variable outside the rules that can be used inside a rule. e.g:

variables.groovy
ruleset("Questions") {

    require(['code'])

    def questions = ['428606': 'do you want fries with that?', '428605': 'supersize that coke for ya?']

    rule("Question Punters") {
        fact.question = questions[fact.code]
    }

    test(code: '428606') {question 'do you want fries with that?'}
    test(code: '428605') {question 'supersize that coke for ya?'}
    test(code: '428607') {question null}
}

Here we define the variable Map "questions" and use it as a lookup table to get the questions associated with the code number.

regex.groovy
ruleset("milkshake"){
    require(["singer"])
    rule("singer is Kelis"){
        when { singer =~ /(?i)Kelis/ }
        then { boys = 'In the yard' }
        otherwise { boys = 'not in the yard' }
    }
    test(singer: 'Kelis') { boys 'In the yard' }
    test(singer: 'kElis') { boys 'In the yard' }
    test(singer: 'Snoop Dog') { boys 'not in the yard' }
}

Here we’re using a Groovy regex in the rule. If you make a mistake in the code One Ring rules engine will tell you.

array.groovy
ruleset('test') {
    require(['value'])
    rule('quantity within range') {
        when {
            value < 1 || value > 10
        }
        then {
            failed = true
            message = ["invalid quantity"]
        }
        otherwise {
            failed = false
            message = []
        }
    }
    test(value: 0) {
        failed true
        message (["invalid quantity"])
    }
}

This example shows how to test an array that is passed back as a fact. You need to put brackets () around the array ["invalid quantity"].

7. REST

Get started using the RESTful interface by pointing your app (a browser will do) at /rulesEngine/rest/applyRules and add a couple of parameters like:

params
ruleSet=Means Test
income=900
expenses=400

You can GET or POST your facts to the rules engine as JSON, and it will return a JSON map/object of the results.

You can post the facts as (encoded) XML and get XML results. e.g.

facts.xml
<list>
    <map>
        <entry key="income">900</entry>
        <entry key="expenses">400</entry>
    </map>
</list>

If you use POST you can do a call as pure JSON or XML using the contentType e.g. application/json or text/xml. So you can just post {ruleSet: 'Questions', facts: [{code: '428606'}]} using contentType application/json then you’ll get back a JSON response.

To send a REST call as XML you’d send the following with the contentType set to text/xml.

post.xml
<params>
    <ruleSet>Means Test</ruleSet>
    <facts>
        <list>
            <map>
                <entry key="income">900</entry>
                <entry key="expenses">300</entry>
            </map>
        </list>
    </facts>
</params>

8. SOAP

Get started with the SOAP interface by pointing your browser at /rulesEngine/services then take in the WSDL.

You can use JSON or XML encoded facts, JSON:

soap json
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rul="http://rules.nerderg.com/">
    <soapenv:Header/>
    <soapenv:Body>
        <rul:applyRules>
            <rul:ruleSet>Means Test</rul:ruleSet>
            <rul:facts>[{income: 900, expenses: 600}, {income:900, expenses: 300}]</rul:facts>
        </rul:applyRules>
    </soapenv:Body>
</soapenv:Envelope>

Note the XML escapery going on here for the XML encoded facts:

soap xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rul="http://rules.nerderg.com/">
<soapenv:Header/>
<soapenv:Body>
    <rul:applyRules>
        <rul:ruleSet>Means Test</rul:ruleSet>
        <rul:facts>
            <![CDATA[ <list> <map> <entry key="income">900</entry> <entry key="expenses">300</entry> </map> </list> ]]>
        </rul:facts>
    </rul:applyRules>
</soapenv:Body>
</soapenv:Envelope>