durable_rules Documentation

repository·master·Indexed 23 days ago

https://github.com/jruizgit/rules

A polyglot micro-framework for real-time, consistent, and scalable coordination of events using a Rete-based forward-chaining rules engine. It supports Node.js, Python, and Ruby, providing tools for defining rulesets with antecedents and consequents, managing context state, and implementing complex logic via Statecharts and Flowcharts. Features include forward inference to derive new facts, high-performance string pattern matching via DFA, and support for both ephemeral events and persistent facts.

Tokens
24K
Snippets
72
Records
89
Agent score
79%

What's inside durable_rules

  1. Understand Fact and Event Identity

    master

    The framework distinguishes between facts and events based on how they are identified:

    • Facts: Identified by their property names and values. If you attempt to assert a fact that is identical to one already present, it will raise a MessageObservedError.
    • Events: Identified by their posting time. Even if two events have identical properties, they are considered different because they are posted at different times. Durable.post will not raise an error for duplicate event properties.
  2. How rules work: Antecedents and Consequents

    master

    In durable_rules, a rule is defined by two main components:

    1. Antecedent: The pattern of events or facts that must match for the rule to trigger (defined using @when_all).
    2. Consequent: The action to take when the antecedent is matched (the function body).

    You define rulesets using a with ruleset('name'): block. To trigger a rule, you use post('name', data) to send an event or assert_fact('name', data) to add a fact to the knowledge base.

    from durable.lang import *
    
    with ruleset('test'):
            # antecedent
            @when_all(m.subject == 'World')
            def say_hello(c):
                # consequent
                print ('Hello {0}'.format(c.m.subject))
    
    post('test', { 'subject': 'World' })
  3. Perform pattern matching with `.matches()`

    master

    The durable_rules engine implements a pattern matching dialect that compiles expressions into a deterministic state machine. This ensures $O(n)$ event processing complexity (where $n$ is the size of the event) and avoids backtracking.

    Repetition Operators:

    • +: 1 or more repetitions
    • *: 0 or more repetitions
    • ?: optional (0 or 1 occurrence)

    Special Characters:

    • (): group
    • |: disjunct
    • []: range
    • {}: repeat

    Character Classes:

    • .: all characters
    • %a: letters
    • %c: control characters
    • %d: digits
    • %l: lower case letters
    • %p: punctuation characters
    • %s: space characters
    • %u: upper case letters
    • %w: alphanumeric characters
    • %x: hexadecimal digits

    Note: Use % to escape special characters.

    from durable.lang import *
    
    with ruleset('match'):
        @when_all(m.url.matches('(https?://)?([0-9a-z.-]+)%.[a-z]{2,6}(/[A-z0-9_.-]+/?)*'))
        def approved(c):
            print ('match-> url {0}'.format(c.m.url))
    
    def match_complete_callback(e, state):
        print('match -> expected {0}'.format(e.message))
    
    post('match', { 'url': 'https://github.com' })
    post('match', { 'url': 'http://github.com/jruizgit/rul!es' }, match_complete_callback)
  4. Use Identity in Rules

    master

    Identity allows rules to react to the existence or absence of specific attributes within the context using the $ex operator.

    • $ex: Checks for the existence of a key.
    • m$not: Negates the existence check (e.g., m$not with $ex checks if a key does NOT exist).
    {
      "bookstore": {
        "r_0": {
          "all": [
            {
              "m": {
                "$ex": {
                  "status": 1
                }
              }
            }
          ],
          "run": "log"
        },
        "r_1": {
          "all": [
            {
              "m": {
                "$ex": {
                  "name": 1
                }
              }
            }
          ],
          "run": "retract"
        },
        "r_2": {
          "all": [
            {
              "m$not": {
                "$ex": {
                  "name": 1
                }
              }
            }
          ],
          "run": "log"
        }
      }
    }
  5. Define Rules using all, any, and run

    master

    Rules in durable_rules are defined using two main components: the antecedent (the condition) and the consequent (the action).

    • all and any are used to label the antecedent definition. all requires all conditions to be met, while any requires at least one.
    • run labels the consequent definition, specifying what happens when the antecedent is satisfied.
    {
      "test": {
        "r_0": {
          "all": [
            {
              "m": {
                "subject": "World"
              }
            }
          ],
          "run": "approved"
        }
      }
    }
  6. Evaluate State Changes in Rules

    master

    Rules can evaluate changes in the context state. By convention, context state change events include the attribute "$s": 1. This allows you to trigger logic specifically when a state transition occurs.

    {
      "flow": {
        "r_0": {
          "all": [
            {
              "m": {
                "$and": [
                  {
                    "state": "start"
                  },
                  {
                    "$s": 1
                  }
                ]
              }
            }
          ],
          "run": "next"
        }
      }
    }
  7. Choose between 'all' and 'any' sequence types

    master

    When defining event sequences, use these labels to control triggering logic:

    • "all": A set of event or fact patterns. All of them must match to trigger the action.
    • "any": A set of event or fact patterns. Any one match will trigger the action.
    {
      "expense": {
        "r_0": {
          "any": [
            {
              "m_0$all": [
                { "first": { "subject": "approve" } },
                { "second": { "amount": 1000 } }
              ]
            },
            {
              "m_1$all": [
                { "third": { "subject": "jumbo" } },
                { "fourth": { "amount": 10000 } }
              ]
            }
          ],
          "run": "log"
        }
      }
    }
  8. Use Events for ephemeral data

    master

    Events are ephemeral facts that are retracted immediately before executing a consequent. This means an event can only be observed once. Events are useful for triggering logic based on transient occurrences without polluting the long-term knowledge base with facts.

    Use Durable.post to dispatch an event.

    require "durable"
    
    Durable.ruleset :risk do
      when_all c.first = m.t == "purchase",
               c.second = m.location != first.location do
        puts "fraud detected -> #{first.location}, #{second.location}"
      end
    end
    
    Durable.post :risk, { :t => "purchase", :location => "US" }
    Durable.post :risk, { :t => "purchase", :location => "CA" }
  9. Organize rules using Flowcharts

    master

    A Flowchart organizes rules where each stage represents an action to be executed. Unlike statecharts (which track state), a flowchart results in a transition to another stage upon execution.

    Key Concepts:

    • Stages: A flowchart consists of one or more stages. An initial stage is a vertex without incoming edges.
    • Actions: A stage can have an action (run).
    • Conditions: A stage can have zero or more conditions. Each condition has a rule and a destination stage.
    • Reflexive Conditions: Using self as a condition name causes the engine to return to the same stage if the condition is met.

    Use d.flowchart(name, callback) to define the structure and d.post(name, payload) to drive the flow.

    var d = require('durable');
    
    d.flowchart('expense', function() {
        input: {
            request: m.subject == 'approve' && m.amount <= 1000 
            deny:  m.subject == 'approve' && m.amount > 1000
        }
    
        request: {
            run: console.log('Requesting approve')
            approve: m.subject == 'approved'
            deny: m.subject == 'denied'
            self: m.subject == 'retry'
        }
    
        approve: {
            run: console.log('Expense approved')
        }
    
        deny: {
            run: console.log('Expense denied')
        }
    });
    
    d.post('expense', { subject: 'approve', amount: 100 });
  10. Evaluate correlated sequences of events

    master

    Rules can evaluate sequences of correlated events or facts using the whenAll label.

    Key Concepts:

    • Assignment: Use the = operator within a whenAll block to name an event or fact (e.g., first = m.amount > 10). This name can then be used in subsequent expressions.
    • Arithmetic: You can use +, -, *, and / to perform calculations involving assigned events.
    • Distinctness: By default, correlated sequences capture distinct messages. If you want a single event to satisfy multiple conditions in a sequence, set the distinct: false attribute (Note: the documentation example shows // distinct: true as a comment, implying the default behavior is distinct).

    Reference: Assigned events/facts allow access to all their properties (e.g., first.amount).

    var d = require('durable');
    
    d.ruleset('risk', function() {
        whenAll: {
            first = m.amount > 10
            second = m.amount > first.amount * 2
            third = m.amount > (first.amount + second.amount) / 2
        }
        run: {
           	console.log('fraud detected -> ' + first.amount);
            console.log('               -> ' + second.amount);
            console.log('               -> ' + third.amount);
        }
    });
    
    d.post('risk', { amount: 50 });
    d.post('risk', { amount: 200 });
    d.post('risk', { amount: 251 });
  11. Use Facts to define a knowledge base

    master

    Facts are JSON objects asserted into the system that define the knowledge base. They are stored until they are explicitly retracted. When a fact satisfies a rule antecedent, the rule's consequent is triggered. You can use assert to add facts and retract to remove them.

    In rules, you can capture data from facts using assignment within the antecedent (e.g., c.first = ...) to be used later in the consequent.

    require "durable"
    
    Durable.ruleset :animal do
      # Captures data into 'first' to be used in the consequent
      when_all c.first = (m.predicate == "eats") & (m.object == "flies") do
        assert :subject => first.subject, :predicate => "is", :object => "frog"
      end
    
      when_all +m.subject do
        puts "fact: #{m.subject} #{m.predicate} #{m.object}"
      end
    end
    
    Durable.assert :animal, { :subject => "Kermit", :predicate => "eats", :object => "flies" }
  12. Organize logic using Statecharts

    master

    A Statechart is a Deterministic Finite Automaton (DFA) used to organize rules into states.

    Key Concepts:

    • States: A statechart has one or more states. It requires an initial state (a vertex with no incoming edges).
    • Triggers: A trigger moves the context from one state to another. A trigger can be associated with a rule (the condition for transition) and an action (executed before the state change).
    • Nested States: States can contain sub-states. If a sub-state does not handle an event, the event is automatically bubbled up to the super-state.
    • Instance IDs: You can target specific statechart instances using the :sid key in your events.
    require "durable"
    
    Durable.statechart :expense do
      state :input do
        to :denied, when_all((m.subject == "approve") & (m.amount > 1000)) do
          puts "denied amount #{m.amount}"
        end
    
        to :pending, when_all((m.subject == "approve") & (m.amount <= 1000)) do
          puts "requesting approve amount #{m.amount}"
        end
      end  
    
      state :pending do
        to :approved, when_all(m.subject == "approved") do
          puts "expense approved"
        end
      end
    
      state :approved
      state :denied
    end
    
    # Target default instance
    Durable.post :expense, { :subject => 'approve', :amount => 100 }
    # Target specific instance ID 1
    Durable.post :expense, { :sid => 1, :subject => 'denied' }