torm 0.0.1

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