philiprehberger-rule_engine
Lightweight rule engine with declarative conditions and actions
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem 'philiprehberger-rule_engine'Or install directly:
gem install philiprehberger-rule_engineUsage
require 'philiprehberger/rule_engine'
engine = Philiprehberger::RuleEngine.new do
rule 'discount' do
condition { |f| f[:total] > 100 }
action { |f| f[:total] * 0.9 }
end
rule 'free_shipping' do
condition { |f| f[:total] > 50 }
action { |_| 'free shipping applied' }
end
end
results = engine.evaluate({ total: 150 })
# => [{ rule: 'discount', result: 135.0 }, { rule: 'free_shipping', result: 'free shipping applied' }]Priority
engine = Philiprehberger::RuleEngine.new do
rule 'low' do
priority 10
condition { |_| true }
action { |_| 'runs second' }
end
rule 'high' do
priority 1
condition { |_| true }
action { |_| 'runs first' }
end
endFirst-Match Mode
engine = Philiprehberger::RuleEngine.new(mode: :first) do
rule 'premium' do
priority 1
condition { |f| f[:tier] == 'premium' }
action { |_| { discount: 0.20 } }
end
rule 'standard' do
priority 2
condition { |_| true }
action { |_| { discount: 0.05 } }
end
end
results = engine.evaluate({ tier: 'premium' })
# => [{ rule: 'premium', result: { discount: 0.20 } }]Composite Conditions
engine = Philiprehberger::RuleEngine.new do
rule 'access' do
condition { |f| all?(f[:active], any?(f[:admin], f[:moderator])) }
action { |_| 'granted' }
end
rule 'blocked' do
condition { |f| none?(f[:verified], f[:trusted]) }
action { |_| 'denied' }
end
endRule Tags
Organize rules into categories and selectively evaluate subsets:
engine = Philiprehberger::RuleEngine.new do
rule "validate_age", tags: [:validation] do
condition { |f| f[:age] >= 18 }
action { |_| "valid" }
end
rule "apply_discount", tags: [:pricing] do
condition { |f| f[:premium] }
action { |_| "discount applied" }
end
end
engine.evaluate(facts, tags: [:validation]) # only validation rules
engine.dry_run(facts, tags: [:pricing]) # dry run only pricing
engine.rules_by_tag(:validation) # list rules with tagMultiple tags use OR logic — a rule matches if it has any of the specified tags.
Dynamic Rule Management
engine = Philiprehberger::RuleEngine.new
engine.add_rule('dynamic') do
condition { |f| f[:ready] }
action { |_| 'go' }
end
engine.disable_rule('dynamic')
engine.evaluate({ ready: true }) # => []
engine.enable_rule('dynamic')
engine.evaluate({ ready: true }) # => [{ rule: 'dynamic', result: 'go' }]
engine.remove_rule('dynamic')Rule Chaining
engine = Philiprehberger::RuleEngine.new do
rule 'fetch' do
action { |_| 10 }
end
rule 'double' do
action { |f| f[:input] * 2 }
end
rule 'format' do
action { |f| "Result: #{f[:input]}" }
end
end
engine.chain('fetch', 'double', 'format')
# => 'Result: 20'Execution Statistics
engine = Philiprehberger::RuleEngine.new do
rule 'tracked' do
condition { |_| true }
action { |_| 'ok' }
end
end
3.times { engine.evaluate({}) }
engine.stats
# => { 'tracked' => { evaluations: 3, matches: 3, executions: 3, avg_time: 0.00001, last_triggered: <Time> } }
engine.reset_stats!Dry Run
Evaluate without executing actions:
matched = engine.dry_run(facts)
# => [{ name: "discount", priority: 10 }]Conflict Detection
engine.detect_conflicts
# => [{ rules: ["rule_a", "rule_b"], priorities: [10, 5] }]Rule Validation
engine.validate_rules
# => { valid: false, issues: ["Rule 'x' has no action"] }Serialization
engine = Philiprehberger::RuleEngine.new(mode: :first) do
rule 'alpha' do
priority 1
condition { |_| true }
action { |_| 'go' }
end
end
data = engine.to_h
# => { mode: :first, rules: [{ name: 'alpha', priority: 1, enabled: true }] }
restored = Philiprehberger::RuleEngine.from_h(data) do |r|
r.condition { |_| true }
r.action { |_| 'restored' }
endAPI
Engine
| Method | Description |
|---|---|
.new(mode:) { } |
Create engine with rule definitions |
.from_h(data, &resolver) |
Reconstruct engine from serialized hash |
#rule(name) { } |
Define a rule with condition and action |
#evaluate(facts, tags: nil) |
Evaluate rules against facts; filter by tags if given |
#rules |
Array of registered rules |
#mode |
Current evaluation mode |
#to_h |
Serialize engine configuration to hash |
#add_rule(name) { } |
Add a rule after engine creation |
#remove_rule(name) |
Remove a rule by name |
#disable_rule(name) |
Disable a rule (skipped during evaluation) |
#enable_rule(name) |
Re-enable a disabled rule |
#chain(*rule_names) |
Execute rules sequentially as a pipeline |
#stats |
Per-rule execution statistics |
#reset_stats! |
Clear all execution statistics |
#rules_by_tag(tag) |
Return rules with the given tag |
#dry_run(facts, tags: nil) |
Evaluate rules without executing actions |
#detect_conflicts |
Find rule pairs that could both match |
#validate_rules |
Check all rules have conditions and actions |
Rule DSL
| Method | Description |
|---|---|
condition { |facts| } |
Set the match condition |
action { |facts| } |
Set the action to execute |
priority(n) |
Set priority (lower runs first) |
Composite Condition Helpers
| Method | Description |
|---|---|
all?(*conditions) |
True if all conditions are truthy |
any?(*conditions) |
True if any condition is truthy |
none?(*conditions) |
True if no conditions are truthy |
Development
bundle install
bundle exec rspec
bundle exec rubocopSupport
If you find this project useful: