SFDCRules – Simple yet powerful Rule Engine for Salesforce

Open source project to evaluate rules in Salesforce using Apex

SFDC Rule Engine

Coming from Java background, we know that there are many open source or free Business Rules Management System (BRMS) tools that can be used like Easy rules or Drools. I was in search of such tool for Salesforce but didn’t find any. There are few AppExchange BRMS products however they are paid and heavy in terms of features needed. Who don’t love free stuff 🙂 and wanted it free and open source. While searching, I came across this post of Martin Fowler and it encouraged me to write my own rule engine for Salesforce.

So, here it is. Free, light weight, basic but powerful rule engine written in Apex for Apex.

Why would I need SFDCRules ?

Lets say, you need to implement some complex routing rule to assign opportunity to proper sales person. Rules could be like, if Opportunity state is CT then assign it to someone who worked on latest Won Opportunity for same state. We cannot use assignment rules in these case. We cannot hard-code this condition in Trigger or Apex, as it could be changed in future and therefore maintenance would be problem. We need kind of framework, which will read conditions from custom object, evaluate weather its true or false and according perform operation.
There are three pieces here –
1. Get binding values which could be used to drive (in this case Opp State)
2. Evaluating condition
3. Taking action if its true / false. (Assign Opp to someone who worked on latest Won Opportunity)
Lets say, we want to use Workflow rule , how we will take action, because it needs to read latest Opportunity.

So seems, going custom could be the way. In this case as well, end user wants to declare rule on fly. We can give them wizard, where they can choose, objects, fields, and condition. Other part of wizard would be what action needs to taken if condition is true. For action part, it would be altogether different topic, so lets skip it. Now, the question is, how to develop Apex code, which can work like Workflow rule or Assignment rule, which can evaluate conditions on basis of merge fields passed and return true or false. I know it might not be best solution to the problem, this requirement may raise many more questions however wanted to just focus on dynamically condition evaluation problem part.

How to use SFDCRules

Its as simple as saying 123 😉 . In order to use SFDCRules, we need to follow below steps

  1. Define set of all allowed operators. We can move this step to some helper method to skip this step
  2. Define set of binding values, which will replace variable or merge fields in rule
  3. call eval() method of Rule class and it will return Boolean value indicating that rule evaluated is true or false
//Define set of all allowed operators
//Mostly we don't need to change this, it can be added in setup method
Operations opObj = Operations.getInstance(); 
opObj.registerOperation(OperationFactory.getInstance('&&'));
opObj.registerOperation(OperationFactory.getInstance('==')); 
opObj.registerOperation(OperationFactory.getInstance('!=')); 
opObj.registerOperation(OperationFactory.getInstance('||'));
opObj.registerOperation(OperationFactory.getInstance('('));
opObj.registerOperation(OperationFactory.getInstance(')'));
opObj.registerOperation(OperationFactory.getInstance('<'));
opObj.registerOperation(OperationFactory.getInstance('<=')); opObj.registerOperation(OperationFactory.getInstance('>'));
opObj.registerOperation(OperationFactory.getInstance('>='));

//Define bindings, which will replace variables while
//evaluating rules		
Map<String, String> bindings = new Map<String, String>();
bindings.put('Case.OwnerName__c'.toLowerCase(), 'Jitendra');   
bindings.put('Case.IsEscalated'.toLowerCase(), 'false');  
bindings.put('Case.age_mins__c'.toLowerCase(), '62'); 

//Define rule
String expr  = 'Case.OwnerName__c == Minal || ( Case.age_mins__c &amp;amp;lt; 75 && Case.IsEscalated == false )' ; 

//Initialize Rule Engine
Rule r = new Rule().setExpression(expr);   

//Evaluate rule with Binding values
Boolean retVal = r.eval(bindings) ;

//Check if expected result is correct
System.assertEquals(true, retVal);  

Capabilities, Considerations and Limitations

  • All Operators, variables and values must be separated by one or more spaces. Spaces are used to tokenize expression. We fix this by introducing some normalizing method however it would cost some CPU time.
  • Instead of writing Value1 == 100 OR Value1 == 200 we can use comma separated values. So, it can be written as Value1 == 100,200. 
  • Comma separated value is only supported for Integer and Decimal datatype, not for Strings.
  • Arithmetic operations like Value1 < 2*3 not supported.
  • Binding variables must be used as lower case.
  • Spaces in string values are not allowed. So, Value1 == ‘My Bad’ is not supported.
  • Code does not support short circuit execution of logic yet.
    • Example : Value1 == 100 && Value2 == 400 . In this case, if first condition fails, we should not evaluate second, as result will always be false.

