One model to rule them all, one qualifier to find them, one assignment to bring them all and in the context bind them.

Introduction

A very cool but little known feature available in WebObjects - and in SOPE - is the EOControl based rule system. It is used extensively by the DirectToWeb framework (part of WebObjects 4.5) to write database based web applications which require no code and are just composed of a set of rules plus a database model.
It is a bit hard to get into the rule concept and to "think in rules", but it certainly pays and is incredibly powerful if applied right.

SOPE doesn't come with a full DirectToWeb implementation (mostly because it lacks an EOF 4 implementation), but it does provide a rule engine similiar to the one used for DirectToWeb. The engine itself can be used in a lot of contexts to make application behaviour configurable without the need to write code. As demonstrated by D2W it can be even used to assemble completely new applications from prefabricated components using just rules.

So what can NGRule get used for? The applications are numerous, just some ideas:
rule based change of HTML UI based on object properties (eg bgcolor of overdue tasks), rule based control of page flow simple rule based workflows, ...
It can be used in a lot of cases where you need to select some behaviour or values depending on the state of some object. We'll provide some more examples below.

Rule Model

Lets first start by looking at the rule model so that you get an impression what it looks like prior digging into Objective-C code ;-)

Rule

A rule is always composed of a left hand side (LHS) and a right hand side (RHS), plus some optional evaluation parameters like the priority of a rule in a set:

document.status = 'published' => pageName = 'MyReviewPage'

This rule says: if the property 'status' of the object 'document' stored in the rule context is 'published', then return the value 'MyReviewPage' if the rule context is asked for the value of the key 'pageName'.
Its less difficult than it sounds ;-) In plain English the intension of the rule: if you get a published document show the MyReviewPage page.

The generic format of a rule is:

LHS => RHS
qualifier => assignment [; options]

In the example above the qualifier - a regular EOQualifier object as available in EOControl - is:

document.status = 'saved'

The 'document.status' will be evaluated against the rule context using the regular -valueForKeyPath: KVC method. Of course you can use any EOQualifier you like, eg:

document.priority > 3 AND contact.firstname LIKE '*Dagobert*'

On the right hand side of a rule is the so called 'assignment'. Assignments are pretty similiar in concept to WOAssociation's, more on that further below. In our example:

pageName = 'MyReviewPage'

This just tells the rule engine to return the constant string 'MyReviewPage' if the application requests a value for the 'pageName' key.

The 'other' common assignment is a KVC assignment:

pageName = currentPageName

This form retrieves the value of the 'currentPageName' property from the rule context and returns it for the 'pageName' key when requested by the application.

Ruleset

Rules alone serve no real purpose, it needs a set of rules to do something useful. Only one rule of a set will be evaluated depending on its qualifier and its priority.
Sample rule model (property list representation):

( "context.soRequestType='WebDAV' => renderer = 'SoWebDAVRenderer' ; high",
"context.soRequestType='XML-RPC' => renderer = 'SoXmlRpcRenderer' ; high",
"context.soRequestType='SOAP' => renderer = 'SoSOAPRenderer' ; high",
"context.soRequestType='WCAP' => renderer = 'SoWCAPRenderer' ; high",
"*true* => renderer = 'SoDefaultRenderer' ; fallback" )

This is part of a ruleset from SOPE which selects the renderer class for SoObjects depending on the type of the client. By moving this selection to a rule set we don't need to hardcode the way objects are rendered.
For example if we need a specific renderer for broken IE WebDAV, we could just add a rule like:

context.soRequestType='WebDAV' AND
context.clientCapabilities.isInternetExplorer = YES
=> renderer = 'SoHackWebDAVRenderer' ; high

The rule model also shows the special *true* left hand side and the use of priorities. The *true* LHS always makes the rule run unless a higher priority matches and is used to provide fallback values. This is why *true* should only get used with the fallback priority.
In the case that multiple rules match and have the same priority the rule engine will select the rule which has the most specific qualifier (the qualifier with most subqualifiers).

Rule models notably are not restricted to evaluate to a single value! You can (and will) define as many result keys in your model as you like, eg:

( "context.soRequestType='WebDAV' => renderer = 'SoWebDAVRenderer' ; high",
"context.soRequestType='WebDAV' => debugOn = YES; high", )

In this model two properties are offered for evaluation, 'renderer' and 'debugOn'. If the application asks the rule engine for 'debugOn', it will return YES for WebDAV requests and if it asks for 'renderer' it will return 'SoWebDAVRenderer'. Quite useful for configuring a lot of colors and styles of a webpage depending on the state of an object.

Evaluating Rulesets in Objective-C

Now that we know how a rule model looks like, we should start using the model.
Consider that your rule model is stored in an array default called 'MyRules', in this case you can take advantage of some shortcut method and directly create a working rule context:

NGRuleContext *rules;
// create rule context
rules = [NGRuleContext ruleContextWithModelInUserDefault:@"MyRules"];
// fill in parameters
[rules takeValue:_rq forKey:@"request"];
[rules takeValue:_ctx forKey:@"context"];
// query value
renderClassToBeUsed = [rules valueForKey:@"renderer"];

Thats it. You create a rule context using some rule model, you pass in some objects you want to use in your rule ('context' and 'request'), then you evaluate values of your rule ('renderer'). Quite simple.

Note that rule context is a regular object which can be queried using key/value coding making it perfect for use in NGObjWeb templates:

TableCell: WOGenericContainer {
elementName = "td";
bgcolor = rules.color;
}

This assumes that the component returns a rule context in the 'rules' method. A component setup like this can be easily customized just by changing the rules thus avoiding the requirement to hack component controller code.

Loading Rule Models from a File

If you store the rule model in some separate property list file, you need to create the model and init the context with that:

NGRuleModel *model;
NGRuleContext *rules;
model = [[NGRuleModel alloc] initWithContentsOfFile:@"myrules.plist"];
rules = [NGRuleContext ruleContextWithModel:model];

NGRuleModel contains some other methods to parse rules, eg from in-memory property lists or defaults.

Reusing a Rule Context

You do not need to recreate a rule context for each evaluation. Just ensure that it is properly cleared prior applying new values by calling -reset:

[rules reset];

Calling -reset is also recommended after you are done evaluating all values, so that the parameters passed into the rule are properly released.

Evaluating a Set of Values

Another shortcut method you can use is the evaluation of a rule model for a set of objects.
Eg assume a model like:

( "document.age < 5 => color = 'white'", "document.age > 4 => color = 'green'" )

If you want to get the rule-determined colors of a set of documents with the 'age' property, you can run:

colors = [rules valuesForKeyPath:@"color"
takingSuccessiveValues:ageObjects
forKey:@"document"];

This will walk over the 'ageObjects' array and perform a

[rules takeValue:ageObject forKey:@"document"]

for each object and add the result of

[rules valueForKeyPath:@"color"]

to the result array. This will either return white or green according to the model and is completely extensible for new colors or changes in the logic to apply colors!

Note that 'nil' results (eg no rule did apply) will be replaced with NSNull objects in the result array.

Using Complex Objects

Finally remember that assignment results do not need to be base values, they can also be complex objects.

The following takes to account objects which are supposed to have an 'email', the 'mailObjects' array contains email objects with a 'priority' property. The code will walk over each mail and return the email address of the account who is responsible for the mail (either the boss for high priority stuff or the secretary for regular issues):

[rules takeValue:bossObject forKey:@"boss"];
[rules takeValue:secretary forKey:@"secretary"];
contactEMail = [rules valuesForKeyPath:@"contact.email"];
contactEMails = [rules valuesForKeyPath:@"contact.email"
takingSuccessiveValues:mailObjects
forKey:@"mail"];

with the ruleset:

