No project description provided
Project description
genruler
Table of Contents
- Overview
- Quick Start
- Installation
- Ruler DSL
- API Reference
- Extending GenRuler
- Error Handling
- Contributing
- License
Overview
A rule DSL language parser in Python that allows you to write and evaluate rules using a LISP-inspired syntax.
Quick Start
import genruler
# Parse a simple rule
rule = genruler.parse('(condition.equal (basic.field "name") "John")')
# Apply the rule to a context
context = {"name": "John"}
result = rule(context) # Returns True
Installation
You can install genruler directly from PyPI:
pip install genruler
Alternatively, you can install from source:
git clone https://github.com/jeffrey04/genruler.git
cd genruler
pip install -e .
Requirements
- Python 3.12 or higher
- funcparserlib >= 1.0.1
Ruler DSL
This mini-language is partially inspired by LISP. A rule is represented by a an s-expression.
Syntax & Structure
(namespace.function_name "some_arguments" "more_arguments_if_applicable")
A rule is usually consist of a function name, and a list of (sometimes optional) arguments. Function names are often namespaced (e.g. "boolean.and"
, "condition.equal"
etc.) and usually only recognized if placed in the first elemnt.
Unless otherwise specified, a rule can be inserted as an argument to another rule, for example a boolean.and
rule.
(boolean.and (condition.equal (basic.field "fieldA") "X"),
condition.equal (basic.field "fieldB") "Y")
Parsing and Evaluation
In order to parse the rule, just call genruler.parse
. The result is a function where you can put in a context object in order for it to compute a result.
import genruler
rule = genruler.parse('(condition.Equal (basic.Field "fieldA") "X")')
context = {"fieldA": "X"}
rule(context) // should return true
API Reference
Basic Functions
Functions for basic operations like field access and value handling.
basic.coalesce
(basic.coalesce $value $arg1 $arg2 ...)
Returns the first non-empty (truthy) value from a sequence of values. Similar to SQL's COALESCE function. Arguments are evaluated in order until a truthy value is found.
Examples:
# Returns "value" since it's the first truthy value
rule = genruler.parse('(basic.coalesce "" "value" "other")')
context = {}
result = rule(context) # Returns "value"
# Works with nested expressions
rule = genruler.parse('(basic.coalesce (basic.field "a") (basic.field "b") "default")')
context = {"b": "value", "a": None}
result = rule(context) # Returns "value"
basic.context
(basic.context $context_sub $argument)
Access nested context values by evaluating a sub-context expression and then evaluating an argument within that sub-context.
Examples:
# Access nested object
rule = genruler.parse('(basic.context (basic.field "user") (basic.field "name"))')
context = {"user": {"name": "John"}}
result = rule(context) # Returns "John"
# Multiple levels of nesting
rule = genruler.parse('(basic.context (basic.field "data") (basic.context (basic.field "user") (basic.field "email")))')
context = {"data": {"user": {"email": "john@example.com"}}}
result = rule(context) # Returns "john@example.com"
basic.field
(basic.field $key [$default])
Access field values from a dictionary or list context. For dictionaries, supports optional default values for missing keys. For lists, uses direct index access.
Examples:
# Dictionary access
rule = genruler.parse('(basic.field "name")')
context = {"name": "John"}
result = rule(context) # Returns "John"
# With default value
rule = genruler.parse('(basic.field "age" 0)')
context = {}
result = rule(context) # Returns 0
# List access
rule = genruler.parse('(basic.field 0)')
context = ["first", "second"]
result = rule(context) # Returns "first"
basic.value
Creates a constant value that is returned as-is, ignoring the context. Useful for comparing fields against fixed values:
- Only accepts literal values (numbers, strings)
- List syntax produces tuples:
("a" "b")
->("a", "b")
- Cannot contain sub-rules (will raise an error)
Examples:
# Simple constant values
rule = genruler.parse('(basic.value 42)')
result = rule({}) # Returns 42
rule = genruler.parse('(basic.value "active")')
result = rule({}) # Returns "active"
rule = genruler.parse('(basic.value ("a" "b" "c"))')
result = rule({}) # Returns ("a", "b", "c")
# Sub-rules are not allowed
rule = genruler.parse('(basic.value (basic.field "status"))') # ValueError: basic.value cannot accept sub-rules
# Use basic.value for constant comparisons
rule = genruler.parse('(condition.equal (basic.field "status") (basic.value "active"))')
result = rule({"status": "active"}) # Returns True
Number Functions
Functions for numeric operations.
number.add
(number.add $value1 $value2 ...)
Adds multiple numbers together. Values are evaluated in the context before addition.
Examples:
# Simple addition
rule = genruler.parse('(number.add 1 2 3)')
context = {}
result = rule(context) # Returns 6
# With field values
rule = genruler.parse('(number.add (basic.field "price") (basic.field "tax"))')
context = {"price": 100, "tax": 20}
result = rule(context) # Returns 120
number.subtract
(number.subtract $value1 $value2)
Subtracts the second value from the first value. Values are evaluated in the context before subtraction.
Examples:
# Simple subtraction
rule = genruler.parse('(number.subtract 10 3)')
context = {}
result = rule(context) # Returns 7
# With field values
rule = genruler.parse('(number.subtract (basic.field "total") (basic.field "discount"))')
context = {"total": 100, "discount": 20}
result = rule(context) # Returns 80
number.multiply
(number.multiply $value1 $value2 ...)
Multiplies multiple numbers together. Values are evaluated in the context before multiplication.
Examples:
# Simple multiplication
rule = genruler.parse('(number.multiply 2 3 4)')
context = {}
result = rule(context) # Returns 24
# With field values
rule = genruler.parse('(number.multiply (basic.field "quantity") (basic.field "price"))')
context = {"quantity": 5, "price": 10}
result = rule(context) # Returns 50
number.divide
(number.divide $value1 $value2)
Divides the first value by the second value. Values are evaluated in the context before division.
Examples:
# Simple division
rule = genruler.parse('(number.divide 10 2)')
context = {}
result = rule(context) # Returns 5.0
# With field values
rule = genruler.parse('(number.divide (basic.field "total") (basic.field "parts"))')
context = {"total": 100, "parts": 4}
result = rule(context) # Returns 25.0
number.modulo
(number.modulo $value1 $value2)
Computes the remainder when dividing the first value by the second value. Values are evaluated in the context before the modulo operation.
Examples:
# Simple modulo
rule = genruler.parse('(number.modulo 7 3)')
context = {}
result = rule(context) # Returns 1
# With field values
rule = genruler.parse('(number.modulo (basic.field "items") (basic.field "per_page"))')
context = {"items": 17, "per_page": 5}
result = rule(context) # Returns 2
Boolean Operators
Functions for logical operations.
boolean.and
(boolean.and $value1 $value2 ...)
Performs a logical AND operation on all values. Values are evaluated in the context before the operation. Returns True only if all values are True.
Examples:
# Simple AND operation
rule = genruler.parse('(boolean.and (condition.gt (basic.field "age") 18) (condition.equal (basic.field "verified") (boolean.tautology)))')
context = {"age": 21, "verified": True}
result = rule(context) # Returns True
# Multiple conditions
rule = genruler.parse('(boolean.and (basic.field "active") (basic.field "paid") (basic.field "verified"))')
context = {"active": True, "paid": True, "verified": True}
result = rule(context) # Returns True
boolean.or
(boolean.or $value1 $value2 ...)
Performs a logical OR operation on all values. Values are evaluated in the context before the operation. Returns True if any value is True.
Examples:
# Check multiple conditions
rule = genruler.parse('(boolean.or (condition.equal (basic.field "role") "admin") (condition.equal (basic.field "role") "moderator"))')
context = {"role": "admin"}
result = rule(context) # Returns True
# With field values
rule = genruler.parse('(boolean.or (basic.field "premium") (basic.field "trial"))')
context = {"premium": False, "trial": True}
result = rule(context) # Returns True
boolean.not
(boolean.not $value)
Performs a logical NOT operation on the value. The value is evaluated in the context before the operation.
Examples:
# Negate a condition
rule = genruler.parse('(boolean.not (condition.equal (basic.field "status") "blocked"))')
context = {"status": "active"}
result = rule(context) # Returns True
# Negate a field value
rule = genruler.parse('(boolean.not (basic.field "disabled"))')
context = {"disabled": False}
result = rule(context) # Returns True
boolean.tautology
(boolean.tautology)
Always returns True, regardless of the context. Useful as a default condition or in complex logical expressions.
Examples:
# Simple tautology
rule = genruler.parse('(boolean.tautology)')
context = {}
result = rule(context) # Returns True
# In combination with AND
rule = genruler.parse('(boolean.and (boolean.tautology) (condition.equal (basic.field "valid") true))')
context = {"valid": true}
result = rule(context) # Same as just checking valid=true
boolean.contradiction
(boolean.contradiction)
Always returns False, regardless of the context. Useful as a default condition or in complex logical expressions.
Examples:
# Simple contradiction
rule = genruler.parse('(boolean.contradiction)')
context = {}
result = rule(context) # Returns False
# In combination with OR
rule = genruler.parse('(boolean.or (boolean.contradiction) (condition.equal (basic.field "valid") true))')
context = {"valid": true}
result = rule(context) # Same as just checking valid=true
String Functions
Functions for string manipulation and field access.
string.concat
(string.concat $separator $value1 $value2 ...)
Joins multiple values into a single string using the specified separator. Each value is evaluated in the context and converted to a string before joining.
Examples:
# Join with comma separator
rule = genruler.parse('(string.concat "," "a" "b" "c")')
context = {}
result = rule(context) # Returns "a,b,c"
# Join with space, using field values
rule = genruler.parse('(string.concat " " (basic.field "first") (basic.field "last"))')
context = {"first": "John", "last": "Doe"}
result = rule(context) # Returns "John Doe"
string.concat_fields
(string.concat_fields $separator $field1 $field2 ...)
Similar to string.concat
but specifically for joining field values. Automatically retrieves and joins the values of specified fields from the context.
Examples:
# Join field values with comma
rule = genruler.parse('(string.concat_fields "," "first" "last")')
context = {"first": "John", "last": "Doe"}
result = rule(context) # Returns "John,Doe"
# Join multiple fields with custom separator
rule = genruler.parse('(string.concat_fields " - " "city" "state" "country")')
context = {"city": "San Francisco", "state": "CA", "country": "USA"}
result = rule(context) # Returns "San Francisco - CA - USA"
string.field
(string.field $key [$default])
Retrieves a field value from the context and converts it to a string. Similar to basic.field
but ensures the result is a string. Optionally accepts a default value if the field doesn't exist.
Examples:
# Basic string field access
rule = genruler.parse('(string.field "name")')
context = {"name": "John"}
result = rule(context) # Returns "John"
# Numbers are converted to strings
rule = genruler.parse('(string.field "age")')
context = {"age": 25}
result = rule(context) # Returns "25"
# With default value
rule = genruler.parse('(string.field "missing" "N/A")')
context = {}
result = rule(context) # Returns "N/A"
string.lower
(string.lower $value)
Converts a value to lowercase. The value is first evaluated in the context and then converted to lowercase.
Examples:
# Simple lowercase conversion
rule = genruler.parse('(string.lower "HELLO")')
context = {}
result = rule(context) # Returns "hello"
# Lowercase field value
rule = genruler.parse('(string.lower (basic.field "name"))')
context = {"name": "JOHN"}
result = rule(context) # Returns "john"
Condition Rules
Functions for comparing values and checking conditions.
condition.equal
(condition.equal $value1 $value2)
Compares two values for equality. Values are evaluated in the context before comparison.
Examples:
# Compare field with constant
rule = genruler.parse('(condition.equal (basic.field "name") "John")')
context = {"name": "John"}
result = rule(context) # Returns True
# Compare two fields
rule = genruler.parse('(condition.equal (basic.field "password") (basic.field "confirm"))')
context = {"password": "secret", "confirm": "secret"}
result = rule(context) # Returns True
condition.in
(condition.in $value $list)
Checks if a value is contained in a list. The value and list are evaluated in the context before checking.
Examples:
# Check against constant list
rule = genruler.parse('(condition.in (basic.value "apple") (basic.value ("apple" "banana" "orange")))')
context = {}
result = rule(context) # Returns True
# Check field value against list field
rule = genruler.parse('(condition.in (basic.field "fruit") (basic.field "allowed"))')
context = {"fruit": "apple", "allowed": ["apple", "banana"]}
result = rule(context) # Returns True
condition.is_none
(condition.is_none $value)
Checks if a value is None. The value is evaluated in the context before checking.
Examples:
# Check if field is None
rule = genruler.parse('(condition.is_none (basic.field "optional"))')
context = {"optional": None}
result = rule(context) # Returns True
# Check with nested expression
rule = genruler.parse('(basic.context (basic.field "user") (condition.is_none (basic.field "email")))')
context = {"user": {"email": None}}
result = rule(context) # Returns True
condition.is_true
(condition.is_true $value)
Checks if a value is exactly True (not just truthy). The value is evaluated in the context before checking.
Examples:
# Check boolean field
rule = genruler.parse('(condition.is_true (basic.field "active"))')
context = {"active": True}
result = rule(context) # Returns True
# Non-True values return False
rule = genruler.parse('(condition.is_true (basic.field "count"))')
context = {"count": 1} # Even though 1 is truthy, it's not True
result = rule(context) # Returns False
condition.gt (Greater Than)
(condition.gt $value1 $value2)
Checks if the first value is greater than the second value. Values are evaluated in the context before comparison.
Examples:
# Compare numbers
rule = genruler.parse('(condition.gt (basic.field "age") 18)')
context = {"age": 21}
result = rule(context) # Returns True
# Compare field values
rule = genruler.parse('(condition.gt (basic.field "score") (basic.field "threshold"))')
context = {"score": 85, "threshold": 70}
result = rule(context) # Returns True
condition.ge (Greater Than or Equal)
(condition.ge $value1 $value2)
Checks if the first value is greater than or equal to the second value. Values are evaluated in the context before comparison.
Examples:
# Compare numbers
rule = genruler.parse('(condition.ge (basic.field "age") 18)')
context = {"age": 18}
result = rule(context) # Returns True
# Compare field values
rule = genruler.parse('(condition.ge (basic.field "score") (basic.field "passing"))')
context = {"score": 70, "passing": 70}
result = rule(context) # Returns True
condition.lt (Less Than)
(condition.lt $value1 $value2)
Checks if the first value is less than the second value. Values are evaluated in the context before comparison.
Examples:
# Compare numbers
rule = genruler.parse('(condition.lt (basic.field "age") 18)')
context = {"age": 17}
result = rule(context) # Returns True
# Compare field values
rule = genruler.parse('(condition.lt (basic.field "score") (basic.field "threshold"))')
context = {"score": 60, "threshold": 70}
result = rule(context) # Returns True
condition.le (Less Than or Equal)
(condition.le $value1 $value2)
Checks if the first value is less than or equal to the second value. Values are evaluated in the context before comparison.
Examples:
# Compare numbers
rule = genruler.parse('(condition.le (basic.field "age") 18)')
context = {"age": 18}
result = rule(context) # Returns True
# Compare field values
rule = genruler.parse('(condition.le (basic.field "score") (basic.field "passing"))')
context = {"score": 70, "passing": 70}
result = rule(context) # Returns True
List Functions
Functions for working with lists and sequences.
list.length
(list.length $list)
Returns the length of a list. The list argument is evaluated in the context before calculating the length.
Examples:
# Direct list length
rule = genruler.parse('(list.length ["a", "b", "c"])')
context = {}
result = rule(context) # Returns 3
# Field list length
rule = genruler.parse('(list.length (basic.field "items"))')
context = {"items": [1, 2, 3, 4]}
result = rule(context) # Returns 4
# Empty list
rule = genruler.parse('(list.length (basic.field "empty"))')
context = {"empty": []}
result = rule(context) # Returns 0
Extending GenRuler
GenRuler can be extended with custom functions through the env
parameter in the parse
function. This allows you to add domain-specific functionality without modifying the core library.
from genruler.library import compute
from genruler.modules import basic
class CustomModule:
@staticmethod
def greet():
return lambda ctx: f"Hello, {compute(basic.field('name', 'World'))}!"
# Use custom functions in rules
rule = genruler.parse("(greet)", env=CustomModule)
result = rule({"name": "Alice"}) # Returns "Hello, Alice!"
Custom functions should:
- Return a callable that takes a context parameter
- Use
genruler.library.compute
for evaluating arguments that might be rules - Keep functions pure - only depend on arguments and context
- Follow the same error handling patterns as built-in functions
Error Handling
The library provides clear error messages for common issues:
# Invalid function name
rule = genruler.parse('(invalid_fn "value")')
# InvalidFunctionNameError: Invalid function name 'invalid_fn'
# Missing closing parenthesis
rule = genruler.parse('(basic.field "name"')
# ValueError: Parse error at position 20
# Missing field in context
rule = genruler.parse('(basic.field "age")')
rule({}) # KeyError: 'age'
# Invalid sub-rule in basic.value
rule = genruler.parse('(basic.value (basic.field "status"))')
# ValueError: basic.value cannot accept sub-rules
Contributing
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a new branch (
git checkout -b feature/improvement
) - Make your changes
- Run the tests (
python -m pytest
) - Commit your changes (
git commit -am 'Add new feature'
) - Push to the branch (
git push origin feature/improvement
) - Create a Pull Request
License
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file genruler-0.2.0.tar.gz
.
File metadata
- Download URL: genruler-0.2.0.tar.gz
- Upload date:
- Size: 16.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.4 CPython/3.12.7 Linux/6.11.0-8-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6eaf49623bc8c5f00a2c1121caaa964cfd4d4078cf4ae1565893421980a52925 |
|
MD5 | 8eadf5abdc699f1e34821f0d08e3764f |
|
BLAKE2b-256 | 7d9a46ed5ee1967b87c6e6a9d52c7dfa04e97b82c60d50a74dcf41741b24ea1c |
File details
Details for the file genruler-0.2.0-py3-none-any.whl
.
File metadata
- Download URL: genruler-0.2.0-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.4 CPython/3.12.7 Linux/6.11.0-8-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0e5cfbe86b4bbeed918d3d6307d2ce54e0d1d9950ba9c7f9026789ddf8ca1f81 |
|
MD5 | 02e5a7ce68b33de4491fb48c5c3c57ba |
|
BLAKE2b-256 | b80a12cb04ffa7140b2bc696e915fff46d93d7636be5cf00f14e4f7d761fda18 |