Class: Philiprehberger::RuleEngine::Engine
- Inherits:
-
Object
- Object
- Philiprehberger::RuleEngine::Engine
- Includes:
- Helpers
- Defined in:
- lib/philiprehberger/rule_engine/engine.rb
Overview
A lightweight rule engine with declarative conditions and actions.
Instance Attribute Summary collapse
-
#mode ⇒ Symbol
readonly
The evaluation mode (:all or :first).
-
#rules ⇒ Array<Rule>
readonly
The registered rules.
Instance Method Summary collapse
-
#add_rule(name, tags: []) {|rule| ... } ⇒ Rule
Add a rule after engine creation.
-
#chain(*rule_names) ⇒ Object
Execute rules sequentially as a pipeline.
-
#clear_rules! ⇒ self
Remove all rules and reset execution statistics.
-
#detect_conflicts ⇒ Array<Hash>
Find rules with potentially overlapping conditions.
-
#disable_rule(name) ⇒ void
Disable a rule by name (skipped during evaluation).
-
#dry_run(facts, tags: nil) ⇒ Array<Hash>
Evaluate rules without executing actions, return matched rule info.
-
#enable_rule(name) ⇒ void
Enable a rule by name.
-
#evaluate(facts, tags: nil) ⇒ Array<Hash>
Evaluate all rules against the given facts.
-
#initialize(mode: :all) {|engine| ... } ⇒ Engine
constructor
Create a new rule engine.
-
#remove_rule(name) ⇒ Rule?
Remove a rule by name.
-
#reset_stats! ⇒ void
Reset all execution statistics.
-
#rule(name, tags: []) {|rule| ... } ⇒ Rule
Define a rule using the DSL.
-
#rule_names ⇒ Array<String>
Return the names of all registered rules in declaration order.
-
#rules_by_tag(tag) ⇒ Array<Rule>
Return rules matching a specific tag.
-
#stats ⇒ Hash
Return per-rule execution statistics.
-
#to_h ⇒ Hash
Serialize the engine configuration to a hash.
-
#validate_rules ⇒ Hash
Validate all rules have conditions and actions defined.
Methods included from Helpers
Constructor Details
#initialize(mode: :all) {|engine| ... } ⇒ Engine
Create a new rule engine.
19 20 21 22 23 24 25 26 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 19 def initialize(mode: :all, &block) raise Error, 'mode must be :all or :first' unless %i[all first].include?(mode) @rules = [] @mode = mode @stats = {} instance_eval(&block) if block end |
Instance Attribute Details
#mode ⇒ Symbol (readonly)
Returns the evaluation mode (:all or :first).
13 14 15 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 13 def mode @mode end |
#rules ⇒ Array<Rule> (readonly)
Returns the registered rules.
10 11 12 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 10 def rules @rules end |
Instance Method Details
#add_rule(name, tags: []) {|rule| ... } ⇒ Rule
Add a rule after engine creation.
47 48 49 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 47 def add_rule(name, tags: [], &block) rule(name, tags: , &block) end |
#chain(*rule_names) ⇒ Object
Execute rules sequentially as a pipeline. Each rule’s action result is passed as input: to the next rule.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 216 def chain(*rule_names) chain_rules = rule_names.map do |name| found = @rules.find { |r| r.name == name } raise Error, "rule not found: #{name}" unless found found end input = nil chain_rules.each do |r| facts = { input: input } input = r.execute(facts) end input end |
#clear_rules! ⇒ self
Remove all rules and reset execution statistics.
Clears the internal rule list and discards all per-rule stat entries, returning the engine to a fresh state while preserving its mode.
100 101 102 103 104 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 100 def clear_rules! @rules.clear @stats.clear self end |
#detect_conflicts ⇒ Array<Hash>
Find rules with potentially overlapping conditions
180 181 182 183 184 185 186 187 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 180 def detect_conflicts pairs = [] sorted = enabled_rules_sorted sorted.combination(2) do |a, b| pairs << { rules: [a.name, b.name], priorities: [a.priority, b.priority] } end pairs end |
#disable_rule(name) ⇒ void
This method returns an undefined value.
Disable a rule by name (skipped during evaluation).
68 69 70 71 72 73 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 68 def disable_rule(name) found = @rules.find { |r| r.name == name } raise Error, "rule not found: #{name}" unless found found.enabled = false end |
#dry_run(facts, tags: nil) ⇒ Array<Hash>
Evaluate rules without executing actions, return matched rule info
171 172 173 174 175 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 171 def dry_run(facts, tags: nil) matched = (enabled_rules_sorted, ).select { |rule| rule.matches?(facts) } matched = [matched.first].compact if @mode == :first matched.map { |rule| { name: rule.name, priority: rule.priority } } end |
#enable_rule(name) ⇒ void
This method returns an undefined value.
Enable a rule by name.
79 80 81 82 83 84 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 79 def enable_rule(name) found = @rules.find { |r| r.name == name } raise Error, "rule not found: #{name}" unless found found.enabled = true end |
#evaluate(facts, tags: nil) ⇒ Array<Hash>
Evaluate all rules against the given facts.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 118 def evaluate(facts, tags: nil) sorted = (@rules.select(&:enabled), ).sort_by(&:priority) results = [] sorted.each do |r| stat = @stats[r.name] ||= new_stat_entry stat[:evaluations] += 1 next unless r.matches?(facts) stat[:matches] += 1 start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = r.execute(facts) elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time stat[:executions] += 1 stat[:total_time] += elapsed stat[:avg_time] = stat[:total_time] / stat[:executions] stat[:last_triggered] = Time.now results << { rule: r.name, result: result } break if @mode == :first end results end |
#remove_rule(name) ⇒ Rule?
Remove a rule by name.
55 56 57 58 59 60 61 62 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 55 def remove_rule(name) index = @rules.index { |r| r.name == name } return nil unless index removed = @rules.delete_at(index) @stats.delete(name) removed end |
#reset_stats! ⇒ void
This method returns an undefined value.
Reset all execution statistics.
163 164 165 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 163 def reset_stats! @stats.each_key { |k| @stats[k] = new_stat_entry } end |
#rule(name, tags: []) {|rule| ... } ⇒ Rule
Define a rule using the DSL.
33 34 35 36 37 38 39 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 33 def rule(name, tags: [], &block) r = Rule.new(name, tags: ) r.instance_eval(&block) if block @rules << r @stats[name] = new_stat_entry r end |
#rule_names ⇒ Array<String>
Return the names of all registered rules in declaration order.
109 110 111 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 109 def rule_names @rules.map(&:name) end |
#rules_by_tag(tag) ⇒ Array<Rule>
Return rules matching a specific tag.
90 91 92 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 90 def rules_by_tag(tag) @rules.select { |r| r..include?(tag.to_sym) } end |
#stats ⇒ Hash
Return per-rule execution statistics.
148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 148 def stats @stats.transform_values do |s| { evaluations: s[:evaluations], matches: s[:matches], executions: s[:executions], avg_time: s[:avg_time], last_triggered: s[:last_triggered] } end end |
#to_h ⇒ Hash
Serialize the engine configuration to a hash.
204 205 206 207 208 209 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 204 def to_h { mode: @mode, rules: @rules.map(&:to_h) } end |
#validate_rules ⇒ Hash
Validate all rules have conditions and actions defined
192 193 194 195 196 197 198 199 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 192 def validate_rules issues = [] @rules.each do |rule| issues << "Rule '#{rule.name}' has no condition" unless rule.instance_variable_get(:@condition) issues << "Rule '#{rule.name}' has no action" unless rule.instance_variable_get(:@action) end { valid: issues.empty?, issues: issues } end |