zeroSteiner / rule-engine

A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
https://zerosteiner.github.io/rule-engine/
BSD 3-Clause "New" or "Revised" License
445 stars 54 forks source link

Nested rules #31

Closed wwwslinger closed 2 years ago

wwwslinger commented 2 years ago

I have a rule like the following:

(prior.measurement_1 > 100 or prior.measurment_2 > 25) and (current.measurement_1 > 100 or current.measurement_2 > 25)

I'd like to create a rule for just the measurements and another rule for it matching on prior and current events toghether:

measurement_rule = 'measurement_1 > 100 or measurement_2 > 25'
event_rule = 'measurement_rule.matches(prior) and measurement_rule.matches(current)'
event_rule.matches(prior_and_current_event_object)

It doesn't have to be structured like this, but I'd prefer to keep the whole rule as one, leveraging other rules within, to avoid coding up the logic in Python. Is that possible? I can't find anything like this in your examples.

It's trivial to code, but redefining the same rule (like the measurement_rule above) is something I'm going to have to do for hundreds or more. It would be great to mix and match rules.

zeroSteiner commented 2 years ago

If I understand correctly, you want to apply an expression written once to multiple things that are identical. If that's right, then list comprehension should do the trick for you.

Something like this:

$ PYTHONPATH=$(pwd)/lib python -m rule_engine.debug_repl --edit-console
edit the 'context' and 'thing' objects as necessary
>>> measurements = {'measurement_1': 9999, 'measurement_2': 9999}
>>> thing = {'prior':measurements, 'current':measurements}
>>> exit()
exiting the edit console...
rule > [m for m in [prior, current] if m.measurement_1 > 100 or m.measurement_2 > 25].length > 0
result: 
True
rule > 

In this case, prior and current are both in the top-level namespace. You create an array containing both of them (or even more if you wanted) and then filter them based on the expression that you don't want to repeat. Count the number that were filtered and you should be all set.

wwwslinger commented 2 years ago

Thanks! I like that example. That works well for this case and some others.

In general, though, there is no way to include one rule as part of another? Like this scenario:

rule1 = 'measurement_1 > 100 or measurement_2 > 90'
rule2 = 'measurement_3 < 1000 and measurement_3 > 250'
rule3 = 'measurement_3 == 300 or measurement_1 == 150'

measurements = {'measurement_1': 9999, 'measurement_2': 9999, 'measurement_3': 9999}
testA = rule1.matches(measurements)
...do some things because testA matched...
testB = rule2.matches(measurements)
...do some things because testB matched...
thing = { "r1": testA, "r2": testB, "r3": rule3.matches(measurements), "measurement_2": measurements["measurement_2"] }

rule4 = 'r1 and r2 and r3 and measurement_2 < 100'

testC = rule4.matches(thing)

Would be great if I can use rule1 and rule2 separately, then write rule4 without creating the thing object, more simply as:

rule4 = 'rule1 and rule2 and rule3 and measurement_2 < 100`
testC = rule4.matches(measurements)

(edited for bad rule numbering)

zeroSteiner commented 2 years ago

Not really, no. Each rule is a completely separate object with no reference to any other rules. You'd need to do something in Python to make almost like a macro maybe using format strings, string concatenation or direct AST manipulation.

You could always store the evaluated result from a previous rule but it wouldn't be reevaluated for the current object thats being matched on.