torm 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|