Project

General

Profile

Cruximplement » History » Version 15

Shuvam Misra, 21/09/2023 12:26 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 5 Shuvam Misra
While the fields in the example above are adequate from a purely functional point of view, it may be necessary to have some additional metadata to allow the building of a good UI which will allow users to manage these schema objects. So, an augmented data structure may look like this:
55
56
``` json
57
"patternschema": {
58
    "class": "inventoryitems",
59
    "attr": [{
60
        "name": "cat",
61
        "shortdesc": "Category of item",
62
        "longdesc": "Each item can belong to one of the following categories: textbooks, notebooks, stationery, or reference books.",
63
        "type": "enum",
64
        "vals": [ "textbook", "notebook", "stationery", "refbooks" ],
65
        "enumdesc": [ "Text books", "Notebooks", "Stationery and miscellaneous items", "Reference books, library books" ]
66
    },{
67
        "name": "mrp",
68
        "shortdesc": "Maximum retail price",
69
        "longdesc": "The maximum retail price of the item as declared by the manufacturer."
70
        "type": "float",
71
        "valmax": 20000,
72
        "valmin": 0
73
    },{
74
        "name": "fullname",
75
        "shortdesc": "Full name of item",
76
        "longdesc": "The full human-readable name of the item. Not unique, therefore sometimes confusing.",         
77
        "type": "str",
78
        "lenmax": 40,
79
        "lenmin": 5
80
    },{
81
        "name": "ageinstock",
82
        "shortdesc": "Age in stock, in days",
83
        "longdescr": "The age in days that the oldest sample of this item has been lying in stock",
84
        "type": "int",
85
        "valmax": 1000,
86
        "valmin": 1
87
    },{
88
        "name": "inventoryqty",
89
        "shortdesc": "Number of items in inventory",
90
        "longdescr": "How many of these items are currently present in the inventory",
91
        "type": "int",
92
        "valmax": 10000,
93
        "valmin": 0
94
    }]
95
}
96
```
97
98
Here, the `shortdesc` and `longdesc` are useful attributes of each attribute, for displaying labels and help text in any UI which is displayed to the human user who manages the rules for entities of this class. The `valmax`, `valmin`, `lenmax`, `lenmin`, allow the system to enforce some sanity checks on the patterns defined in any rules for this entity.
99
100
101 2 Shuvam Misra
## Representing the schema of actions
102 1 Shuvam Misra
103
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.
104
* An example of an action word: `invitefordiwali`
105
* An example of an attribute assignment: `discount=7`
106
107
So, the schema of the actions will just specify the valid action names and the attribute names for assignments.
108
109
``` json
110
"actionschema": {
111
    "class": "inventoryitems",
112
    "actions": [ "invitefordiwali", "allowretailsale", "assigntotrash" ],
113
    "attribs": [ "discount", "shipby" ],
114
    "tags": [ "specialvendor", "tryoverseas" ]
115
}
116
```
117
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.
118
119
Putting the `patternschema` and `actionschema` blocks together, a better representation for the full schema for a class of entities will be:
120
121
``` json
122
"ruleschema": {
123
    "class": "inventoryitems",
124
    "patternschema": {
125
        "attr": [{
126
            "name": "cat",
127
            "type": "enum",
128
            "vals": [ "textbook", "notebook", "stationery", "refbooks" ]
129
        },{
130
            "name": "mrp",
131
            "type": "float"
132
        },{
133
            "name": "fullname",
134
            "type": "str",
135
        },{
136
            "name": "ageinstock",
137
            "type": "int"
138
        },{
139
            "name": "inventoryqty",
140
            "type": "int"
141
        }]
142
    }
143
    "actionschema": {
144
        "actions": [ "invitefordiwali", "allowretailsale", "assigntotrash" ],
145
        "attribs": [ "discount", "shipby" ],
146
    }
147
}
148
```
149
150
There will need to be one such `ruleschema` block for each class.
151
152 2 Shuvam Misra
## Representing a pattern
153 1 Shuvam Misra
154 6 Shuvam Misra
Below is an example of a pattern of a rule, which conforms to the schema example given above.
155
156 1 Shuvam Misra
``` json
157
"rulepattern": {
158
    "pattern": [{
159
        "attr": "cat",
160
        "op": "eq",
161
        "val": "textbook"
162
    },{
163
        "attr": "mrp",
164
        "op": "ge",
165
        "val": 2000
166
    },{
167
        "attr": "ageinstock",
168
        "op": "ge",
169
        "val": 90
170
    }]
171
}
172
```
173
174
If a rule has this pattern, it will match any entity which falls in the class `inventoryitems` which
175
* is of type textbook
176
* has MRP (max retail price) greater than INR 2000
177
* has been in stock longer than 90 days 
178
179
For attributes which are of type `int`, `float`, `str` and `ts`, the following comparison operators are available:
180
* Greater than or equal to: `ge`
181
* Greater than: `gt`
182
* Less than or equal to: `le`
183
* Less than: `lt`
184
* Equal to: `eq`
185
* Not equal to: `ne`
186
187
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.
188
189
For enum types, only `eq` and `ne` are available.
190
191 2 Shuvam Misra
## Representing an action
192 1 Shuvam Misra
193
A rule has a set of one or more actions. The following are all examples of the action section of rules:
194
* `invitefordiwali`
195
* `discount=7`
196
* `shipwithoutpo`
197
* `CALL=intlbiz`
198
199
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.*
200
* `reprimand=This cannot go on any longer`
201
202
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.
203
204
The action portion of a rule will have the following structure, shown here as an example:
205
``` json
206
"ruleactions": {
207
    "actions": [ "christmassale", "vipsupport" ],
208
    "attribs": [ "shipby=fedex" ],
209
    "call": "internationalrules",
210
    "return": true,
211
    "exit": false
212
}
213
```
214
This example shows all five attributes of `ruleactions`, but in reality, some of the attributes will typically be missing from most of the rules.
215
216 2 Shuvam Misra
## An entire rule
217 1 Shuvam Misra
218
This is what an entire rule looks like:
219
220
``` json
221
"rule": {
222
    "class": "inventoryitems",
223
    "ver": 4,
224
    "rulepattern": [{
225
        "attr": "cat",
226
        "op": "eq",
227
        "val": "textbook"
228
    },{
229
        "attr": "mrp",
230
        "op": "ge",
231
        "val": 5000
232
    }],
233
    "ruleactions": {
234
        "actions": [ "christmassale" ],
235
        "attribs": [ "shipby=fedex" ]
236
    }
237
}
238
```
239
240
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.
241
242
A rule has a version number, which is incremented whenever the rule is updated. This number is for internal logging and rule engine debugging.
243
244
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:
245
``` json
246
"ruleset": {
247
    "class": "inventoryitems",
248
    "setname": "overseaspo",
249
    "rules": [{
250
        "ver": 4,
251
        "rulepattern": {
252
            :
253
            :
254
        },
255
        "ruleactions": {
256
            :
257
            :
258
        }
259
    }, {
260
        "ver": 3,
261
        "rulepattern": {
262
            :
263
            :
264
        },
265
        "ruleactions": {
266
            :
267
            :
268
        }
269
    }]
270
}
271
```
272
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`.
273
274 2 Shuvam Misra
## The schema manager
275 1 Shuvam Misra
276 7 Shuvam Misra
The schema for each class of entities may be written by hand using a text editor. JSON or YAML files are easy to write. If the schema of one class has less than a dozen attributes, it may be 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.
277 1 Shuvam Misra
278
A schema manager will have the following features:
279
* It will allow the user to create new instances of `ruleschema`
280
* 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
281
  * the addition of additional attributes in a `patternschema` or
282
  * additional attributes, action names or tags in an `actionschema`.
283
* It will ensure that there is no scope for typos when defining the schema.
284
285 3 Shuvam Misra
## The rule manager
286 1 Shuvam Misra
287
The rule manager will allow a user to manage rules. Core functionality:
288
* It will provide a user interface to let the user edit rules.
289
* 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.
290
* It will allow the user to move a rule up or down in the sequence, since ordering is important.
291
* If a rule is being defined with a `CALL` action, then the rule manager will ensure that a ruleset with that target name exists.
292
* 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.
293
* 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.
294
295 2 Shuvam Misra
## The matching engine
296 1 Shuvam Misra
297
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.
298
299 11 Shuvam Misra
The operation of the engine, in a highly simplified notation, is:
300
```
301
for each rule in the ruleset do
302
    match the pattern of the rule with the entity
303
    if the pattern matches, then
304
        collect the actions from the rule into the actionset
305
    endif
306
endfor
307
```
308 1 Shuvam Misra
309 2 Shuvam Misra
### Matching one rule's pattern
310 1 Shuvam Misra
311 12 Shuvam Misra
The algorithm for the matching of one rule's pattern is shown below. Here, it is assumed that the object being matched is in `entity` and pattern of the rule being matched is in `rulepattern`.
312 1 Shuvam Misra
```
313
func matchOnePattern()
314
    input parameters: entity, rulepattern
315
    returns patternmatch: boolean
316
317
for patternterm in rulepattern do
318
    for entityoneterm in entity.attrs do
319
        if entityoneterm.attr == patternterm.attr then
320
            entitytermval = entityoneterm.val
321
        endif
322
    endfor
323
    case patternterm.op in
324
    "eq":
325
        if entitytermval != patternterm.val then
326
            return false
327
        endif
328
    "ne":
329
        if entitytermval == patternterm.val then
330
            return false
331
        endif
332
    endcase
333
    if patternterm.type in [ "int", "float", "ts", "str" ] then
334
        case patternterm.op in
335
        "le":
336
            if entitytermval > patternterm.val then
337
                return false
338
            endif
339
        "lt":
340
            if entitytermval >= patternterm.val then
