🎉 JSON parser from scratch chapter is now out!
Rule engine
A Simple Rule

A simple rule

Let's first visualize a basic rule.

facts = {'age': 19}
rule = Rule('age', 'gt', 18)
print(rule.run(facts)) # true 

To bring this to life, let's start by defining the Rule structure. Which takes in fact_key, operator & value. It'll have a run function which will just run the mathematical operation based on the operator.

@dataclass
class Rule:
    fact_key: str | int
    operator: str
    value: str | int
 
    def run(self, facts):
        if self.operator == "gt":
            return facts.get(self.fact_key) > self.value

We'll add more operators going foreard. Now with this, running the rule would be simply running

rule = Rule("age", "gt", 18)
print(rule.run(facts={"age": 19})) # true
print(rule.run(facts={"age": 18})) # false

Now to the next step, what does it take to build a AndRule?

Building an AndRule

An AndRule takes in a list of rules, runs all of those Rules and returns the result.

@dataclass
class AndRule:
    rules: list[Rule]
 
    def run(self, facts):
        return all([r.run(facts=facts) for r in self.rules])

now with this

rule = AndRule([Rule("age", "gt", 18), Rule("country", "eq", "IN")])
print(rule.run(facts={"age": 19, "country": "IN"}))  # true
print(rule.run(facts={"age": 18, "country": "RU"}))  # false

Similarly, an OrRule can also work. Just that instead of using all, we'll be using any.

This marks a checkpoint, where the rule engine actually is entirely functional!

Now the question is about context, we know the result of the rule. But how do we understand why?

We'll need to propagate context upwards.