philiprehberger-rule_engine 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +125 -3
- data/lib/philiprehberger/rule_engine/engine.rb +129 -2
- data/lib/philiprehberger/rule_engine/helpers.rb +32 -0
- data/lib/philiprehberger/rule_engine/rule.rb +19 -2
- data/lib/philiprehberger/rule_engine/version.rb +1 -1
- data/lib/philiprehberger/rule_engine.rb +29 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48759336967f009c48193bd19ba3abc3edf2c45712e21f18bbc3382b50ce2c19
|
|
4
|
+
data.tar.gz: 2894eca7b2c22440eb52c0920ae3edcb1642ada7193668f3e2d01213c6d0f560
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f1df77fa3a9caa8dc480eeb6cdb9c2010806f2b57108b8c4fde06b60830c853387918d225d48584d56cca89aeb2e790eee245ef0081debe6f9b8af723b3fa47
|
|
7
|
+
data.tar.gz: 9cd80a35da5092a44dd170965cd508faed0d3aba1d25a4005a91af5da037dff4a2e41cf9e54cb4995533e7240f4dd47155c5c26259d495d238f0eb41fd8bb9ee
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `engine.to_h` and `RuleEngine.from_h` for rule configuration serialization
|
|
14
|
+
- Composite condition helpers: `all?`, `any?`, `none?` for readable logic
|
|
15
|
+
- `engine.stats` for per-rule execution statistics with timing
|
|
16
|
+
- Dynamic rule management: `add_rule`, `remove_rule`, `disable_rule`, `enable_rule`
|
|
17
|
+
- `engine.chain(*rule_names)` for sequential rule pipelines
|
|
18
|
+
|
|
10
19
|
## [0.1.2] - 2026-03-24
|
|
11
20
|
|
|
12
21
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/philiprehberger/rb-rule-engine/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-rule_engine)
|
|
5
|
+
[](https://github.com/philiprehberger/rb-rule-engine/releases)
|
|
6
|
+
[](https://github.com/philiprehberger/rb-rule-engine/commits/main)
|
|
5
7
|
[](LICENSE)
|
|
8
|
+
[](https://github.com/philiprehberger/rb-rule-engine/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
9
|
+
[](https://github.com/philiprehberger/rb-rule-engine/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
10
|
+
[](https://github.com/sponsors/philiprehberger)
|
|
6
11
|
|
|
7
12
|
Lightweight rule engine with declarative conditions and actions
|
|
8
13
|
|
|
@@ -15,7 +20,7 @@ Lightweight rule engine with declarative conditions and actions
|
|
|
15
20
|
Add to your Gemfile:
|
|
16
21
|
|
|
17
22
|
```ruby
|
|
18
|
-
gem
|
|
23
|
+
gem 'philiprehberger-rule_engine'
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
Or install directly:
|
|
@@ -27,7 +32,7 @@ gem install philiprehberger-rule_engine
|
|
|
27
32
|
## Usage
|
|
28
33
|
|
|
29
34
|
```ruby
|
|
30
|
-
require
|
|
35
|
+
require 'philiprehberger/rule_engine'
|
|
31
36
|
|
|
32
37
|
engine = Philiprehberger::RuleEngine.new do
|
|
33
38
|
rule 'discount' do
|
|
@@ -84,6 +89,100 @@ results = engine.evaluate({ tier: 'premium' })
|
|
|
84
89
|
# => [{ rule: 'premium', result: { discount: 0.20 } }]
|
|
85
90
|
```
|
|
86
91
|
|
|
92
|
+
### Composite Conditions
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
engine = Philiprehberger::RuleEngine.new do
|
|
96
|
+
rule 'access' do
|
|
97
|
+
condition { |f| all?(f[:active], any?(f[:admin], f[:moderator])) }
|
|
98
|
+
action { |_| 'granted' }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
rule 'blocked' do
|
|
102
|
+
condition { |f| none?(f[:verified], f[:trusted]) }
|
|
103
|
+
action { |_| 'denied' }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Dynamic Rule Management
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
engine = Philiprehberger::RuleEngine.new
|
|
112
|
+
|
|
113
|
+
engine.add_rule('dynamic') do
|
|
114
|
+
condition { |f| f[:ready] }
|
|
115
|
+
action { |_| 'go' }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
engine.disable_rule('dynamic')
|
|
119
|
+
engine.evaluate({ ready: true }) # => []
|
|
120
|
+
|
|
121
|
+
engine.enable_rule('dynamic')
|
|
122
|
+
engine.evaluate({ ready: true }) # => [{ rule: 'dynamic', result: 'go' }]
|
|
123
|
+
|
|
124
|
+
engine.remove_rule('dynamic')
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Rule Chaining
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
engine = Philiprehberger::RuleEngine.new do
|
|
131
|
+
rule 'fetch' do
|
|
132
|
+
action { |_| 10 }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
rule 'double' do
|
|
136
|
+
action { |f| f[:input] * 2 }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
rule 'format' do
|
|
140
|
+
action { |f| "Result: #{f[:input]}" }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
engine.chain('fetch', 'double', 'format')
|
|
145
|
+
# => 'Result: 20'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Execution Statistics
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
engine = Philiprehberger::RuleEngine.new do
|
|
152
|
+
rule 'tracked' do
|
|
153
|
+
condition { |_| true }
|
|
154
|
+
action { |_| 'ok' }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
3.times { engine.evaluate({}) }
|
|
159
|
+
|
|
160
|
+
engine.stats
|
|
161
|
+
# => { 'tracked' => { evaluations: 3, matches: 3, executions: 3, avg_time: 0.00001, last_triggered: <Time> } }
|
|
162
|
+
|
|
163
|
+
engine.reset_stats!
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Serialization
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
engine = Philiprehberger::RuleEngine.new(mode: :first) do
|
|
170
|
+
rule 'alpha' do
|
|
171
|
+
priority 1
|
|
172
|
+
condition { |_| true }
|
|
173
|
+
action { |_| 'go' }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
data = engine.to_h
|
|
178
|
+
# => { mode: :first, rules: [{ name: 'alpha', priority: 1, enabled: true }] }
|
|
179
|
+
|
|
180
|
+
restored = Philiprehberger::RuleEngine.from_h(data) do |r|
|
|
181
|
+
r.condition { |_| true }
|
|
182
|
+
r.action { |_| 'restored' }
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
87
186
|
## API
|
|
88
187
|
|
|
89
188
|
### `Engine`
|
|
@@ -91,10 +190,19 @@ results = engine.evaluate({ tier: 'premium' })
|
|
|
91
190
|
| Method | Description |
|
|
92
191
|
|--------|-------------|
|
|
93
192
|
| `.new(mode:) { }` | Create engine with rule definitions |
|
|
193
|
+
| `.from_h(data, &resolver)` | Reconstruct engine from serialized hash |
|
|
94
194
|
| `#rule(name) { }` | Define a rule with condition and action |
|
|
95
195
|
| `#evaluate(facts)` | Evaluate rules against facts |
|
|
96
196
|
| `#rules` | Array of registered rules |
|
|
97
197
|
| `#mode` | Current evaluation mode |
|
|
198
|
+
| `#to_h` | Serialize engine configuration to hash |
|
|
199
|
+
| `#add_rule(name) { }` | Add a rule after engine creation |
|
|
200
|
+
| `#remove_rule(name)` | Remove a rule by name |
|
|
201
|
+
| `#disable_rule(name)` | Disable a rule (skipped during evaluation) |
|
|
202
|
+
| `#enable_rule(name)` | Re-enable a disabled rule |
|
|
203
|
+
| `#chain(*rule_names)` | Execute rules sequentially as a pipeline |
|
|
204
|
+
| `#stats` | Per-rule execution statistics |
|
|
205
|
+
| `#reset_stats!` | Clear all execution statistics |
|
|
98
206
|
|
|
99
207
|
### Rule DSL
|
|
100
208
|
|
|
@@ -104,6 +212,14 @@ results = engine.evaluate({ tier: 'premium' })
|
|
|
104
212
|
| `action { \|facts\| }` | Set the action to execute |
|
|
105
213
|
| `priority(n)` | Set priority (lower runs first) |
|
|
106
214
|
|
|
215
|
+
### Composite Condition Helpers
|
|
216
|
+
|
|
217
|
+
| Method | Description |
|
|
218
|
+
|--------|-------------|
|
|
219
|
+
| `all?(*conditions)` | True if all conditions are truthy |
|
|
220
|
+
| `any?(*conditions)` | True if any condition is truthy |
|
|
221
|
+
| `none?(*conditions)` | True if no conditions are truthy |
|
|
222
|
+
|
|
107
223
|
## Development
|
|
108
224
|
|
|
109
225
|
```bash
|
|
@@ -112,6 +228,12 @@ bundle exec rspec
|
|
|
112
228
|
bundle exec rubocop
|
|
113
229
|
```
|
|
114
230
|
|
|
231
|
+
## Support
|
|
232
|
+
|
|
233
|
+
- [Bug Reports](https://github.com/philiprehberger/rb-rule-engine/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
234
|
+
- [Feature Requests](https://github.com/philiprehberger/rb-rule-engine/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
235
|
+
- [GitHub Sponsors](https://github.com/sponsors/philiprehberger)
|
|
236
|
+
|
|
115
237
|
## License
|
|
116
238
|
|
|
117
|
-
MIT
|
|
239
|
+
[MIT](LICENSE)
|
|
@@ -4,6 +4,8 @@ module Philiprehberger
|
|
|
4
4
|
module RuleEngine
|
|
5
5
|
# A lightweight rule engine with declarative conditions and actions.
|
|
6
6
|
class Engine
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
7
9
|
# @return [Array<Rule>] the registered rules
|
|
8
10
|
attr_reader :rules
|
|
9
11
|
|
|
@@ -15,10 +17,11 @@ module Philiprehberger
|
|
|
15
17
|
# @param mode [Symbol] :all to run all matching rules, :first to stop after first match
|
|
16
18
|
# @yield [engine] block for defining rules using the DSL
|
|
17
19
|
def initialize(mode: :all, &block)
|
|
18
|
-
raise Error,
|
|
20
|
+
raise Error, 'mode must be :all or :first' unless %i[all first].include?(mode)
|
|
19
21
|
|
|
20
22
|
@rules = []
|
|
21
23
|
@mode = mode
|
|
24
|
+
@stats = {}
|
|
22
25
|
instance_eval(&block) if block
|
|
23
26
|
end
|
|
24
27
|
|
|
@@ -31,27 +34,151 @@ module Philiprehberger
|
|
|
31
34
|
r = Rule.new(name)
|
|
32
35
|
r.instance_eval(&block) if block
|
|
33
36
|
@rules << r
|
|
37
|
+
@stats[name] = new_stat_entry
|
|
34
38
|
r
|
|
35
39
|
end
|
|
36
40
|
|
|
41
|
+
# Add a rule after engine creation.
|
|
42
|
+
#
|
|
43
|
+
# @param name [String] the rule name
|
|
44
|
+
# @yield [rule] block for configuring the rule
|
|
45
|
+
# @return [Rule] the created rule
|
|
46
|
+
def add_rule(name, &)
|
|
47
|
+
rule(name, &)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Remove a rule by name.
|
|
51
|
+
#
|
|
52
|
+
# @param name [String] the rule name to remove
|
|
53
|
+
# @return [Rule, nil] the removed rule, or nil if not found
|
|
54
|
+
def remove_rule(name)
|
|
55
|
+
index = @rules.index { |r| r.name == name }
|
|
56
|
+
return nil unless index
|
|
57
|
+
|
|
58
|
+
removed = @rules.delete_at(index)
|
|
59
|
+
@stats.delete(name)
|
|
60
|
+
removed
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Disable a rule by name (skipped during evaluation).
|
|
64
|
+
#
|
|
65
|
+
# @param name [String] the rule name to disable
|
|
66
|
+
# @return [void]
|
|
67
|
+
def disable_rule(name)
|
|
68
|
+
found = @rules.find { |r| r.name == name }
|
|
69
|
+
raise Error, "rule not found: #{name}" unless found
|
|
70
|
+
|
|
71
|
+
found.enabled = false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Enable a rule by name.
|
|
75
|
+
#
|
|
76
|
+
# @param name [String] the rule name to enable
|
|
77
|
+
# @return [void]
|
|
78
|
+
def enable_rule(name)
|
|
79
|
+
found = @rules.find { |r| r.name == name }
|
|
80
|
+
raise Error, "rule not found: #{name}" unless found
|
|
81
|
+
|
|
82
|
+
found.enabled = true
|
|
83
|
+
end
|
|
84
|
+
|
|
37
85
|
# Evaluate all rules against the given facts.
|
|
38
86
|
#
|
|
39
87
|
# @param facts [Object] the facts to evaluate
|
|
40
88
|
# @return [Array<Hash>] results with :rule and :result for each matched rule
|
|
41
89
|
def evaluate(facts)
|
|
42
|
-
sorted = @rules.sort_by(&:priority)
|
|
90
|
+
sorted = @rules.select(&:enabled).sort_by(&:priority)
|
|
43
91
|
results = []
|
|
44
92
|
|
|
45
93
|
sorted.each do |r|
|
|
94
|
+
stat = @stats[r.name] ||= new_stat_entry
|
|
95
|
+
stat[:evaluations] += 1
|
|
96
|
+
|
|
46
97
|
next unless r.matches?(facts)
|
|
47
98
|
|
|
99
|
+
stat[:matches] += 1
|
|
100
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
101
|
result = r.execute(facts)
|
|
102
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
103
|
+
|
|
104
|
+
stat[:executions] += 1
|
|
105
|
+
stat[:total_time] += elapsed
|
|
106
|
+
stat[:avg_time] = stat[:total_time] / stat[:executions]
|
|
107
|
+
stat[:last_triggered] = Time.now
|
|
108
|
+
|
|
49
109
|
results << { rule: r.name, result: result }
|
|
50
110
|
break if @mode == :first
|
|
51
111
|
end
|
|
52
112
|
|
|
53
113
|
results
|
|
54
114
|
end
|
|
115
|
+
|
|
116
|
+
# Return per-rule execution statistics.
|
|
117
|
+
#
|
|
118
|
+
# @return [Hash] stats keyed by rule name
|
|
119
|
+
def stats
|
|
120
|
+
@stats.transform_values do |s|
|
|
121
|
+
{
|
|
122
|
+
evaluations: s[:evaluations],
|
|
123
|
+
matches: s[:matches],
|
|
124
|
+
executions: s[:executions],
|
|
125
|
+
avg_time: s[:avg_time],
|
|
126
|
+
last_triggered: s[:last_triggered]
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Reset all execution statistics.
|
|
132
|
+
#
|
|
133
|
+
# @return [void]
|
|
134
|
+
def reset_stats!
|
|
135
|
+
@stats.each_key { |k| @stats[k] = new_stat_entry }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Serialize the engine configuration to a hash.
|
|
139
|
+
#
|
|
140
|
+
# @return [Hash] engine metadata including mode and rules
|
|
141
|
+
def to_h
|
|
142
|
+
{
|
|
143
|
+
mode: @mode,
|
|
144
|
+
rules: @rules.map(&:to_h)
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Execute rules sequentially as a pipeline.
|
|
149
|
+
# Each rule's action result is passed as input: to the next rule.
|
|
150
|
+
#
|
|
151
|
+
# @param rule_names [Array<String>] ordered rule names to chain
|
|
152
|
+
# @return [Object] the final action's result
|
|
153
|
+
def chain(*rule_names)
|
|
154
|
+
chain_rules = rule_names.map do |name|
|
|
155
|
+
found = @rules.find { |r| r.name == name }
|
|
156
|
+
raise Error, "rule not found: #{name}" unless found
|
|
157
|
+
|
|
158
|
+
found
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
input = nil
|
|
162
|
+
chain_rules.each do |r|
|
|
163
|
+
facts = { input: input }
|
|
164
|
+
input = r.execute(facts)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
input
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def new_stat_entry
|
|
173
|
+
{
|
|
174
|
+
evaluations: 0,
|
|
175
|
+
matches: 0,
|
|
176
|
+
executions: 0,
|
|
177
|
+
total_time: 0.0,
|
|
178
|
+
avg_time: 0.0,
|
|
179
|
+
last_triggered: nil
|
|
180
|
+
}
|
|
181
|
+
end
|
|
55
182
|
end
|
|
56
183
|
end
|
|
57
184
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module RuleEngine
|
|
5
|
+
# Composite condition helpers for readable logic in condition blocks.
|
|
6
|
+
module Helpers
|
|
7
|
+
# Returns true if all conditions are truthy.
|
|
8
|
+
#
|
|
9
|
+
# @param conditions [Array<Boolean, Proc>] values or procs to evaluate
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def all?(*conditions)
|
|
12
|
+
conditions.all? { |c| c.is_a?(Proc) ? c.call : c }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns true if any condition is truthy.
|
|
16
|
+
#
|
|
17
|
+
# @param conditions [Array<Boolean, Proc>] values or procs to evaluate
|
|
18
|
+
# @return [Boolean]
|
|
19
|
+
def any?(*conditions)
|
|
20
|
+
conditions.any? { |c| c.is_a?(Proc) ? c.call : c }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns true if no conditions are truthy.
|
|
24
|
+
#
|
|
25
|
+
# @param conditions [Array<Boolean, Proc>] values or procs to evaluate
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def none?(*conditions)
|
|
28
|
+
conditions.none? { |c| c.is_a?(Proc) ? c.call : c }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -4,15 +4,21 @@ module Philiprehberger
|
|
|
4
4
|
module RuleEngine
|
|
5
5
|
# A single rule with a name, condition, action, and priority.
|
|
6
6
|
class Rule
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
7
9
|
# @return [String] the rule name
|
|
8
10
|
attr_reader :name
|
|
9
11
|
|
|
12
|
+
# @return [Boolean] whether the rule is enabled
|
|
13
|
+
attr_accessor :enabled
|
|
14
|
+
|
|
10
15
|
# @param name [String] the rule name
|
|
11
16
|
def initialize(name)
|
|
12
17
|
@name = name
|
|
13
18
|
@priority = 0
|
|
14
19
|
@condition = nil
|
|
15
20
|
@action = nil
|
|
21
|
+
@enabled = true
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
# Set the condition for this rule.
|
|
@@ -50,7 +56,7 @@ module Philiprehberger
|
|
|
50
56
|
def matches?(facts)
|
|
51
57
|
return false unless @condition
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
!!instance_exec(facts, &@condition)
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Execute the action with the given facts.
|
|
@@ -60,7 +66,18 @@ module Philiprehberger
|
|
|
60
66
|
def execute(facts)
|
|
61
67
|
return nil unless @action
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
instance_exec(facts, &@action)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Serialize rule metadata to a hash.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash] rule metadata (name, priority, enabled)
|
|
75
|
+
def to_h
|
|
76
|
+
{
|
|
77
|
+
name: @name,
|
|
78
|
+
priority: @priority,
|
|
79
|
+
enabled: @enabled
|
|
80
|
+
}
|
|
64
81
|
end
|
|
65
82
|
end
|
|
66
83
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'rule_engine/version'
|
|
4
|
+
require_relative 'rule_engine/helpers'
|
|
4
5
|
require_relative 'rule_engine/rule'
|
|
5
6
|
require_relative 'rule_engine/engine'
|
|
6
7
|
|
|
@@ -16,5 +17,33 @@ module Philiprehberger
|
|
|
16
17
|
def self.new(mode: :all, &block)
|
|
17
18
|
Engine.new(mode: mode, &block)
|
|
18
19
|
end
|
|
20
|
+
|
|
21
|
+
# Reconstruct an engine from a serialized hash.
|
|
22
|
+
# The resolver block maps rule names to condition/action implementations.
|
|
23
|
+
#
|
|
24
|
+
# @param data [Hash] serialized engine data from Engine#to_h
|
|
25
|
+
# @yield [name] block that receives a rule name and should configure the rule
|
|
26
|
+
# @return [Engine] the reconstructed engine
|
|
27
|
+
def self.from_h(data, &resolver)
|
|
28
|
+
raise Error, 'resolver block is required' unless resolver
|
|
29
|
+
|
|
30
|
+
mode = (data[:mode] || data['mode'] || :all).to_sym
|
|
31
|
+
engine = Engine.new(mode: mode)
|
|
32
|
+
|
|
33
|
+
rules = data[:rules] || data['rules'] || []
|
|
34
|
+
rules.each do |rule_data|
|
|
35
|
+
name = rule_data[:name] || rule_data['name']
|
|
36
|
+
priority_val = rule_data[:priority] || rule_data['priority'] || 0
|
|
37
|
+
enabled_val = rule_data.key?(:enabled) ? rule_data[:enabled] : rule_data.fetch('enabled', true)
|
|
38
|
+
|
|
39
|
+
r = engine.add_rule(name) do
|
|
40
|
+
priority priority_val
|
|
41
|
+
end
|
|
42
|
+
r.enabled = enabled_val
|
|
43
|
+
resolver.call(r)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
engine
|
|
47
|
+
end
|
|
19
48
|
end
|
|
20
49
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-rule_engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A lightweight rule engine with a declarative DSL for defining conditions
|
|
14
14
|
and actions. Supports priority-based ordering, first-match and all-match evaluation
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- README.md
|
|
25
25
|
- lib/philiprehberger/rule_engine.rb
|
|
26
26
|
- lib/philiprehberger/rule_engine/engine.rb
|
|
27
|
+
- lib/philiprehberger/rule_engine/helpers.rb
|
|
27
28
|
- lib/philiprehberger/rule_engine/rule.rb
|
|
28
29
|
- lib/philiprehberger/rule_engine/version.rb
|
|
29
30
|
homepage: https://github.com/philiprehberger/rb-rule-engine
|