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 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