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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 28b08b8646e0f7e2e8345d914abc082a018c9d50
4
- data.tar.gz: f0a53d0a8ac7b90010733cadfb0d7f82f43d0efb
3
+ metadata.gz: cf7d146ecc3d6ebd40de586588fefc52a94d4552
4
+ data.tar.gz: c861e3383026631ec4c2e293555e16c30963bb5f
5
5
  SHA512:
6
- metadata.gz: faabf0c69a4bf2efb60baf50307b6e67661145c9ddc25a0dfd6ac282508e9e529e18a41ada804a8b9a847a3fab149d296190c57661a57a9b325e855fceca61e7
7
- data.tar.gz: acaa6b02d117fd38a6765d122170172346decf05634f3dffaea74818aef871ba559477b5617ef1b31a5594dad3b9e447eaab7a377e8b340820a60d72fbb94719
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
- ## Usage
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
- # Setup a new engine
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
- # Define a bunch of rules: name, value/range, priority, conditions
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
@@ -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?
@@ -1,21 +1,22 @@
1
1
  module Torm
2
- class RulesEngine
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 :verbose, :policies, :conditions_whitelist
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: {}, conditions_whitelist: {}, dirty: false, policies: DEFAULT_POLICIES.dup, verbose: false)
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
- @verbose = verbose
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 = File.read(rules_file)
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 a file.
102
- def save(rules_file: self.class.rules_file)
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
@@ -1,3 +1,3 @@
1
1
  module Torm
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -3,7 +3,7 @@ require 'minitest_helper'
3
3
  describe Torm::RulesEngine do
4
4
  let(:engine) { Torm::RulesEngine.new }
5
5
 
6
- describe '.decide' do
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: torm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wes Oldenbeuving