( "mail.priority = 'high' => contact = boss",
"mail.priority = 'normal' => contact = secretary",
"mail.priority = 'low' => contact = secretary" )

Another speciality with the above ruleset is that it uses NGRuleKeyAssignment assignments, that is, it retrieves the value of the assignment from the rule context (the boss or secretary objects previously set as parameters).

Priorities

NGExtensions predefines a set of symbolic priorities which should be used instead of numeric ones unless you really need very finegrained control.

In case you use numeric values those should be between 50 (low) and 150 (high) so that the priorities are still overridden by very high and important and do not got below fallback.

Predefined priorities:

  • important
  • very high
  • high
  • normal / default
  • low
  • very low
  • fallback

  • NGRule Classes

    The rule system is implemented as a part of the NGExtensions framework which is located inside sope-core and is composed of just five public classes (plus one internal class, the rule parser).

    NGRuleContext

    NGRuleContext is the primary object the user is working with. It is used to configure the rule parameters and it is used to retrieve assignment values. The context is initialized with an NGRuleModel (it also provides a shortcut to setup a context with a rule model stored in the defaults).

    NGRuleModel

    NGRuleModel represents the set of NGRule objects. Besides parsing the rules from files, property lists or user defaults, the model contains the -candidateRulesForKey: method which determines the rules which are required to retrieve a certain RHS value.

    NGRule

    Represents a single rule. A rule is composed of a qualifier (a regular EOQualifier as provided by EOControl) - the LHS, an assignment (an NGRuleAssignment object) - the RHS, and a priority.
    The primary methods are -isCandidateForKey: to determine whether a rule should be evaluated to retrieve a certain assignment key and -fireInContext: which just triggers the assignment and returns the result.

    NGRuleAssignment

    Assignments are the right-hand side of a rule in the rule system, if a rule is selected by qualifier and priority the assignment is the "thing" which is executed.
    The base class also acts as the default assignment which is used for constant evaluation, that is, the assignment value is directly passed out as the result. Eg:

    color = 'green'
    NGRuleKeyAssignment

    In case the assignment value doesn't start with a digit or with a quote, the rule parser creates a key assignment object. The key assignment object looks up the value in the rule context and therefore allows for values which are configured at runtime.

    color = currentColor;

    More Examples

    If you think of some good use cases missing, let me know and I add them to this section!

    Webpage Controlflow

    Another neat application for rules is the selection of the "next page", that is, to control the navigation inside a web application. Consider a ruleset like this:

    ( "document.status = 'saved' => pageName = 'MyReviewPage'",
    "document.status = 'created' => pageName = 'MyReviewPage'",
    "document.status = 'reviewed' => pageName = 'MyPublishPage'" )
    

    and an action method like this:

    - (id)showNextPage {
    return [self pageWithName:[rules valueForKey:@"pageName"]];
    }
    

    or even simpler, without any code at all, just the .wod:

    NextPage: WOHyperlink { pageName = rules.pageName; }

    This code will automatically determine the correct page to be shown depending on the rules and the state of an object. Eg if you later decide that the publish page should also get selected for 'saved' documents which are 'green', just enhance the rule to:

    ( "document.status = 'saved' => pageName = 'MyReviewPage'",
    "document.status = 'created' => pageName = 'MyReviewPage'",
    "document.status = 'reviewed' => pageName = 'MyPublishPage'",
    "document.status = 'saved' AND document.color = 'green' => pageName = 'MyPublishPage'; priority = high",
    )
    

    Note that the priority is not strictly necessary because the rule system will select the most specific rule, that is the "AND" qualifier automagically wins over the simple check for 'saved'.

    OpenGroupware.org

    Unforunately DirectToWeb / rules where not yet available when the OpenGroupware.org WebUI was written, so it currently doesn't use rules at all. But stay tuned for changes in OGo 1.2 ;-)

    Links

  • DirectToWeb
  • BDRuleEngine

  • ... written by Helge Heß