philiprehberger-rule_engine 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b150f5bc67b381f5f5a1c88032c5241c4bc83f952c565ceaf5d66baa852d4f65
4
- data.tar.gz: 0fc2ae7a0e84089dcfddee3b80d917201924522c249c320bc80dd6bbc317e9f2
3
+ metadata.gz: 6632ea70fd2a2bd623fdbd89fdd2f1a399254742af0fc9dd737ac3fae82c9e04
4
+ data.tar.gz: 67207e22ad77f60478e22343957f298bd1c431cb5c8410688087b54bb65b1004
5
5
  SHA512:
6
- metadata.gz: 99ab211d1ba3beefd989c2c32444fcc40386d72fdaa90963cbb2ae845021db06ebe4b882843ff8eea76f9773879610f47f71fec26044065b165530ae8d387767
7
- data.tar.gz: 0b3ab9ec81247acaab0f07b7c44add184aa0437cf650f66c760d99666f3797d3049d4730c788e9a280ef4352a7333546878a0105289fa64933d5b76787c83af1
6
+ metadata.gz: 4b32d00eedfedbf1e00d49ec7728594d86b222f047135dfd3acbd3d485ff72900b1271e91ac4c358ee40e54f76ffad646e39a3cf939155c9487ac31a4e321087
7
+ data.tar.gz: 7b9d1b7ef2d0173e1c7385f05b3c3af4db9b7af7c0d1afbc7e6cde4aaa042d47b09fa973509613419bbcf2960ab0749138a2f2318bf842c7321b8bfa6fe45dce
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-10
11
+
12
+ ### Added
13
+ - Rule tagging via `tags:` option on `rule` and `add_rule`
14
+ - `Engine#evaluate(facts, tags:)` and `Engine#dry_run(facts, tags:)` filter by tags (OR logic)
15
+ - `Engine#rules_by_tag(tag)` returns rules with a specific tag
16
+ - Tags included in `Rule#to_h` and restored by `RuleEngine.from_h`
17
+
10
18
  ## [0.3.0] - 2026-04-01
11
19
 
12
20
  ### Added
@@ -48,3 +56,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
48
56
  - First-match mode to stop after first match
49
57
  - Rule evaluation against arbitrary facts
50
58
  - Results with rule name and action output
59
+
60
+ [0.4.0]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.4.0
61
+ [0.3.0]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.3.0
62
+ [0.2.1]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.2.1
63
+ [0.2.0]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.2.0
64
+ [0.1.2]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.1.2
65
+ [0.1.1]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.1.1
66
+ [0.1.0]: https://github.com/philiprehberger/rb-rule-engine/releases/tag/v0.1.0
data/README.md CHANGED
@@ -100,6 +100,30 @@ engine = Philiprehberger::RuleEngine.new do
100
100
  end
101
101
  ```
102
102
 
103
+ ### Rule Tags
104
+
105
+ Organize rules into categories and selectively evaluate subsets:
106
+
107
+ ```ruby
108
+ engine = Philiprehberger::RuleEngine.new do
109
+ rule "validate_age", tags: [:validation] do
110
+ condition { |f| f[:age] >= 18 }
111
+ action { |_| "valid" }
112
+ end
113
+
114
+ rule "apply_discount", tags: [:pricing] do
115
+ condition { |f| f[:premium] }
116
+ action { |_| "discount applied" }
117
+ end
118
+ end
119
+
120
+ engine.evaluate(facts, tags: [:validation]) # only validation rules
121
+ engine.dry_run(facts, tags: [:pricing]) # dry run only pricing
122
+ engine.rules_by_tag(:validation) # list rules with tag
123
+ ```
124
+
125
+ Multiple tags use OR logic — a rule matches if it has any of the specified tags.
126
+
103
127
  ### Dynamic Rule Management
104
128
 
105
129
  ```ruby
@@ -210,7 +234,7 @@ end
210
234
  | `.new(mode:) { }` | Create engine with rule definitions |
211
235
  | `.from_h(data, &resolver)` | Reconstruct engine from serialized hash |
212
236
  | `#rule(name) { }` | Define a rule with condition and action |
213
- | `#evaluate(facts)` | Evaluate rules against facts |
237
+ | `#evaluate(facts, tags: nil)` | Evaluate rules against facts; filter by tags if given |
214
238
  | `#rules` | Array of registered rules |
215
239
  | `#mode` | Current evaluation mode |
216
240
  | `#to_h` | Serialize engine configuration to hash |
@@ -221,7 +245,8 @@ end
221
245
  | `#chain(*rule_names)` | Execute rules sequentially as a pipeline |
222
246
  | `#stats` | Per-rule execution statistics |
223
247
  | `#reset_stats!` | Clear all execution statistics |
224
- | `#dry_run(facts)` | Evaluate rules without executing actions |
248
+ | `#rules_by_tag(tag)` | Return rules with the given tag |
249
+ | `#dry_run(facts, tags: nil)` | Evaluate rules without executing actions |
225
250
  | `#detect_conflicts` | Find rule pairs that could both match |
226
251
  | `#validate_rules` | Check all rules have conditions and actions |
227
252
 
@@ -30,8 +30,8 @@ module Philiprehberger
30
30
  # @param name [String] the rule name
31
31
  # @yield [rule] block for configuring the rule
32
32
  # @return [Rule] the created rule
33
- def rule(name, &block)
34
- r = Rule.new(name)
33
+ def rule(name, tags: [], &block)
34
+ r = Rule.new(name, tags: tags)
35
35
  r.instance_eval(&block) if block
36
36
  @rules << r
37
37
  @stats[name] = new_stat_entry
@@ -41,10 +41,11 @@ module Philiprehberger
41
41
  # Add a rule after engine creation.
42
42
  #
43
43
  # @param name [String] the rule name
44
+ # @param tags [Array<Symbol>] optional tags
44
45
  # @yield [rule] block for configuring the rule
45
46
  # @return [Rule] the created rule
46
- def add_rule(name, &)
47
- rule(name, &)
47
+ def add_rule(name, tags: [], &block)
48
+ rule(name, tags: tags, &block)
48
49
  end
49
50
 
50
51
  # Remove a rule by name.
@@ -82,12 +83,21 @@ module Philiprehberger
82
83
  found.enabled = true
83
84
  end
84
85
 
86
+ # Return rules matching a specific tag.
87
+ #
88
+ # @param tag [Symbol] the tag to filter by
89
+ # @return [Array<Rule>]
90
+ def rules_by_tag(tag)
91
+ @rules.select { |r| r.tags.include?(tag.to_sym) }
92
+ end
93
+
85
94
  # Evaluate all rules against the given facts.
86
95
  #
87
96
  # @param facts [Object] the facts to evaluate
97
+ # @param tags [Array<Symbol>, nil] only evaluate rules with at least one matching tag
88
98
  # @return [Array<Hash>] results with :rule and :result for each matched rule
89
- def evaluate(facts)
90
- sorted = @rules.select(&:enabled).sort_by(&:priority)
99
+ def evaluate(facts, tags: nil)
100
+ sorted = filter_by_tags(@rules.select(&:enabled), tags).sort_by(&:priority)
91
101
  results = []
92
102
 
93
103
  sorted.each do |r|
@@ -139,8 +149,8 @@ module Philiprehberger
139
149
  #
140
150
  # @param facts [Hash] the facts to evaluate
141
151
  # @return [Array<Hash>] matched rules with name and priority
142
- def dry_run(facts)
143
- matched = enabled_rules_sorted.select { |rule| rule.matches?(facts) }
152
+ def dry_run(facts, tags: nil)
153
+ matched = filter_by_tags(enabled_rules_sorted, tags).select { |rule| rule.matches?(facts) }
144
154
  matched = [matched.first].compact if @mode == :first
145
155
  matched.map { |rule| { name: rule.name, priority: rule.priority } }
146
156
  end
@@ -203,6 +213,13 @@ module Philiprehberger
203
213
 
204
214
  private
205
215
 
216
+ def filter_by_tags(rules, tags)
217
+ return rules unless tags
218
+
219
+ tag_syms = tags.map(&:to_sym)
220
+ rules.select { |r| r.tags.intersect?(tag_syms) }
221
+ end
222
+
206
223
  def enabled_rules_sorted
207
224
  @rules.select(&:enabled).sort_by(&:priority)
208
225
  end
@@ -9,12 +9,17 @@ module Philiprehberger
9
9
  # @return [String] the rule name
10
10
  attr_reader :name
11
11
 
12
+ # @return [Array<Symbol>] the rule tags
13
+ attr_reader :tags
14
+
12
15
  # @return [Boolean] whether the rule is enabled
13
16
  attr_accessor :enabled
14
17
 
15
18
  # @param name [String] the rule name
16
- def initialize(name)
19
+ # @param tags [Array<Symbol>] optional tags for grouping
20
+ def initialize(name, tags: [])
17
21
  @name = name
22
+ @tags = tags.map(&:to_sym)
18
23
  @priority = 0
19
24
  @condition = nil
20
25
  @action = nil
@@ -76,7 +81,8 @@ module Philiprehberger
76
81
  {
77
82
  name: @name,
78
83
  priority: @priority,
79
- enabled: @enabled
84
+ enabled: @enabled,
85
+ tags: @tags
80
86
  }
81
87
  end
82
88
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module RuleEngine
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -35,8 +35,9 @@ module Philiprehberger
35
35
  name = rule_data[:name] || rule_data['name']
36
36
  priority_val = rule_data[:priority] || rule_data['priority'] || 0
37
37
  enabled_val = rule_data.key?(:enabled) ? rule_data[:enabled] : rule_data.fetch('enabled', true)
38
+ tags_val = (rule_data[:tags] || rule_data['tags'] || []).map(&:to_sym)
38
39
 
39
- r = engine.add_rule(name) do
40
+ r = engine.add_rule(name, tags: tags_val) do
40
41
  priority priority_val
41
42
  end
42
43
  r.enabled = enabled_val
metadata CHANGED
@@ -1,18 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-rule_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.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-04-02 00:00:00.000000000 Z
11
+ date: 2026-04-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A lightweight rule engine with a declarative DSL for defining conditions
14
- and actions. Supports priority-based ordering, first-match and all-match evaluation
15
- modes, and composable rule definitions.
14
+ and actions. Supports priority-based ordering, rule tagging with selective evaluation,
15
+ first-match and all-match modes, dry run, conflict detection, serialization, chaining,
16
+ and per-rule execution statistics.
16
17
  email:
17
18
  - me@philiprehberger.com
18
19
  executables: []