Skip to content
garyld1962Public

About

Rust business-rules engine with a custom DSL — clean-room regeneration of gorules: 12+ audited bugs fixed, 15 operators added, 121 tests

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

21 Commits

Folders and files

Repository files navigation

rustrules

CI

A business rules engine that evaluates a custom DSL against JSON data — a clean-room Rust regeneration of gorules (2016), and the "after" half of a regenerate-don't-patch case study. The Go original was audited by four AI specialists (37 findings), its test suite rehabilitated 29→124 to pin down actual behavior, and then this crate was generated fresh from that specification: 12+ audited bugs fixed, 15 operators added, 121 tests (cargo test --workspace).

DSL Syntax

# Compare a JSON path to a constant
country IsEqualTo 'USA'

# Compare two paths
country IsEqualTo state

# Array selectors — apply a rule to every element
ALL orderItems.weight IsEqualTo '11'
ANY orderItems.availableInventory IsEqualTo 'NC'

# Conjunctions
country IsEqualTo 'USA' AND state IsEqualTo 'FL'
country IsEqualTo 'CANADA' OR state IsEqualTo 'TX'

# Multi-line rules (precedence parsing)
country IsEqualTo 'CANADA'
    AND state IsEqualTo 'FL'
    OR state IsEqualTo 'TX'

# Math expressions
'10' IsEqualTo |ADD '5' '5'|
'256' IsEqualTo |SQUARE '16'|

# String expressions
'SOUTH' IsEqualTo |TAKETILL '-' 'SOUTH-EAST'|
'TEST' IsEqualTo |TOUPPER 'test'|

Operators

Comparison

Operator Description
IsEqualTo Equality check
IsNotEqualTo Inequality check
IsGreaterThan Numeric greater than
IsGreaterThanOrEqualTo Numeric greater than or equal
IsLessThan Numeric less than
IsLessThanOrEqualTo Numeric less than or equal
Contains String contains substring
StartsWith String starts with prefix
EndsWith String ends with suffix
IsNull Value is empty
IsNotNull Value is not empty
In Value in comma-separated list
NotIn Value not in comma-separated list
IsLike Regex match
IsNotLike Regex non-match
IsTrue Boolean true check
IsFalse Boolean false check

Math

ADD, SUB, MUL, DIV, SQUARE — used inside pipe delimiters: |ADD '5' '10'|

String

TAKETILL, TOUPPER, TOLOWER — used inside pipe delimiters: |TOUPPER 'hello'|

Selectors

Selector Description
THIS Default — compare a single value
ANY True if any array element matches (OR)
ALL True if all array elements match (AND)

Usage as a Library

Add to your Cargo.toml:

[dependencies]
rustrules-core = { path = "crates/rustrules-core" }
use rustrules_core::{dsl_evaluate, evaluate_rules};
use serde_json::json;

let data = json!({
    "country": "USA",
    "state": "FL",
    "orderItems": [
        {"weight": "11"},
        {"weight": "11"}
    ]
});

// Simple evaluation
let result = dsl_evaluate("country IsEqualTo 'USA'", &data).unwrap();
assert!(result);

// Array selector
let result = dsl_evaluate("ALL orderItems.weight IsEqualTo '11'", &data).unwrap();
assert!(result);

// Multi-line with precedence
let rule = "country IsEqualTo 'USA'\nAND\nstate IsEqualTo 'FL'";
let result = evaluate_rules(rule, &data).unwrap();
assert!(result);

Rule Processing

Process rules from multiple sources:

use rustrules_core::{process_rules, RuleSource, RuleResult};
use std::collections::HashMap;

let data = json!({"country": "USA"});

// Single rule
let source = RuleSource::Text("country IsEqualTo 'USA'".into());
let result = process_rules(&source, &data).unwrap();
// RuleResult::Single(true)

// Multiple rules
let source = RuleSource::Collection(vec![
    "country IsEqualTo 'USA'".into(),
    "country IsEqualTo 'CANADA'".into(),
]);
let result = process_rules(&source, &data).unwrap();
// RuleResult::List([true, false])

// Named rules
let mut rules = HashMap::new();
rules.insert("is_usa".into(), "country IsEqualTo 'USA'".into());
rules.insert("is_canada".into(), "country IsEqualTo 'CANADA'".into());
let source = RuleSource::ObjectCollection(rules);
let result = process_rules(&source, &data).unwrap();
// RuleResult::Named({"is_usa": true, "is_canada": false})

// From a JSON rules file
let source = RuleSource::File {
    name: "is-country-canada".into(),
    path: "rulez.json".into(),
};
let result = process_rules(&source, &data).unwrap();

HTTP API

The rustrules-server crate provides an Axum HTTP server.

Run the server

cargo run -p rustrules-server
# or with custom host/port:
RUSTRULES_HOST=0.0.0.0 RUSTRULES_PORT=3000 cargo run -p rustrules-server

Endpoints

GET /status

curl http://localhost:8080/status
# {"status":"Service running"}

GET /operators/comparison

curl http://localhost:8080/operators/comparison
# {"operators":["IsEqualTo","IsNotEqualTo",...]}

GET /operators/math

curl http://localhost:8080/operators/math
# {"operators":["ADD","SUB","MUL","DIV","SQUARE"]}

GET /operators/string

curl http://localhost:8080/operators/string
# {"operators":["TAKETILL","TOUPPER","TOLOWER"]}

POST /rule

Evaluate a rule against JSON data:

curl -X POST http://localhost:8080/rule \
  -H "Content-Type: application/json" \
  -d '{
    "rule": "country IsEqualTo '\''USA'\''",
    "data": {"country": "USA", "state": "FL"}
  }'
# {"result":true}

Supports single rules, collections, and named rules:

# Collection — returns array of bools
curl -X POST http://localhost:8080/rule \
  -H "Content-Type: application/json" \
  -d '{
    "rule": ["country IsEqualTo '\''USA'\''", "state IsEqualTo '\''TX'\''"],
    "data": {"country": "USA", "state": "FL"}
  }'
# {"result":[true,false]}

# Named — returns map of bools
curl -X POST http://localhost:8080/rule \
  -H "Content-Type: application/json" \
  -d '{
    "rule": {"check_usa": "country IsEqualTo '\''USA'\''", "check_tx": "state IsEqualTo '\''TX'\''"},
    "data": {"country": "USA", "state": "FL"}
  }'
# {"result":{"check_usa":true,"check_tx":false}}

POST /expression

Evaluate a value expression:

curl -X POST http://localhost:8080/expression \
  -H "Content-Type: application/json" \
  -d '{
    "expression": "|ADD '\''5'\'' '\''10'\''|",
    "data": {}
  }'
# {"result":"15"}

Project Structure

rustrules/
├── Cargo.toml                         # Workspace root
├── crates/
│   ├── rustrules-core/                # Library crate
│   │   ├── src/
│   │   │   ├── lib.rs                 # Public API re-exports
│   │   │   ├── error.rs               # RustrulesError enum
│   │   │   ├── operator.rs            # All operator enums + evaluate
│   │   │   ├── value.rs               # Value types + JSON path traversal
│   │   │   ├── expr/                  # Expression trait + types
│   │   │   │   ├── mod.rs
│   │   │   │   ├── rule_expr.rs       # RuleExpression (with selectors)
│   │   │   │   ├── conjunction_expr.rs # AND/OR combinations
│   │   │   │   └── boolean_expr.rs    # Constant true/false
│   │   │   ├── parser/                # DSL parser
│   │   │   │   ├── mod.rs             # parse_dsl, parse_dsl_with_precedence
│   │   │   │   └── tokens.rs          # Tokenization utilities
│   │   │   ├── eval.rs                # Evaluation entry points
│   │   │   └── rule.rs                # Rule processor (file/collection/named)
│   │   └── tests/                     # Integration tests
│   │       ├── evaluator_tests.rs
│   │       └── rule_processor_tests.rs
│   └── rustrules-server/              # Binary crate (Axum HTTP API)
│       └── src/
│           ├── main.rs                # Server setup + graceful shutdown
│           ├── routes.rs              # Route handlers
│           └── models.rs              # Request/response types
└── tests/fixtures/                    # Test data
    ├── rulez.json
    └── test_data.json

Building

cargo build              # Debug build
cargo build --release    # Release build
cargo test               # Run all 121 tests
cargo clippy             # Lint check

License

MIT

About

Rust business-rules engine with a custom DSL — clean-room regeneration of gorules: 12+ audited bugs fixed, 15 operators added, 121 tests

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages