Skip to main content

No project description provided

Project description

genruler

Python 3.12+ License: BSD-3-Clause

Table of Contents

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:

  1. Fork the repository
  2. Create a new branch (git checkout -b feature/improvement)
  3. Make your changes
  4. Run the tests (python -m pytest)
  5. Commit your changes (git commit -am 'Add new feature')
  6. Push to the branch (git push origin feature/improvement)
  7. 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

genruler-0.2.0.tar.gz (16.8 kB view details)

Uploaded Source

Built Distribution

genruler-0.2.0-py3-none-any.whl (15.4 kB view details)

Uploaded Python 3

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

Hashes for genruler-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6eaf49623bc8c5f00a2c1121caaa964cfd4d4078cf4ae1565893421980a52925
MD5 8eadf5abdc699f1e34821f0d08e3764f
BLAKE2b-256 7d9a46ed5ee1967b87c6e6a9d52c7dfa04e97b82c60d50a74dcf41741b24ea1c

See more details on using hashes here.

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

Hashes for genruler-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0e5cfbe86b4bbeed918d3d6307d2ce54e0d1d9950ba9c7f9026789ddf8ca1f81
MD5 02e5a7ce68b33de4491fb48c5c3c57ba
BLAKE2b-256 b80a12cb04ffa7140b2bc696e915fff46d93d7636be5cf00f14e4f7d761fda18

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page