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_count ⇒ Integer
Total number of registered rules, including disabled ones.
-
#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.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 223 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
187 188 189 190 191 192 193 194 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 187 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
178 179 180 181 182 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 178 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.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 125 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.
170 171 172 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 170 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_count ⇒ Integer
Total number of registered rules, including disabled ones.
116 117 118 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 116 def rule_count @rules.length 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.
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 155 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.
211 212 213 214 215 216 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 211 def to_h { mode: @mode, rules: @rules.map(&:to_h) } end |
#validate_rules ⇒ Hash
Validate all rules have conditions and actions defined
199 200 201 202 203 204 205 206 |
# File 'lib/philiprehberger/rule_engine/engine.rb', line 199 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 |