Rule engine written in Rust for parsing and evaluating rules, with customisable tags and objects to evaluate against.
### *Note: Easy to understand config files are already present if you'd like to get started right away.*
A simple, concise syntax for writing matching rules.
=- equals!- not equals&- logical AND|- logical OR()- grouping for precedence,- shorthand for OR within the same field (e.g.color=red | color=bluebecomescolor=red,blue)
Simple equality:
color=red
AND condition:
color=red & size=large
OR condition:
color=red | color=blue
OR shorthand (comma):
color=red,blue
Equivalent to: color=red | color=blue
NOT condition:
color!red
Complex grouping:
(color=red,blue) & size!small
Matches: color is red OR blue, AND size is NOT small
Nested logic:
status=active & (priority=high | type=urgent)
Matches: status is active AND (priority is high OR type is urgent)
Multiple field conditions:
(type=admin,moderator) & status=active & role!guest
Matches: type is admin OR moderator, AND status is active, AND role is NOT guest
The rules engine uses three configuration files in the config/ directory:
Defines available tags (fields) and their possible values.
File: config/my_tags.tags
- Colour: Blue, Green, Red
- Shape: Circle, Rectangle, Square
- Size: Small, Medium, Large
Contains the actual matching rules written in the DSL syntax.
File: config/my_rules.rules
- (colour=blue,red) & shape!circle
- (colour=green) | shape=rectangle
Contains objects to be evaluated against the rules. Objects are grouped by type for flexibility.
File: config/my_objects.yaml
objects:
shapes:
- colour: [red, green]
shape: rectangle
size: large
- colour: green
shape: circle
size: small
cars:
- colour: grey
size: small
doors: 3
- colour: black
size: large
doors: 5Adding new object types:
Simply add a new key under objects with a list of items. Each type can have completely different attributes:
objects:
your_type_name:
- attribute1: value1
attribute2: value2
- attribute1: value3
attribute2: value4The type name (e.g., shapes, cars) is automatically assigned to each object in that group.
- Comments: Use
#for comments in all config files - Case-insensitive: All parsing is case-insensitive
- No quotes: Values don't require quotes
- Spaces: Optional and ignored in rules
The matching engine uses a DNF-based approach for efficient rule evaluation.
Parse the tags file and build an index of all available tags and their valid values. Validate the format and ensure each tag has a unique name and at least one value.
Parse each rule, validate syntax, convert to OR-of-ANDs format (each AND group is a "subrule"), build subrule objects, and create tag-to-subrule maps. For each subrule, track the expected clause count, actual match count (initialized to 0), comparison operators, and tag key-value pairs.
Example:
Original: (colour=blue,red) & shape!circle
DNF: (colour=blue & shape!circle) | (colour=red & shape!circle)
└────────── SR1 ───────────┘ └────────── SR2 ──────────┘
Subrule Objects:
SR1: {
expected_count: 2,
actual_count: 0,
comparison_ops: [ISEQ, NOEQ],
tag_kvs: {"colour": "blue", "shape": "circle"}
}
SR2: {
expected_count: 2,
actual_count: 0,
comparison_ops: [ISEQ, NOEQ],
tag_kvs: {"colour": "red", "shape": "circle"}
}
Tag-to-Subrule Maps:
colour map:
"blue" → [SR1]
"red" → [SR2]
shape map:
"circle" → [SR1, SR2] (appears in both with NOEQ operator)
Parse the objects YAML file and build a map of all objects to evaluate. Validate that each object has valid structure and assign object types based on their grouping in the YAML file.
For each object, check which clauses match and increment the actual_count for matching subrules.
Example object:
colour: blue
shape: square
size: largeMatching process:
colour=bluematches → incrementSR1.actual_countto 1shape!circlematches (square ≠ circle) → incrementSR1.actual_countto 2shape!circlematches → incrementSR2.actual_countto 1colour=reddoesn't match →SR2.actual_countstays at 1
A rule matches if any subrule has actual_count == expected_count.
Example:
SR1: actual_count = 2, expected_count = 2 → MATCH ✓
SR2: actual_count = 1, expected_count = 2 → no match
Result: MATCH