torm 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 28b08b8646e0f7e2e8345d914abc082a018c9d50
4
+ data.tar.gz: f0a53d0a8ac7b90010733cadfb0d7f82f43d0efb
5
+ SHA512:
6
+ metadata.gz: faabf0c69a4bf2efb60baf50307b6e67661145c9ddc25a0dfd6ac282508e9e529e18a41ada804a8b9a847a3fab149d296190c57661a57a9b325e855fceca61e7
7
+ data.tar.gz: acaa6b02d117fd38a6765d122170172346decf05634f3dffaea74818aef871ba559477b5617ef1b31a5594dad3b9e447eaab7a377e8b340820a60d72fbb94719
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /vendor/bundle
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in torm.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Wes Oldenbeuving
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ # Torm
2
+
3
+ Torm is a rules engine build in Ruby. It is named after [Torm](http://forgottenrealms.wikia.com/wiki/Torm), the Forgotten Realms god of Law.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'torm'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install torm
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ # Setup a new engine
25
+ 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}
37
+ ```
38
+
39
+ ## How rules are evaluated
40
+
41
+ * Policy origins dictate priority.
42
+ * The lowest priority are the defaults. This is our company policy.
43
+ * On top of defaults we can run experiments.
44
+ * The Code of Conduct (usually specific to a country + payment method) overrules our policies and experiments
45
+ * Law (usually specific to a country) overrules everything
46
+ * Rules have a set of zero or more conditions
47
+ * Each condition must be met in order for a rule to be relevant
48
+ * On equal policy level, more specific rules (more conditions) overrule less specific ones. Rationale: "We usually don't do this, except when it's summer."
49
+ * Decisions take a rule and a bunch of environment conditions
50
+ * We gather all rules, then filter irrelevant rules based on environment conditions
51
+ * Because rules are stored in order of priority, the first rule remaining is the one that applies the best.
52
+
53
+ ## Versioning
54
+
55
+ Torm tries to follow Semantic Versioning 2.0.0, this means that given a version number MAJOR.MINOR.PATCH, it will increment the:
56
+
57
+ * MAJOR version when you make incompatible API changes,
58
+ * MINOR version when you add functionality in a backwards-compatible manner, and
59
+ * PATCH version when you make backwards-compatible bug fixes.
60
+
61
+ As long as the MAJOR version is 0, all bets are off as the library has not been declared stable yet.
62
+ In this case, treat MINOR version changes as a sign to check the changelog for breaking chagnes.
63
+
64
+ ## Contributing
65
+
66
+ 1. Fork it ( https://github.com/narnach/torm/fork )
67
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
68
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
69
+ 4. Push to the branch (`git push origin my-new-feature`)
70
+ 5. Create a new Pull Request
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task :default => :test
10
+
@@ -0,0 +1,31 @@
1
+ # External dependencies
2
+ require 'multi_json'
3
+
4
+ # No internal dependencies
5
+ require 'torm/version'
6
+ require 'torm/tools'
7
+
8
+ # This is where the magic happens
9
+ require 'torm/rules_engine'
10
+
11
+ module Torm
12
+ class << self
13
+ include Tools
14
+
15
+ attr_accessor :instance, :default_rules_file
16
+
17
+ def instance
18
+ @instance ||= RulesEngine.load || RulesEngine.new
19
+ end
20
+
21
+ def default_rules_file
22
+ @default_rules_file ||= File.expand_path('tmp/rules.json')
23
+ end
24
+
25
+ # Load an engine with the current rules, yield it (to add rules) and then save it if rules were added.
26
+ def set_defaults(engine: instance)
27
+ yield engine
28
+ engine.save if engine.dirty?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,187 @@
1
+ module Torm
2
+ class RulesEngine
3
+ # Policies (priorities) in order of important -> least important.
4
+ DEFAULT_POLICIES = [:law, :coc, :experiment, :default].freeze
5
+
6
+ attr_reader :rules
7
+ attr_accessor :verbose, :policies, :conditions_whitelist
8
+ attr_accessor :dirty
9
+
10
+ def initialize(rules: {}, conditions_whitelist: {}, dirty: false, policies: DEFAULT_POLICIES.dup, verbose: false)
11
+ @rules = rules
12
+ @conditions_whitelist = conditions_whitelist
13
+ @dirty = dirty
14
+ @policies = policies
15
+ @verbose = verbose
16
+ end
17
+
18
+ # Have any rules been added since the last save or load?
19
+ def dirty?
20
+ @dirty
21
+ end
22
+
23
+ # Add a new rule.
24
+ # Will mark the engine as dirty when a rules was added.
25
+ # @return [Torm::RulesEngine] (self) Returns the engine that rules were added to.
26
+ def add_rule(name, value, policy, conditions={})
27
+ raise "Illegal policy: #{policy.inspect}, must be one of: #{policies.inspect}" unless policies.include?(policy)
28
+ rules_array = rules_for(name)
29
+ value = { minimum: value.min, maximum: value.max } if Range === value
30
+ new_rule = { value: value.freeze, policy: policy, conditions: conditions.freeze }.freeze
31
+ unless rules_array.include?(new_rule)
32
+ rules_array << new_rule
33
+ # Sort rules so that the highest policy level is sorted first and then the most complex rule before the more general ones
34
+ rules_array.sort_by! { |rule| [policies.index(rule[:policy]), -rule[:conditions].size] }
35
+ conditions_whitelist_for(name).merge conditions.keys
36
+ @dirty = true
37
+ end
38
+ self
39
+ end
40
+
41
+ def decide(name, environment={})
42
+ raise "Unknown rule: #{name.inspect}" unless rules.has_key?(name)
43
+ environment = Torm.symbolize_keys(environment)
44
+ decision_environment = Torm.slice(environment, *conditions_whitelist_for(name))
45
+ answer = make_decision(name, decision_environment)
46
+ #Rails.logger.debug "DECISION: #{answer.inspect} (#{name.inspect} -> #{environment.inspect})"
47
+ answer
48
+ end
49
+
50
+ def as_hash
51
+ {
52
+ policies: policies,
53
+ rules: rules
54
+ }
55
+ end
56
+
57
+ def to_json
58
+ MultiJson.dump(as_hash)
59
+ end
60
+
61
+ # Load an engine from JSON. This means we can export rules engines across systems: store rules in 1 place, run them 'everywhere' at native speed.
62
+ # Due to the high number of symbols we use, we have to convert the JSON string data for each rule on import.
63
+ # Good thing: we should only have to do this once on boot.
64
+ def self.from_json(json)
65
+ dump = MultiJson.load(json)
66
+ data = {
67
+ policies: dump['policies'].map(&:to_sym),
68
+ }
69
+ engine = new(data)
70
+ dump['rules'].each do |name, rules|
71
+ rules.each do |rule|
72
+ value = rule['value']
73
+ value = Torm.symbolize_keys(value) if Hash === value
74
+ policy = rule['policy'].to_sym
75
+ conditions = Torm.symbolize_keys(rule['conditions'])
76
+ engine.add_rule(name, value, policy, conditions)
77
+ end
78
+ end
79
+ engine.dirty = false
80
+ engine
81
+ end
82
+
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
+ # Load rules from a file and create a new engine for it.
89
+ # Note: this does *not* replace the Torm::RulesEngine.instance, you have to do this yourself if required.
90
+ #
91
+ # @return [Torm::RulesEngine] A new engine with the loaded rules
92
+ def self.load(rules_file: Torm.default_rules_file)
93
+ if File.exist?(rules_file)
94
+ json = File.read(rules_file)
95
+ self.from_json(json)
96
+ else
97
+ nil
98
+ end
99
+ end
100
+
101
+ # Save the current rules to a file.
102
+ def save(rules_file: self.class.rules_file)
103
+ Torm.atomic_save(rules_file, to_json + "\n")
104
+ @dirty = false
105
+ nil
106
+ end
107
+
108
+ private
109
+
110
+ # TODO: Refactor once useful
111
+ def make_decision(name, environment={})
112
+ # Fetch all rules for this decision. Duplicate to allow us to manipulate the Array with #reject!
113
+ relevant_rules = rules_for(name).dup
114
+
115
+ # Filter through all rules. Eliminate the rules not matching our environment.
116
+ relevant_rules.reject! do |rule|
117
+ reject_rule = false
118
+ rule[:conditions].each do |condition, value|
119
+ if environment.has_key?(condition)
120
+ if environment[condition] == value
121
+ # This rule condition applies to our environment, so evaluate the next condition
122
+ next
123
+ else
124
+ # The rule has a condition which is a mismatch with our environment, so it does not apply
125
+ reject_rule = true
126
+ break
127
+ end
128
+ else
129
+ # The rule is more specific than our environment, so it does not apply
130
+ reject_rule = true
131
+ break
132
+ end
133
+ end
134
+ reject_rule
135
+ end
136
+
137
+ # Check the remaining rules in order of priority
138
+ result = nil
139
+ relevant_rules.each do |rule|
140
+ rule_value = rule[:value]
141
+ case rule_value
142
+ when Hash
143
+ result ||= rule_value.dup
144
+ # Lower-priority rules can decrease a maximum value, but not increase it
145
+ if rule_value[:maximum]
146
+ if result[:maximum]
147
+ result[:maximum] = rule_value[:maximum] if rule_value[:maximum] < result[:maximum]
148
+ else
149
+ result[:maximum] = rule_value[:maximum]
150
+ end
151
+ end
152
+
153
+ # Lower-priority rules can increase a minimum value, but not decrease it
154
+ if rule_value[:minimum]
155
+ if result[:minimum]
156
+ result[:minimum] = rule_value[:minimum] if rule_value[:minimum] > result[:minimum]
157
+ else
158
+ result[:minimum] = rule_value[:minimum]
159
+ end
160
+ end
161
+
162
+ # Minimum above maximum is invalid, so reject the result and return nil
163
+ return nil if result[:minimum] && result[:maximum] && result[:minimum] > result[:maximum]
164
+ else
165
+ return rule_value
166
+ end
167
+ end
168
+ result
169
+ end
170
+
171
+ def conditions_whitelist_for(name)
172
+ conditions_whitelist[name] ||= Set.new
173
+ end
174
+
175
+ def rules_for(name)
176
+ rules[name] ||= []
177
+ 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
+ end
187
+ end
@@ -0,0 +1,24 @@
1
+ module Torm::Tools
2
+ # Save data to a temporary file, then rename it to the final file.
3
+ def atomic_save(target_file, data)
4
+ tmp_file = target_file + ".#{Process.pid}.tmp"
5
+ File.open(tmp_file, 'w') { |f| f.write data }
6
+ File.rename(tmp_file, target_file)
7
+ end
8
+
9
+ # Return a new Hash with all keys symbolized
10
+ def symbolize_keys(hash)
11
+ symbolized_hash = {}
12
+ hash.each { |k, v| symbolized_hash[k.to_sym] = v }
13
+ symbolized_hash
14
+ end
15
+
16
+ # Return a new Hash with only they white listed keys
17
+ def slice(hash, *white_listed_keys)
18
+ sliced_hash = {}
19
+ white_listed_keys.each do |key|
20
+ sliced_hash[key] = hash[key] if hash.has_key?(key)
21
+ end
22
+ sliced_hash
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module Torm
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'torm'
3
+
4
+ require 'minitest/spec'
5
+ require 'minitest/autorun'
@@ -0,0 +1,143 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Torm::RulesEngine do
4
+ let(:engine) { Torm::RulesEngine.new }
5
+
6
+ describe '.decide' do
7
+ describe 'basic decision making' do
8
+ before(:each) do
9
+ engine.add_rule 'show unsubscribe link', false, :default
10
+ engine.add_rule 'show unsubscribe link', true, :coc, country: 'FR'
11
+ end
12
+
13
+ it 'should pick the default without any conditions' do
14
+ # This means it should *not* apply rules which only apply to conditions we don't have
15
+ engine.decide('show unsubscribe link').must_equal false
16
+ end
17
+
18
+ it 'should pick a specific rule over the default' do
19
+ engine.decide('show unsubscribe link', country: 'FR').must_equal true
20
+ end
21
+
22
+ it 'should ignore conditions which do not match' do
23
+ engine.decide('show unsubscribe link', country: 'NL', happy: true, season: :summer).must_equal false
24
+
25
+ engine.add_rule 'show unsubscribe link', true, :default, season: :summer
26
+ engine.decide('show unsubscribe link', country: 'NL', happy: true, season: :summer).must_equal true
27
+ end
28
+ end
29
+
30
+ describe 'range behavior' do
31
+ it 'should support maximum' do
32
+ engine.add_rule 'FSK level', { maximum: 18 }, :default
33
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, referrer: 'google'
34
+ engine.add_rule 'FSK level', { maximum: 12 }, :coc, referrer: 'disney'
35
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
36
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'DE'
37
+ engine.add_rule 'FSK level', { maximum: 12 }, :law, country: 'TR'
38
+
39
+ # Default
40
+ engine.decide('FSK level').must_equal({ maximum: 18 })
41
+ # Default
42
+ engine.decide('FSK level', country: 'NL').must_equal({ maximum: 18 })
43
+ # CoC for DE
44
+ engine.decide('FSK level', country: 'DE').must_equal({ maximum: 16 })
45
+ # ref=google
46
+ engine.decide('FSK level', country: 'NL', referrer: 'google').must_equal({ maximum: 16 })
47
+
48
+ # should start with 12+ for TR, and not modify it to 16+ for ref=google
49
+ engine.decide('FSK level', country: 'TR', referrer: 'google').must_equal({ maximum: 12 })
50
+
51
+ # should start with 16+ for FR, but tighten it to 12+ for ref=disney
52
+ engine.decide('FSK level', country: 'FR', referrer: 'disney').must_equal({ maximum: 12 })
53
+ end
54
+
55
+ it 'should support minimum' do
56
+ engine.add_rule 'FSK level', { minimum: 1 }, :default
57
+ engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
58
+ engine.add_rule 'FSK level', { minimum: 16 }, :default, softcore: true
59
+ engine.add_rule 'FSK level', { minimum: 18 }, :default, hardcore: true
60
+
61
+
62
+ # Query individual FSK thresholds
63
+ engine.decide('FSK level').must_equal({ minimum: 1 })
64
+ engine.decide('FSK level', sexy: true).must_equal({ minimum: 12 })
65
+ engine.decide('FSK level', softcore: true).must_equal({ minimum: 16 })
66
+ engine.decide('FSK level', hardcore: true).must_equal({ minimum: 18 })
67
+
68
+ # Combine multiple FSK threshold
69
+ #engine.verbose = true
70
+ engine.decide('FSK level', hardcore: true, sexy: true).must_equal({ minimum: 18 })
71
+ engine.decide('FSK level', softcore: true, sexy: true).must_equal({ minimum: 16 })
72
+ end
73
+
74
+ it 'should combine minimum and maximum' do
75
+ engine.add_rule 'FSK level', { minimum: 1, maximum: 18 }, :default
76
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, referrer: 'google'
77
+ engine.add_rule 'FSK level', { maximum: 12 }, :coc, referrer: 'disney'
78
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
79
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'DE'
80
+ engine.add_rule 'FSK level', { maximum: 12 }, :law, country: 'TR'
81
+
82
+ engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
83
+ engine.add_rule 'FSK level', { minimum: 16 }, :default, softcore: true
84
+ engine.add_rule 'FSK level', { minimum: 18 }, :default, hardcore: true
85
+
86
+ engine.decide('FSK level', country: 'NL').must_equal({ minimum: 1, maximum: 18 })
87
+ engine.decide('FSK level', country: 'FR').must_equal({ minimum: 1, maximum: 16 })
88
+ engine.decide('FSK level', country: 'FR', sexy: true).must_equal({ minimum: 12, maximum: 16 })
89
+
90
+ # Return nil because of conflicting requirements: softcore is 16+, TR is 12-
91
+ engine.decide('FSK level', country: 'TR', softcore: true).must_equal nil
92
+ end
93
+
94
+ it 'should allow range syntax' do
95
+ engine.add_rule 'FSK level', 1..18, :default
96
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
97
+ engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
98
+
99
+ engine.decide('FSK level', country: 'NL').must_equal({ minimum: 1, maximum: 18 })
100
+ engine.decide('FSK level', country: 'FR', sexy: true).must_equal({ minimum: 12, maximum: 16 })
101
+ end
102
+ end
103
+ end
104
+
105
+ describe '#to_json' do
106
+ it 'should export all rules as a Hash' do
107
+ engine.add_rule 'FSK level', 1..18, :default
108
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
109
+ engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
110
+
111
+ rule_hash = {
112
+ policies: [:law, :coc, :experiment, :default],
113
+ rules: {
114
+ 'FSK level' => [
115
+ { policy: :coc, value: { maximum: 16 }, conditions: { country: 'FR' } },
116
+ { policy: :default, value: { minimum: 12 }, conditions: { sexy: true } },
117
+ { policy: :default, value: { minimum: 1, maximum: 18 }, conditions: {} },
118
+ ]
119
+ }
120
+ }
121
+ engine.as_hash.must_equal rule_hash
122
+
123
+ engine.to_json.must_equal MultiJson.dump(engine.as_hash)
124
+ end
125
+ end
126
+
127
+ describe '.from_json' do
128
+ it 'should return a working rules engine' do
129
+ engine.add_rule 'FSK level', 1..18, :default
130
+ engine.add_rule 'FSK level', { maximum: 16 }, :coc, country: 'FR'
131
+ engine.add_rule 'FSK level', { minimum: 12 }, :default, sexy: true
132
+
133
+ engine.decide('FSK level', country: 'NL').must_equal({ minimum: 1, maximum: 18 })
134
+ engine.decide('FSK level', country: 'FR', sexy: true).must_equal({ minimum: 12, maximum: 16 })
135
+
136
+ engine2 = Torm::RulesEngine.from_json(engine.to_json)
137
+ engine2.as_hash.must_equal engine.as_hash
138
+
139
+ engine2.decide('FSK level', country: 'NL').must_equal({ minimum: 1, maximum: 18 })
140
+ engine2.decide('FSK level', country: 'FR', sexy: true).must_equal({ minimum: 12, maximum: 16 })
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,38 @@
1
+ require 'minitest_helper'
2
+
3
+ # This module is included in Torm, so we use that to test its behavior.
4
+ describe Torm::Tools do
5
+ describe '#atomic_save' do
6
+ tmp_file = 'tmp/atomic.test'
7
+ after do
8
+ File.delete(tmp_file) if File.exist?(tmp_file)
9
+ Dir.delete('tmp') if File.exist?('tmp')
10
+ end
11
+
12
+ it 'should save data to a file' do
13
+ Dir.mkdir('tmp')
14
+ Torm.atomic_save(tmp_file, 'test')
15
+ assert File.exist?(tmp_file)
16
+ File.read(tmp_file).must_equal 'test'
17
+ end
18
+ end
19
+
20
+ describe '#symbolize_keys' do
21
+ it 'should convert string keys to symbols' do
22
+ Torm.symbolize_keys({ 'a' => 'b', :c => :d }).must_equal({ a: 'b', c: :d })
23
+ end
24
+ end
25
+
26
+ describe '#slice' do
27
+ it 'should return a hash with only the white listed keys' do
28
+ hash = {
29
+ foo: 1,
30
+ baz: 3
31
+ }
32
+ Torm.slice(hash, :foo, :bar).must_equal({ foo: 1 })
33
+
34
+ # Ensure we did not modify the original Hash
35
+ hash[:baz].must_equal 3
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Torm do
4
+ describe '.instance' do
5
+ it 'should load an Engine with default rules when there is a default rules file'
6
+ it 'should instantiate a new Engine when there are no default rules'
7
+ it 'should return the same instance when one has already been accessed'
8
+ end
9
+
10
+ describe '.set_defaults' do
11
+ it 'should yield the engine'
12
+ it 'should save the engine when dirty after running the block'
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'torm/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'torm'
8
+ spec.version = Torm::VERSION
9
+ spec.authors = ['Wes Oldenbeuving']
10
+ spec.email = ['narnach@gmail.com']
11
+ spec.summary = %q{Ruby rules engine}
12
+ spec.description = %q{Rules engine. Named after the Forgotten Realms god of Law.}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ # Ruby 2.1 introduces required named keywords
22
+ spec.required_ruby_version = '>= 2.1.0'
23
+
24
+ # MultiJson follow Semantic Versioning, so any 1.x should work.
25
+ spec.add_dependency 'multi_json', '~> 1.0'
26
+
27
+ # Defaults from generating the gemspec
28
+ spec.add_development_dependency 'bundler', '~> 1.7'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'minitest'
31
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: torm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Wes Oldenbeuving
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: multi_json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Rules engine. Named after the Forgotten Realms god of Law.
70
+ email:
71
+ - narnach@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/torm.rb
83
+ - lib/torm/rules_engine.rb
84
+ - lib/torm/tools.rb
85
+ - lib/torm/version.rb
86
+ - test/minitest_helper.rb
87
+ - test/torm/rules_engine_test.rb
88
+ - test/torm/tools_test.rb
89
+ - test/torm_test.rb
90
+ - torm.gemspec
91
+ homepage: ''
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 2.1.0
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.4.1
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Ruby rules engine
115
+ test_files:
116
+ - test/minitest_helper.rb
117
+ - test/torm/rules_engine_test.rb
118
+ - test/torm/tools_test.rb
119
+ - test/torm_test.rb