rule-engine

repository·master·Indexed 20 days ago

https://github.com/zerosteiner/rule-engine

A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects. It provides a safe alternative to exec or eval for evaluating rules from untrusted sources, featuring support for nested data access, builtin attributes for various data types, and user-defined schemas via the OBJECT data type. The library includes tools for deriving schemas from Python dataclasses and SQLAlchemy ORM mapped classes, as well as a debug REPL for testing expressions.

Tokens
17.7K
Snippets
58
Records
91
Agent score
70%

What's inside rule-engine

  1. What is the Rule Engine and how does it work?

    master

    The Rule Engine is a tool for filtering arbitrary Python objects using string expressions written in a custom language. The syntax is similar to Python but borrows features from Ruby.

    Crucially, it does not use Python's exec or eval functions. Instead, it uses a custom parser to evaluate expressions, making it safe and secure for evaluating rules provided by untrusted sources (like end users).

  2. Perform time-based arithmetic with TIMEDELTA

    master

    The TIMEDELTA datatype (introduced in v3.5.0) is backed by Python's datetime.timedelta. It allows for calculating durations and time differences.

    Supported mathematical operations include:

    • datetime + timedelta $\rightarrow$ datetime
    • datetime - timedelta $\rightarrow$ datetime
    • datetime - datetime $\rightarrow$ timedelta
    • timedelta + timedelta $\rightarrow$ timedelta
    • timedelta - timedelta $\rightarrow$ timedelta
  3. Access nested data using the attribute operator

    master

    You can use the attribute operator (.) to recursively resolve values from compound Python data types, such as objects or dictionaries. This allows rules to evaluate members and sub-members of the data being processed. If the resolver fails to find a member via the data type, the engine will then check if the name matches a builtin attribute.

    # Example concept of attribute access
    # If evaluating an object 'user':
    # 'user.name' resolves the 'name' attribute of 'user'
    # 'user.address.city' resolves 'city' from 'user.address'
  4. Handle recursion and forward references in OBJECT types

    master

    When defining complex schemas, you may need to handle recursive or mutually-recursive types:

    1. Self-references: Use DataType.OBJECT.self as a shorthand sentinel to refer back to the enclosing object's schema.
    2. Forward references: Use DataType.OBJECT.reference('TypeName') to create a placeholder for a type that hasn't been defined yet.
    3. Mutual recursion: For types that refer to each other (e.g., Person refers to Company and Company refers to Person), place both types in the type_resolver dictionary of the rule_engine.Context. The references will then be resolved lazily at rule parse time.
    # Self-reference example
    Hero = rule_engine.DataType.OBJECT('Hero', attributes={
        'name': rule_engine.DataType.STRING,
        'nemesis': rule_engine.DataType.OBJECT.self,  # resolved to Hero
    })
    
    # Mutual recursion example
    Person = rule_engine.DataType.OBJECT('Person', attributes={
        'name': rule_engine.DataType.STRING,
        'employer': rule_engine.DataType.OBJECT.reference('Company'),
    })
    Company = rule_engine.DataType.OBJECT('Company', attributes={
        'name': rule_engine.DataType.STRING,
        'ceo': rule_engine.DataType.OBJECT.reference('Person'),
    })
    
    context = rule_engine.Context(type_resolver={
        'employee': Person,
        'Person': Person,
        'Company': Company,
    })
    rule = rule_engine.Rule('employee.employer.ceo.name == "Palpatine"', context=context)
  5. Handle nullability with the NULLABLE type

    master

    The NULLABLE(T) type constructor marks a data slot as permitting None (null) at runtime. It is structurally distinct from both T and NULL.

    Strictness and Errors

    Rule Engine uses Python-style semantics (not SQL three-valued logic). Many operators are strict and will reject a NULLABLE operand at parse time with an EvaluationError or FunctionCallError. These include:

    • Arithmetic (+, -, *, /, etc.)
    • Ordered comparisons (<, <=, >, >=)
    • Regex operators (=~, =~~, !~, !~~)
    • Bitwise operators
    • Unary minus
    • Containment (x in container)
    • Attribute/Item access (obj.attr, container[key], container[a:b])
    • Function arguments

    Discharging Nullability

    To use a nullable value with strict operators, you must "discharge" the nullability using one of these mechanisms:

    1. Null-coalesce (left ?? right): Evaluates to left if it is not None, otherwise returns right. This converts a NULLABLE(T) into a non-nullable type (unless right is also nullable).
    2. Safe Navigation: Use obj&.attr for safe attribute access or container&[key] for safe item access. These allow the expression to proceed without error, but the resulting value remains NULLABLE and must be discharged later if passed to a strict operator.

    Lenient Operators

    Equality and logical connectives always accept NULLABLE operands and return a plain BOOLEAN:

    • ==, !=, and, or, not
    • Ternary expressions (cond ? a : b) propagate nullability to the result.
    • not NULLABLE(BOOLEAN) returns a plain BOOLEAN (not None is True).
    # Example of discharging nullability with null-coalesce
    # If 'user.name' is NULLABLE(STRING), this ensures a STRING result
    rule_text = "user.name ?? 'Unknown'"
  6. Define OBJECT data types for schema validation

    master

    The rule_engine.DataType.OBJECT type allows you to define a schema with named, typed attributes. This enables parse-time validation: if a rule references an attribute not in the schema, an ObjectAttributeError is raised immediately.

    • Use rule_engine.DataType.OBJECT.self for self-referential attributes.
    • Use rule_engine.DataType.OBJECT.reference('TypeName') for cross-type references.
    • Attributes in an OBJECT are accessed using dot syntax (e.g., hero.name). Item access (e.g., hero['name']) is not supported.
    HeroType = rule_engine.DataType.OBJECT('Hero', attributes={
        'name': rule_engine.DataType.STRING,
        'publisher': rule_engine.DataType.STRING,
        'first_appearance': rule_engine.DataType.DATETIME,
        'nemesis': rule_engine.DataType.OBJECT.self,
    })
    
    context = rule_engine.Context(type_resolver={'Hero': HeroType})
    rule = rule_engine.Rule('hero.name == "Batman"', context=context)
  7. Use Array Comprehension to generate new arrays

    master

    Array comprehension allows you to apply an operation to each member of an iterable to generate a new ARRAY. The syntax is similar to Python's list comprehension.

    Syntax: [ result_expression for variable in iterable [if condition] ]

    Limitations: Unlike Python, the variable assignment cannot contain more than one value (no unpacking support).

    # Square an array of numbers
    [ v ** 2 for v in [1, 2, 3] ]
    
    # Square only odd numbers
    [ v ** 2 for v in [1, 2, 3] if v % 2 ]
  8. Understand the Rule Engine exception hierarchy

    master

    Rule Engine exceptions are organized into a hierarchy that allows for granular error handling. The two primary branches are EngineError (runtime and syntax issues) and DeprecationWarning. Under EngineError, errors are split into EvaluationError (problems during rule execution) and SyntaxError (problems with the rule's structure).

    EngineError
     +-- EvaluationError
          +-- ArithmeticError
          +-- AttributeResolutionError
               +-- ObjectAttributeError
          +-- AttributeTypeError
          +-- FunctionCallError
          +-- LookupError
          +-- SymbolResolutionError
          +-- SymbolTypeError
     +-- SyntaxError
          +-- BytesSyntaxError
          +-- DatetimeSyntaxError
          +-- FloatSyntaxError
          +-- RegexSyntaxError
          +-- RuleSyntaxError
          +-- StringSyntaxError
          +-- TimedeltaSyntaxError
     DeprecationWarning
      +-- MappingAttributeLookupDeprecation
  9. Access builtin symbols using the $ prefix

    master

    The rule engine provides a set of default symbols (functions, constants, and timestamps) accessible via the $ prefix. For example, you can access the constant pi using $pi.

    These builtins are provided by the Builtins.from_defaults() method. If you need to override these default values, you must create a custom subclass of Context and set its builtins attribute.

    # Example of accessing a builtin symbol in a rule string
    rule = "$pi > 3"