torm 0.0.1 → 0.1.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/README.md +27 -13
- data/lib/torm.rb +4 -0
- data/lib/torm/rules_engine.rb +45 -26
- data/lib/torm/version.rb +1 -1
- data/test/torm/rules_engine_test.rb +16 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf7d146ecc3d6ebd40de586588fefc52a94d4552
|
4
|
+
data.tar.gz: c861e3383026631ec4c2e293555e16c30963bb5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d55a9a923f5fb23af03694b42e01c6bad1c295f4e0529be376832c71b6e9054ab2d4816c4cf99fbe22dfb729ea832c745b74a23e1909da904eee0e493606eb72
|
7
|
+
data.tar.gz: 43d60da6351014ab2576d994c60333e17749883801fa1aaadc8078166860120abadbc7e45254a649b8130933f23ff747ced466fab8f744a883575af2da4bde93
|
data/README.md
CHANGED
@@ -18,22 +18,36 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
$ gem install torm
|
20
20
|
|
21
|
-
##
|
21
|
+
## Example in a (Rails) app context
|
22
|
+
|
23
|
+
Load the rules engine, define defaults so new rules get saved.
|
22
24
|
|
23
25
|
```ruby
|
24
|
-
#
|
26
|
+
# Set a custom rules file before accessing the default rules engine.
|
27
|
+
Torm.default_rules_file = Rails.root.join('tmp/my_rules.json').to_s
|
28
|
+
|
29
|
+
# Torm.set_defaults will load an engine if a rules file exists, otherwise you get an empty engine.
|
30
|
+
# Add rules, then after the block it will automatically save the rules file when new rules were changed.
|
31
|
+
Torm.set_defaults do |engine|
|
32
|
+
# Add a new rule named 'Happy', with a 'default' policy value of true
|
33
|
+
engine.add_rules 'Happy', true, :default do |rule|
|
34
|
+
# Add a variant on the 'Happy' rule: we're not happy when it rains
|
35
|
+
rule.variant false, :default, rain: true
|
36
|
+
# Another variant. Due to the abundance of rain, in Great Britain the law dictates you're still happy when it rains.
|
37
|
+
rule.variant true, :law, rain: true, country: 'GB'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Torm.instance holds the default engine used by Torm.set_defaults, so we can use it for making decisions.
|
42
|
+
Torm.instance.decide('Happy', country: 'NL') # => true
|
43
|
+
Torm.instance.decide('Happy', country: 'NL', rain: true) # => false
|
44
|
+
Torm.instance.decide('Happy', country: 'GB', rain: true) # => true
|
45
|
+
|
46
|
+
|
47
|
+
# If you need more rules engines, instantiate a non-global engine when you need one.
|
25
48
|
engine = Torm::RulesEngine.new
|
26
|
-
|
27
|
-
|
28
|
-
# Each rule should at least have a default value without any conditions.
|
29
|
-
# Default priorities: :law > :coc > :experiment > :default
|
30
|
-
engine.add_rule 'FSK level', 1..18, :default
|
31
|
-
engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
|
32
|
-
engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
|
33
|
-
|
34
|
-
# Let the engine decide which rules apply and how they intersect
|
35
|
-
engine.decide('FSK level', country: 'NL') # => {minimum: 1, maximum: 18}
|
36
|
-
engine.decide('FSK level', country: 'FR', sexy: true) # => {minimum: 12, maximum: 16}
|
49
|
+
engine.add_rule 'Happy', true, :default
|
50
|
+
engine.decide('Happy', country: 'NL') # => true
|
37
51
|
```
|
38
52
|
|
39
53
|
## How rules are evaluated
|
data/lib/torm.rb
CHANGED
@@ -14,15 +14,19 @@ module Torm
|
|
14
14
|
|
15
15
|
attr_accessor :instance, :default_rules_file
|
16
16
|
|
17
|
+
# @return [Torm::RulesEngine] Singleton RulesEngine
|
17
18
|
def instance
|
18
19
|
@instance ||= RulesEngine.load || RulesEngine.new
|
19
20
|
end
|
20
21
|
|
22
|
+
# @return [String] Path where the default rules can be stored
|
21
23
|
def default_rules_file
|
22
24
|
@default_rules_file ||= File.expand_path('tmp/rules.json')
|
23
25
|
end
|
24
26
|
|
25
27
|
# Load an engine with the current rules, yield it (to add rules) and then save it if rules were added.
|
28
|
+
#
|
29
|
+
# @yield [Torm::RulesEngine]
|
26
30
|
def set_defaults(engine: instance)
|
27
31
|
yield engine
|
28
32
|
engine.save if engine.dirty?
|
data/lib/torm/rules_engine.rb
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
module Torm
|
2
|
-
|
2
|
+
class RulesEngine
|
3
3
|
# Policies (priorities) in order of important -> least important.
|
4
4
|
DEFAULT_POLICIES = [:law, :coc, :experiment, :default].freeze
|
5
5
|
|
6
|
-
attr_reader :rules
|
7
|
-
attr_accessor :
|
8
|
-
attr_accessor :dirty
|
6
|
+
attr_reader :rules, :conditions_whitelist
|
7
|
+
attr_accessor :policies
|
8
|
+
attr_accessor :dirty, :rules_file
|
9
9
|
|
10
|
-
def initialize(rules: {},
|
10
|
+
def initialize(rules: {}, dirty: false, policies: DEFAULT_POLICIES.dup, rules_file: Torm.default_rules_file)
|
11
11
|
@rules = rules
|
12
|
-
@conditions_whitelist = conditions_whitelist
|
13
12
|
@dirty = dirty
|
14
13
|
@policies = policies
|
15
|
-
@
|
14
|
+
@rules_file = rules_file
|
15
|
+
@conditions_whitelist = {}
|
16
16
|
end
|
17
17
|
|
18
18
|
# Have any rules been added since the last save or load?
|
19
|
+
# @return [true, false]
|
19
20
|
def dirty?
|
20
21
|
@dirty
|
21
22
|
end
|
@@ -38,15 +39,42 @@ module Torm
|
|
38
39
|
self
|
39
40
|
end
|
40
41
|
|
42
|
+
# Simple helper class to add the block DSL to add_rules
|
43
|
+
class RuleVariationHelper
|
44
|
+
def initialize(engine, name)
|
45
|
+
@engine = engine
|
46
|
+
@name = name
|
47
|
+
end
|
48
|
+
|
49
|
+
def variation(value, policy, **conditions)
|
50
|
+
@engine.add_rule(@name, value, policy, conditions)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_rules(name, value, policy)
|
55
|
+
# Add the default rule
|
56
|
+
add_rule(name, value, policy)
|
57
|
+
|
58
|
+
rule_variation = RuleVariationHelper.new(self, name)
|
59
|
+
yield rule_variation if block_given?
|
60
|
+
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Evaluate a rule and return its result. Depending on the rule, different values are returned.
|
65
|
+
#
|
66
|
+
# @raise [RuntimeError] Raise when the rule is not defined.
|
41
67
|
def decide(name, environment={})
|
42
68
|
raise "Unknown rule: #{name.inspect}" unless rules.has_key?(name)
|
43
69
|
environment = Torm.symbolize_keys(environment)
|
44
70
|
decision_environment = Torm.slice(environment, *conditions_whitelist_for(name))
|
45
71
|
answer = make_decision(name, decision_environment)
|
46
|
-
#Rails.logger.debug "DECISION: #{answer.inspect} (#{name.inspect} -> #{environment.inspect})"
|
47
72
|
answer
|
48
73
|
end
|
49
74
|
|
75
|
+
# Return a hash with all rules and policies, useful for serialisation.
|
76
|
+
#
|
77
|
+
# @return [Hash]
|
50
78
|
def as_hash
|
51
79
|
{
|
52
80
|
policies: policies,
|
@@ -54,6 +82,9 @@ module Torm
|
|
54
82
|
}
|
55
83
|
end
|
56
84
|
|
85
|
+
# Serialise the data from +as_hash+.
|
86
|
+
#
|
87
|
+
# @return [String]
|
57
88
|
def to_json
|
58
89
|
MultiJson.dump(as_hash)
|
59
90
|
end
|
@@ -80,26 +111,23 @@ module Torm
|
|
80
111
|
engine
|
81
112
|
end
|
82
113
|
|
83
|
-
# Where we store the rules file.
|
84
|
-
def self.rules_file
|
85
|
-
Rails.root.join('tmp', 'rules.json').to_s
|
86
|
-
end
|
87
|
-
|
88
114
|
# Load rules from a file and create a new engine for it.
|
89
115
|
# Note: this does *not* replace the Torm::RulesEngine.instance, you have to do this yourself if required.
|
90
116
|
#
|
91
117
|
# @return [Torm::RulesEngine] A new engine with the loaded rules
|
92
118
|
def self.load(rules_file: Torm.default_rules_file)
|
93
119
|
if File.exist?(rules_file)
|
94
|
-
json
|
95
|
-
self.from_json(json)
|
120
|
+
json = File.read(rules_file)
|
121
|
+
engine = self.from_json(json)
|
122
|
+
engine.rules_file = rules_file
|
123
|
+
engine
|
96
124
|
else
|
97
125
|
nil
|
98
126
|
end
|
99
127
|
end
|
100
128
|
|
101
|
-
# Save the current rules to
|
102
|
-
def save
|
129
|
+
# Save the current rules to the file.
|
130
|
+
def save
|
103
131
|
Torm.atomic_save(rules_file, to_json + "\n")
|
104
132
|
@dirty = false
|
105
133
|
nil
|
@@ -107,7 +135,6 @@ module Torm
|
|
107
135
|
|
108
136
|
private
|
109
137
|
|
110
|
-
# TODO: Refactor once useful
|
111
138
|
def make_decision(name, environment={})
|
112
139
|
# Fetch all rules for this decision. Duplicate to allow us to manipulate the Array with #reject!
|
113
140
|
relevant_rules = rules_for(name).dup
|
@@ -175,13 +202,5 @@ module Torm
|
|
175
202
|
def rules_for(name)
|
176
203
|
rules[name] ||= []
|
177
204
|
end
|
178
|
-
|
179
|
-
def puts(message)
|
180
|
-
Kernel.puts(message) if verbose
|
181
|
-
end
|
182
|
-
|
183
|
-
def pp(object)
|
184
|
-
Kernel.pp(object) if verbose
|
185
|
-
end
|
186
205
|
end
|
187
206
|
end
|
data/lib/torm/version.rb
CHANGED
@@ -3,7 +3,7 @@ require 'minitest_helper'
|
|
3
3
|
describe Torm::RulesEngine do
|
4
4
|
let(:engine) { Torm::RulesEngine.new }
|
5
5
|
|
6
|
-
describe '
|
6
|
+
describe '#add_rule and #decide' do
|
7
7
|
describe 'basic decision making' do
|
8
8
|
before(:each) do
|
9
9
|
engine.add_rule 'show unsubscribe link', false, :default
|
@@ -100,6 +100,21 @@ describe Torm::RulesEngine do
|
|
100
100
|
engine.decide('FSK level', country: 'FR', sexy: true).must_equal({ minimum: 12, maximum: 16 })
|
101
101
|
end
|
102
102
|
end
|
103
|
+
|
104
|
+
describe '#add_rules block syntax with #variation' do
|
105
|
+
it 'should just work' do
|
106
|
+
engine.add_rules 'Happy', true, :default do |rule|
|
107
|
+
# Nobody likes rain...
|
108
|
+
rule.variation false, :default, rain: true
|
109
|
+
# ...except for the Brits :-)
|
110
|
+
rule.variation true, :law, rain: true, country: 'GB'
|
111
|
+
end
|
112
|
+
|
113
|
+
assert engine.decide('Happy')
|
114
|
+
assert !engine.decide('Happy', rain: true)
|
115
|
+
assert engine.decide('Happy', rain: true, country: 'GB')
|
116
|
+
end
|
117
|
+
end
|
103
118
|
end
|
104
119
|
|
105
120
|
describe '#to_json' do
|