Project

General

Profile

Cruximplement » History » Version 4

Shuvam Misra, 15/09/2023 10:35 AM

1 4 Shuvam Misra
*(For a conceptual overview and design of Crux, see [[cruxdesign|this page]] if you haven't already.)*
2
3 1 Shuvam Misra
# Implementation of Remiges Crux
4
5
{{>toc}}
6
7
A rules engine implementation must include the following:
8
* **RULE SCHEMA**. A notation to specify the list of valid terms in a rule. This list will be separate for each class of entities. For instance, for items in inventory, the list of attributes may be:
9
  * Price
10
  * Full name
11
  * Age in stock
12
  * Quantity in inventory
13
14
    For vendors, the list could include:
15
  * Amount outstanding
16
  * Total value of business done  in the last financial year
17
18
* **RULE NOTATION**. A notation to specify the pattern and actions of a rule.
19
* **THE MATCHING ENGINE**. Something which will take an entity with all its attributes, apply each rule to it, and follow the trail of rules to come up with a list of actions which will emerge.
20
21
So, if these three can be designed and then implemented, the core of a rules engine or a flow engine can be built.
22
23 2 Shuvam Misra
## Representing the schema of patterns
24 1 Shuvam Misra
25
If using JSON, the schema of all valid patterns may be represented in structures of this form:
26
27
``` json
28
"patternschema": {
29
    "class": "inventoryitems",
30
    "attr": [{
31
        "name": "cat",
32
        "type": "enum",
33
        "vals": [ "textbook", "notebook", "stationery", "refbooks" ]
34
    },{
35
        "name": "mrp",
36
        "type": "float"
37
    },{
38
        "name": "fullname",
39
        "type": "str",
40
    },{
41
        "name": "ageinstock",
42
        "type": "int"
43
    },{
44
        "name": "inventoryqty",
45
        "type": "int"
46
    }]
47
}
48
```
49
50
In this example, the object `patternschema` is the schema for one category of entities. This schema says that for rules which work on entities of type `inventoryitems`, there are five attributes available which may be used to make patterns. Each attribute has a type. Enum types, integers, floating point numbers, timestamps (`ts`) and strings are supported. The example above does not have any attribute of type `ts`.
51
52
So, the full schema of the rules engine will be an array of `patternschema` blocks. Initial examples have discussed inventory items and vendors. The `patternschema` block above is for inventory items. If the schema of patterns for vendors needed to be specified, there would be a second `patternschema` with `“class”: “vendors”`
53
54 2 Shuvam Misra
## Representing the schema of actions
55 1 Shuvam Misra
56
The schema of the action section of rules is simpler than patterns. Each rule's action section will contain a set of zero or more words, each denoting an action, and zero or more attribute assignments. There is no need for any type specification, etc.
57
* An example of an action word: `invitefordiwali`
58
* An example of an attribute assignment: `discount=7`
59
60
So, the schema of the actions will just specify the valid action names and the attribute names for assignments.
61
62
``` json
63
"actionschema": {
64
    "class": "inventoryitems",
65
    "actions": [ "invitefordiwali", "allowretailsale", "assigntotrash" ],
66
    "attribs": [ "discount", "shipby" ],
67
    "tags": [ "specialvendor", "tryoverseas" ]
68
}
69
```
70
The schema of actions above indicates that there are three actions, any or all of which may be present in any rule for this class of entities. There are two attributes which may be assigned values by any rule. And there are two tags for this class of entities – if a rule wishes to tag an entity with one or both of these tags, it may do so.
71
72
Putting the `patternschema` and `actionschema` blocks together, a better representation for the full schema for a class of entities will be:
73
74
``` json
75
"ruleschema": {
76
    "class": "inventoryitems",
77
    "patternschema": {
78
        "attr": [{
79
            "name": "cat",
80
            "type": "enum",
81
            "vals": [ "textbook", "notebook", "stationery", "refbooks" ]
82
        },{
83
            "name": "mrp",
84
            "type": "float"
85
        },{
86
            "name": "fullname",
87
            "type": "str",
88
        },{
89
            "name": "ageinstock",
90
            "type": "int"
91
        },{
92
            "name": "inventoryqty",
93
            "type": "int"
94
        }]
95
    }
96
    "actionschema": {
97
        "actions": [ "invitefordiwali", "allowretailsale", "assigntotrash" ],
98
        "attribs": [ "discount", "shipby" ],
99
    }
100
}
101
```
102
103
There will need to be one such `ruleschema` block for each class.
104
105 2 Shuvam Misra
## Representing a pattern
106 1 Shuvam Misra
107
``` json
108
"rulepattern": {
109
    "pattern": [{
110
        "attr": "cat",
111
        "op": "eq",
112
        "val": "textbook"
113
    },{
114
        "attr": "mrp",
115
        "op": "ge",
116
        "val": 2000
117
    },{
118
        "attr": "ageinstock",
119
        "op": "ge",
120
        "val": 90
121
    }]
122
}
123
```
124
125
If a rule has this pattern, it will match any entity which falls in the class `inventoryitems` which
126
* is of type textbook
127
* has MRP (max retail price) greater than INR 2000
128
* has been in stock longer than 90 days 
129
130
For attributes which are of type `int`, `float`, `str` and `ts`, the following comparison operators are available:
131
* Greater than or equal to: `ge`
132
* Greater than: `gt`
133
* Less than or equal to: `le`
134
* Less than: `lt`
135
* Equal to: `eq`
136
* Not equal to: `ne`
137
138
Collation sequences for strings are system dependent, and will need to be standardised so that they work reliably across programming languages and Unicode strings in any language. That's an implementation issue.
139
140
For enum types, only `eq` and `ne` are available.
141
142 2 Shuvam Misra
## Representing an action
143 1 Shuvam Misra
144
A rule has a set of one or more actions. The following are all examples of the action section of rules:
145
* `invitefordiwali`
146
* `discount=7`
147
* `shipwithoutpo`
148
* `CALL=intlbiz`
149
150
The terms which identify actions, *e.g.* `invitefordiwali`, will automatically be converted to lower-case and stored in the system. Reserved attribute names like `CALL`, `RETURN`, `EXIT`, will always be in uppercase. For an attribute assignment, the value of the attribute will be everything after the first `=` character till the end of the string, thus supporting multi-word values, *e.g.*
151
* `reprimand=This cannot go on any longer`
152
153
The action portion of a rule can have zero or one occurrence of a `CALL` term, a `RETURN` term, and an `EXIT` term. If it contains both a `RETURN` and an `EXIT`, then the `RETURN` will be ignored.
154
155
The action portion of a rule will have the following structure, shown here as an example:
156
``` json
157
"ruleactions": {
158
    "actions": [ "christmassale", "vipsupport" ],
159
    "attribs": [ "shipby=fedex" ],
160
    "call": "internationalrules",
161
    "return": true,
162
    "exit": false
163
}
164
```
165
This example shows all five attributes of `ruleactions`, but in reality, some of the attributes will typically be missing from most of the rules.
166
167 2 Shuvam Misra
## An entire rule
168 1 Shuvam Misra
169
This is what an entire rule looks like:
170
171
``` json
172
"rule": {
173
    "class": "inventoryitems",
174
    "ver": 4,
175
    "rulepattern": [{
176
        "attr": "cat",
177
        "op": "eq",
178
        "val": "textbook"
179
    },{
180
        "attr": "mrp",
181
        "op": "ge",
182
        "val": 5000
183
    }],
184
    "ruleactions": {
185
        "actions": [ "christmassale" ],
186
        "attribs": [ "shipby=fedex" ]
187
    }
188
}
189
```
190
191
This structure represents one rule. The rule applies to entities of class `inventoryitems`. It has a pattern section which tries to match two attributes and an action section which throws up one action and one assignment.
192
193
A rule has a version number, which is incremented whenever the rule is updated. This number is for internal logging and rule engine debugging.
194
195
An array of such structures is a set of rules, and will be traversed in the order in which the rules appear in the array. Named rulesets will be represented thus:
196
``` json
197
"ruleset": {
198
    "class": "inventoryitems",
199
    "setname": "overseaspo",
200
    "rules": [{
201
        "ver": 4,
202
        "rulepattern": {
203
            :
204
            :
205
        },
206
        "ruleactions": {
207
            :
208
            :
209
        }
210
    }, {
211
        "ver": 3,
212
        "rulepattern": {
213
            :
214
            :
215
        },
216
        "ruleactions": {
217
            :
218
            :
219
        }
220
    }]
221
}
222
```
223
The example above shows a ruleset named `overseaspo` for class `inventoryitems` which has two rules. This ruleset may be invoked from any other rule with the action `CALL=overseaspo`.
224
225 2 Shuvam Misra
## The schema manager
226 1 Shuvam Misra
227
The schema for each class of entities may be written by hand using a text editor. JSON or YAML files are easy to write. It is unlikely that the schema of one class will have more than a dozen attributes, which makes the schema short enough to edit or audit by hand. However, a tool to manage and maintain the schema eliminates typos and enforces various types of consistency, and a second-level implementation of a schema manager may also enforce authorisation policies.
228
229
A schema manager will have the following features:
230
* It will allow the user to create new instances of `ruleschema`
231
* It will sharply restrict editing of, and prevent deletion of any `patternschema` block or `actionschema` block if there are rules defined in the rules engine for this class of entities. In other words, schema are editable only as long as there are no rules for the class. The only kind of editing it will permit for “live” schema are
232
  * the addition of additional attributes in a `patternschema` or
233
  * additional attributes, action names or tags in an `actionschema`.
234
* It will ensure that there is no scope for typos when defining the schema.
235
236 3 Shuvam Misra
## The rule manager
237 1 Shuvam Misra
238
The rule manager will allow a user to manage rules. Core functionality:
239
* It will provide a user interface to let the user edit rules.
240
* It will check each rule against the schema for the class, and will not give the user the opportunity to define any rule inconsistent with the schema.
241
* It will allow the user to move a rule up or down in the sequence, since ordering is important.
242
* If a rule is being defined with a `CALL` action, then the rule manager will ensure that a ruleset with that target name exists.
243
* Most important: it will provide a testing facility by which sample entities may be submitted to the rule engine for testing, and the rule manager will display a full trace showing which rules were attempted to match, which rules actually matched, and how the result set of actions, attributes, *etc* grew with each step. This feature will be provided without having to save the rule changes.
244
* Finally, when the editing session is complete and all rulesets need to be saved, it will perform a detailed cross-validation of all rules across each other to ensure consistency. If there is any inconsistency, it will give readable explanations of the problems and not permit saving of the updates.
245
246 2 Shuvam Misra
## The matching engine
247 1 Shuvam Misra
248
The matching engine has a one-line job. It will take a full set of attributes of one entity, apply all the rules which apply to its class, and return with the list of actions, attributes, *etc* from all the matching rules.
249
250
The operation of the engine is best understood if it is broken down into the units of its work.
251
252 2 Shuvam Misra
### Matching one rule's pattern
253 1 Shuvam Misra
254
The algorithm for the matching of one rule's pattern will be as shown below. Here, it is assumed that the object being matched is in `entity` and pattern of the rule being matched is in `rulepattern`.
255
```
256
func matchOnePattern()
257
    input parameters: entity, rulepattern
258
    returns patternmatch: boolean
259
260
for patternterm in rulepattern do
261
    for entityoneterm in entity.attrs do
262
        if entityoneterm.attr == patternterm.attr then
263
            entitytermval = entityoneterm.val
264
        endif
265
    endfor
266
    case patternterm.op in
267
    "eq":
268
        if entitytermval != patternterm.val then
269
            return false
270
        endif
271
    "ne":
272
        if entitytermval == patternterm.val then
273
            return false
274
        endif
275
    endcase
276
    if patternterm.type in [ "int", "float", "ts", "str" ] then
277
        case patternterm.op in
278
        "le":
279
            if entitytermval > patternterm.val then
280
                return false
281
            endif
282
        "lt":
283
            if entitytermval >= patternterm.val then
284
                return false
285
            endif
286
        "ge":
287
            if entitytermval < patternterm.val then
288
                return false
289
            endif
290
        "gt":
291
            if entitytermval <= patternterm.val then
292
                return false
293
            endif
294
        default:
295
            log error with priority = CRITICAL: "system inconsistency with BRE rule terms"
296
        endcase
297
    endif
298
endfor
299
300
return true
301
```
302
303 2 Shuvam Misra
### Collecting the actions from one rule
304 1 Shuvam Misra
305
If the pattern for one rule matches the entity being processed, then the actions of that rule will need to be added to the result set for that entity. Here we assume that the result of the action-collection function will return an object of the following structure. This object will be passed as input to the action-collecting function, and a (possibly extended) object will be returned, after merging the input object with the action terms from the rule just matched. The object structure will be:
306
``` json
307
"actionset": {
308
    "actions": [ "dodiscount", "yearendsale" ],
309
    "attribs": [ "shipby=fedex" ],
310
    "call": "overseaspo",
311
    "return": true,
312
    "exit": false
313
}
314
```
315
These five attributes will always be present in the object. The `actions` and `attribs` attributes will carry an array of strings, which will be a union set of all the action terms and attribute assignments collected from rules matched so far. The `call` attribute will either be a zero-length string or will carry the name of one ruleset to call after the current rule. The `return` and `exit` attributes will carry boolean values.
316
317
Performing a set union of action names is straightforward. Performing a set union of attribute assignments requires choosing one value of an attribute, if there was already the same attribute in the `actionset` and the current rule's actions also assigns a value to that attribute. In that case, the old value of the attribute will be overwritten by the new value.
318
319
```
320
function collectActions()
321
input parameters: actionset, ruleactions
322
    returns actionset
323
324
actionset.actions = actionset.actions UNION ruleactions.actions
325
actionset.attribs = actionset.attribs UNION ruleactions.attribs
326
327
actionset.call = ""
328
actionset.return = false
329
actionset.exit = false
330
if ruleactions.call is defined, then
331
    actionset.call = ruleactions.call
332
endif
333
if ruleactions.return is defined, then
334
    actionset.return = true
335
endif
336
if ruleactions.exit is defined,  then
337
    actionset.exit = true
338
endif
339
```
340
341
The matching engine needs to look at what has emerged from `collectActions()` and then take action. The flow of the matching engine will change based on the values of the `call`, `return` and `exit` attributes.
342
343 2 Shuvam Misra
### The overall matching engine
344 1 Shuvam Misra
345
This engine will go through rules one after another, and for each rule, it will call `matchOnePattern()` followed by `collectActions()` if the pattern matches. And then it will inspect the result obtained from `collectActions()` and decide what to do next.
346
347
This engine will be implemented by the `getRules()` function, which will be a recursive function. It will be called with two parameters:
348
* an entity, for which the rules need to be pulled out
349
* a `depth` parameter, which is an integer, always called with the value zero when called by the framework. Internally, the `getRules()` function will use this parameter to track how deep its recursive calls are at any particular point, to distinguish between `RETURN` and `EXIT`.
350
351
### API for the matching engine
352
353
The matching engine must support the following set of operations:
354
* `doMatch()`: take an entity, pass it through all relevant rules and rulesets, and respond with the set of final results.
355
* `getAttrSet()`: take a class name, pull out from the `patternschema` all the attributes listed against that class, with full details. This is useful to let the caller know what attributes are to be specified when calling `doMatch()`.