341
                return false
342
            endif
343
        "ge":
344
            if entitytermval < patternterm.val then
345
                return false
346
            endif
347
        "gt":
348
            if entitytermval <= patternterm.val then
349
                return false
350
            endif
351
        default:
352
            log error with priority = CRITICAL: "system inconsistency with BRE rule terms"
353
        endcase
354
    endif
355
endfor
356
357
return true
358
```
359
360 2 Shuvam Misra
### Collecting the actions from one rule
361 1 Shuvam Misra
362
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:
363
``` json
364
"actionset": {
365
    "actions": [ "dodiscount", "yearendsale" ],
366
    "attribs": [ "shipby=fedex" ],
367
    "call": "overseaspo",
368
    "return": true,
369
    "exit": false
370
}
371
```
372 8 Shuvam Misra
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 (signifying that no ruleset needs to be called after this rule returns) or will carry the name of one ruleset to call after the current rule. The `return` and `exit` attributes will carry boolean values.
373 1 Shuvam Misra
374
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.
375
376
```
377
function collectActions()
378
input parameters: actionset, ruleactions
379
    returns actionset
380
381
actionset.actions = actionset.actions UNION ruleactions.actions
382
actionset.attribs = actionset.attribs UNION ruleactions.attribs
383
384
actionset.call = ""
385
actionset.return = false
386
actionset.exit = false
387
if ruleactions.call is defined, then
388
    actionset.call = ruleactions.call
389
endif
390
if ruleactions.return is defined, then
391
    actionset.return = true
392
endif
393
if ruleactions.exit is defined,  then
394
    actionset.exit = true