Performance

We are talking about lots of string manipulations and comparison in Salesforce BRMS rule engine (SFDCRules). If its not used wisely, chances of hitting CPU limit are high. Lengthy expression size will result in using more CPU time. In Synchronous Apex, we get 10 sec before hitting CPU time limit error. Speaking about performance, around 7k expressions with 3 to 4 conditions can be evaluated before hitting 10 sec.

Get Source code

More examples of using SFDCRules

/**
* @Author : Jitendra Zaa
* @Date : 25-March-2017
* @Desc : Classes to implement BPM (Business Process Management ) rule engine
*
* @Known Issue : 1. String values cannot have spaces
* : 3. All operators should have spaces , before and after, else it would raise an error.
* : There is no code written yet to auto format expression.
* */
@isTest
public class TestRuleEngine {

static Map<String, String> initBindings() {
Map<String, String> bindings = new Map<String, String>();
bindings.put('Val1'.toLowerCase(), '100');
bindings.put('Val2'.toLowerCase(), '200');
bindings.put('Val3'.toLowerCase(), '300');
bindings.put('Val4'.toLowerCase(), 'RuleEngine');
bindings.put('Val5'.toLowerCase(), 'false');
return bindings;
}

static Operations initOperations() {
Operations opObj = Operations.getInstance();
opObj.registerOperation(OperationFactory.getInstance('&&'));
opObj.registerOperation(OperationFactory.getInstance('=='));
opObj.registerOperation(OperationFactory.getInstance('!='));
opObj.registerOperation(OperationFactory.getInstance('||'));
opObj.registerOperation(OperationFactory.getInstance('('));
opObj.registerOperation(OperationFactory.getInstance(')'));
opObj.registerOperation(OperationFactory.getInstance('<'));
opObj.registerOperation(OperationFactory.getInstance('<=')); opObj.registerOperation(OperationFactory.getInstance('>'));
opObj.registerOperation(OperationFactory.getInstance('>='));
return opObj;
}

public static testmethod void condition_Test_All_Operators(){

Map<String, String> bindings = initBindings();
Operations opObj = initOperations();

//declaration
String expr ;
Rule r = new Rule() ;
Boolean retVal;

expr = 'Val1 != 800' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 != 800 || ( Val2 != 200 && Val3 != 200 )' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 &amp;lt; 110' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 &amp;lt;= 100' ; 
r.setExpression(expr); 
retVal = r.eval(bindings) ; 
System.assertEquals(true, retVal); 
expr = 'Val2 > 110.5' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val2 >= 110' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val3 == 300' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val3 == 300.0' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val4 == RuleEngine' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val4 == "ruleengine"' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val4 == ruleEngine' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val5 == false' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

//As below comparision is not valid, return false
expr = 'Val5 > false' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

//As below comparision is not valid, return false
expr = 'Val5 >= false' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

//As below comparision is not valid, return false
expr = 'Val5 < false' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

//As below comparision is not valid, return false
expr = 'Val5 <= false' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

expr = 'Val1 == 100 || ( Val2 == 300 && Val3 == 200 && Val4 == ruleEngine )' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 == 300 || ( Val2 == 300 && Val3 == 200 && Val4 == ruleEngine )' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

expr = 'Val1 == 300 || ( Val2 == 300 ) && ( Val3 == 200 ) && ( Val4 == ruleEngines )' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

expr = 'Val1 == 300 || ( ( Val2 == 300 ) && ( Val3 == 200 ) && ( Val4 == ruleEngines && Val1 == 120 ) ) || Val1 == 100' ;
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 == 1500 || Val1 == 1400 || Val1 == 1300 || Val1 == 1200 || Val1 == 1100 || Val1 == 1200 || Val1 == 1100 || Val1 == 1000'+
' || Val1 == 900 || Val1 == 800 || Val1 == 700 || Val1 == 600 || Val1 == 500 || Val1 == 400 || Val1 == 300 || Val1 == 200 || Val1 == 100';
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

expr = 'Val1 == 1500 && Val1 == 1400 && Val1 == 1300 && Val1 == 1200 && Val1 == 1100 && Val1 == 1200 && Val1 == 1100 && Val1 == 1000'+
' && Val1 == 900 && Val1 == 800 && Val1 == 700 && Val1 == 600 && Val1 == 500 && Val1 == 400 && Val1 == 300 && Val1 == 200 && Val1 == 100';
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(false, retVal);

//Test comma separated value
expr = 'Val1 == 1,2,3,4,5,6,7,100';
r.setExpression(expr);
retVal = r.eval(bindings) ;
System.assertEquals(true, retVal);

}
}

 

Related posts