395
endif
396 14 Shuvam Misra
397
return actionset
398 1 Shuvam Misra
```
399
400
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.
401
402 13 Shuvam Misra
### Representing one entity
403
404 1 Shuvam Misra
The matching engine matches all the rules of a ruleset against one instance of a class, like one instance of `vendor` or `inventoryitem`. How do we represent this object instance, when the type and the fields are all dynamically determined at runtime and varies from invocation to invocation? Here is one example:
405 14 Shuvam Misra
``` json
406 1 Shuvam Misra
"inputentity": {
407
    "class": "inventoryitems",
408
    "attribs": [{
409 14 Shuvam Misra
        "name": "cat",
410
        "val": "refbook"
411
    },{
412
        "name": "mrp",
413
        "val": "1350"
414
    },{
415
        "name": "fullname",
416
        "val": "Advanced Level Physics, 2/ed"
417
    },{
418
        "name": "ageinstock",
419
        "val": "20"
420
    },{
421
        "name": "inventoryqty",
422
        "val": "540"
423
    }]
424 1 Shuvam Misra
}
425
```
426 14 Shuvam Misra
As this example highlights, all the values are supplied of type string, so that they may be converted from strings to their respective types later. This allows the data structure for specifying an object instance to be strongly typed and still allow attributes of all types to be captured. One more point illustrated is that **all attributes in the `patternschema` of the class must be present** in each object instance of that class.
427 1 Shuvam Misra
428 14 Shuvam Misra
This is the way the entity will be submitted to the matching engine for processing.
429
430 1 Shuvam Misra
### The overall matching engine
431 13 Shuvam Misra
432 1 Shuvam Misra
This engine will go through rules one after another, and for each rule, it will call `matchOnePattern()`. If `matchOnePattern()` returns `true`, it will call `collectActions()`. And then it will inspect the result obtained from `collectActions()` and decide what to do next.
433 13 Shuvam Misra
434 14 Shuvam Misra
This engine will be implemented by the `getRules()` function, which will occasionally call itself recursively. It will be called with three parameters:
435
* an `inputentity`, which will be matched against the ruleset
436
* a `ruleset` which will be traversed by the engine
437
* an `actionset`, which collects the result of the action matching
438 1 Shuvam Misra
439 14 Shuvam Misra
The pseudocode has been written with the assumption that parameters are all pass-by-value.
440 1 Shuvam Misra
441
So, the `doMatch()` engine will work in the following way:
442
```
443
function doMatch()
444 14 Shuvam Misra
input parameters: inputentity, ruleset, actionset
445 1 Shuvam Misra
    returns actionset
446
447 14 Shuvam Misra
for each onerule in ruleset do
448
    if matchOnePattern(inputentity, onerule.pattern) == true then
449
        actionset = collectActions(actionset, onerule.actions)
450
        #
451
        # now check if the actions just collected includes an EXIT clause
452
        #
453
        if actionset.exit == true then
454
            return actionset
455
        endif
456
        #
457
        # If there was no EXIT clause, check if there was a RETURN clause
458
        #
459
        if actionset.return == true then
460
            actionset.return = false
461
            return actionset
462
        endif
463
        #
464
        # If there was no EXIT or RETURN, check if there was a CALL clause
465
        #
466
        if actionset.call is not null then
467
            settocall = actionset.call
468 15 Shuvam Misra
            if settocall.class != inputentity.class then
469
                log error with priority = CRITICAL:
470
                       "system inconsistency with BRE rule terms, attempting to call ", settocall, " from ", ruleset, "!"
471
            endif
472 14 Shuvam Misra
            actionset.call = null
473
            doMatch(inputentity, settocall, actionset)
474
            #
475
            # If the called ruleset has set EXIT to true, then we too need to
476
            # exit, and our caller too needs to exit, ad infinitum
477
            #
478
            if actionset.exit == true then
479
                return actionset
480
            endif
481
        endif
482
    endif
483
    #
484
    # We come here because we've done one rule and we've neither been thrown
485
    # out by an EXIT nor a RETURN clause. So we now loop to the next rule in
486
    # our ruleset.
487
    #
488
endfor
489 13 Shuvam Misra
490 14 Shuvam Misra
return actionset
491 10 Shuvam Misra
```
492 14 Shuvam Misra
493
This matching engine will be able to handle traverse all rulesets
494 1 Shuvam Misra
495
### API for the matching engine
496
497
The matching engine must support the following set of operations:
498
* `doMatch()`: take an entity, pass it through all relevant rules and rulesets, and respond with the set of final results.
499
* `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()`.