strategic 0.9.1 → 1.0.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
  SHA256:
3
- metadata.gz: 2d5c5c2a7b15c1c3e826a37c64b0b4b90f8b4a1dd8d93aaeb5f6c81c796faa93
4
- data.tar.gz: ef054a37db19f752d92e2344f7da9f6269732bcf897babcce4c756e426ca90e2
3
+ metadata.gz: f8ddc7e63dc608ac0a7e97ebb9110431c9a490e7ec07abed6936bb347b6105e8
4
+ data.tar.gz: 4ca98f38d3933fc5f33ac460dc2767a2769cfbe0a870c9f48bfb427ca2a4c1c4
5
5
  SHA512:
6
- metadata.gz: 18fe37a88abab52119d44e03402ea2c005c3235d8709b3802dca8cebaf863af712bbc62c68bed47940ca7846d310135a685e416bc94c4c4f7d6984094ab5071b
7
- data.tar.gz: 959739994722aa145afa46180cad9ef556fe27e57f39e9b49b631074dd788b2cfcc60dda26f564f8d09a2ce94c1100dc6d5994718d3b1421599dbc0665c1bbee
6
+ metadata.gz: '067224647109cf8198006ddce287755a1ec9997ff67fc8a3099d4f172ba6549a8e9ef4f8bf285d44996aa3a5d6ef4949892dffab5a1d26918d17cdc27cbd6e0b'
7
+ data.tar.gz: 1046857c6fd46e0615ab559bbb4e84bc8ba70f7d7995bd621e7d182316d603d22e4d99e2fefc1adce3d2682769519e0cc9189bef7ce614d0aca8354271994e05
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Improve design to better match the authentic Gang of Four Strategy Pattern with `Strategic::Strategy` module, removing the need for inheritance.
6
+ - `#strategy=`/`#strategy` enable setting/getting strategy on model
7
+ - `#context` enables getting strategic model instance on strategy just as per the GOF Design Pattern
8
+ - `default_strategy` class body method to set default strategy
9
+ - Filter strategies by ones ending with `Strategy` in class name
10
+
3
11
  ## 0.9.1
4
12
 
5
13
  - `strategy_name` returns parsed strategy name of current strategy class
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Strategic 0.9.1
1
+ # Strategic 1.0.0
2
2
  ## Painless Strategy Pattern in Ruby and Rails
3
3
  [![Gem Version](https://badge.fury.io/rb/strategic.svg)](http://badge.fury.io/rb/strategic)
4
4
  [![rspec](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
@@ -7,9 +7,8 @@
7
7
 
8
8
  `if`/`case` conditionals can get really hairy in highly sophisticated business domains.
9
9
  Object-oriented inheritance helps remedy the problem, but dumping all
10
- logic variations in subclasses can cause a maintenance nightmare.
11
- Thankfully, the Strategy Pattern as per the [Gang of Four book](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) solves the problem by externalizing logic to
12
- separate classes outside the domain models.
10
+ logic variations in domain model subclasses can cause a maintenance nightmare.
11
+ Thankfully, the Strategy Pattern as per the [Gang of Four book](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) solves the problem by externalizing logic via composition to separate classes outside the domain models.
13
12
 
14
13
  Still, there are a number of challenges with "repeated implementation" of the Strategy Pattern:
15
14
  - Making domain models aware of newly added strategies without touching their
@@ -27,7 +26,9 @@ code (Open/Closed Principle).
27
26
 
28
27
  `Strategic` enables you to make any existing domain model "strategic",
29
28
  externalizing all logic concerning algorithmic variations into separate strategy
30
- classes that are easy to find, maintain and extend while honoring the Open/Closed Principle.
29
+ classes that are easy to find, maintain and extend while honoring the Open/Closed Principle and avoiding conditionals.
30
+
31
+ In summary, if you make a class called `TaxCalculator` strategic by including the `Strategic` mixin module, now you are able to drop strategies under the `tax_calculator` directory sitting next to the class (e.g. `tax_calculator/us_strategy.rb`, `tax_calculator/canada_strategy.rb`) while gaining extra [API](#api) methods to grab strategy names to present in a user interface (`.strategy_names`), set a strategy (`#strategy=(strategy_name)`), and/or instantiate `TaxCalculator` directly with a strategy from the get-go (`.new_with_strategy(strategy_name, *initialize_args)`). Finally, you can simply invoke strategy methods on the main strategic model (e.g. `tax_calculator.tax_for(39.78)`).
31
32
 
32
33
  ### Example
33
34
 
@@ -40,71 +41,74 @@ alt="Strategic Example" />
40
41
  class TaxCalculator
41
42
  include Strategic
42
43
 
43
- def tax_for(amount)
44
- amount * 0.09
45
- end
44
+ # strategies may implement a tax_for(amount) method
46
45
  end
47
46
  ```
48
47
 
49
48
  2. Now, you can add strategies under this directory without having to modify the original class: `tax_calculator`
50
49
 
51
- 3. Add strategy classes under the namespace matching the original class name (`TaxCalculator`) and extending the original class (`TaxCalculator`) just to take advantage of default logic in it:
50
+ 3. Add strategy classes having names ending with `Strategy` by convention (e.g. `UsStrategy`) under the namespace matching the original class name (`TaxCalculator::` as in `tax_calculator/us_strategy.rb` representing `TaxCalculator::UsStrategy`) and including the module (`Strategic::Strategy`):
51
+
52
+ All strategies get access to their context (strategic model instance), which they can use in their logic.
52
53
 
53
54
  ```ruby
54
- class TaxCalculator::UsStrategy < TaxCalculator
55
- def initialize(state)
56
- @state = state
57
- end
55
+ class TaxCalculator::UsStrategy
56
+ include Strategic::Strategy
57
+
58
58
  def tax_for(amount)
59
- amount * state_rate
59
+ amount * state_rate(context.state)
60
60
  end
61
- # ... more code follows
61
+ # ... other strategy methods follow
62
62
  end
63
63
 
64
- class TaxCalculator::CanadaStrategy < TaxCalculator
65
- def initialize(province)
66
- @province = province
67
- end
64
+ class TaxCalculator::CanadaStrategy
65
+ include Strategic::Strategy
66
+
68
67
  def tax_for(amount)
69
- amount * (gst + qst)
68
+ amount * (gst(context.province) + qst(context.province))
70
69
  end
71
- # ... more code follows
70
+ # ... other strategy methods follow
72
71
  end
73
72
  ```
74
73
 
75
- 4. In client code, obtain the needed strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
74
+ (note: if you use strategy inheritance hierarchies, make sure to have strategy base classes end with `StrategyBase` to avoid getting picked up as strategies)
76
75
 
77
- ```ruby
78
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
79
- ```
80
-
81
- 5. Instantiate the strategy object:
76
+ 4. In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
82
77
 
83
78
  ```ruby
84
- tax_calculator_strategy = strategy_class.new('IL')
79
+ tax_calculator = TaxCalculator.new(args)
80
+ tax_calculator.strategy = 'us'
85
81
  ```
86
82
 
87
- 6. Invoke the strategy overridden method:
83
+ 4a. Alternatively, instantiate the strategic model with a strategy to begin with:
88
84
 
89
85
  ```ruby
90
- tax = tax_calculator_strategy.tax_for(39.78)
86
+ tax_calculator = TaxCalculator.new_with_strategy('us', args)
91
87
  ```
92
88
 
93
- **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
89
+ 5. Invoke the strategy implemented method:
94
90
 
95
91
  ```ruby
96
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
97
- tax = tax_calculator_strategy.tax_for(39.78)
92
+ tax = tax_calculator.tax_for(39.78)
98
93
  ```
99
94
 
100
95
  **Default strategy for a strategy name that has no strategy class is the superclass: `TaxCalculator`**
101
96
 
97
+ You may set a default strategy on a strategic model via class method `default_strategy`
98
+
102
99
  ```ruby
103
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
104
- tax_calculator_strategy = tax_calculator_strategy_class.new
105
- tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
100
+ class TaxCalculator
101
+ include Strategic
102
+
103
+ default_strategy 'canada'
104
+ end
105
+
106
+ tax_calculator = TaxCalculator.new(args)
107
+ tax = tax_calculator.tax_for(39.78)
106
108
  ```
107
109
 
110
+ If no strategy is selected and you try to invoke a method that belongs to strategies, Ruby raises an amended method missing error informing you that no strategy is set to handle the method (in case it was a strategy method).
111
+
108
112
  ## Setup
109
113
 
110
114
  ### Option 1: Bundler
@@ -112,7 +116,7 @@ tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
112
116
  Add the following to bundler's `Gemfile`.
113
117
 
114
118
  ```ruby
115
- gem 'strategic', '~> 0.9.1'
119
+ gem 'strategic', '~> 1.0.0'
116
120
  ```
117
121
 
118
122
  ### Option 2: Manual
@@ -120,7 +124,7 @@ gem 'strategic', '~> 0.9.1'
120
124
  Or manually install and require library.
121
125
 
122
126
  ```bash
123
- gem install strategic -v0.9.1
127
+ gem install strategic -v1.0.0
124
128
  ```
125
129
 
126
130
  ```ruby
@@ -130,34 +134,63 @@ require 'strategic'
130
134
  ### Usage
131
135
 
132
136
  Steps:
133
- 1. Have the original class you'd like to strategize include Strategic
134
- 2. Create a directory matching the class underscored file name minus the '.rb' extension
135
- 3. Create a strategy class under that directory, which:
137
+ 1. Have the original class you'd like to strategize include `Strategic` (e.g. `def TaxCalculator; include Strategic; end`
138
+ 2. Create a directory matching the class underscored file name minus the '.rb' extension (e.g. `tax_calculator/`)
139
+ 3. Create a strategy class under that directory (e.g. `tax_calculator/us_strategy.rb`), which:
136
140
  - Lives under the original class namespace
137
- - Extends the original class to strategize
141
+ - Includes the `Strategic::Strategy` module
138
142
  - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
139
- 4. Get needed strategy class using `strategy_class_for` class method taking strategy name (any case) or related object/type (can call `strategy_names` class method to obtain strategy names)
140
- 5. Instantiate strategy with needed constructor parameters
143
+ 4. Set strategy on strategic model using `strategy=` attribute writer method or instantiate with `new_with_strategy` class method, which takes a strategy name string (any case), strategy class, or mirror object (having a class matching strategy name minus the word `Strategy`) (note: you can call `::strategy_names` class method to obtain available strategy names or `::stratgies` to obtain available strategy classes)
141
144
  6. Invoke strategy method needed
142
145
 
143
- Alternative approach:
146
+ ## API
147
+
148
+ - `StrategicClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
149
+ - `StrategicClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
150
+ - `StrategicClass::new_with_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
151
+ - `StrategicClass::strategy_class_for(string_or_class_or_object)`: selects a strategy class based on a string (e.g. 'us' selects UsStrategy) or alternatively a class/object if you have a mirror hierarchy for the strategy hierarchy
152
+ - `StrategicClass::default_strategy`: (used in model class body) sets default strategy as a strategy name string (e.g. 'us' selects UsStrategy) or alternatively a class/object if you have a mirror hierarchy for the strategy hierarchy
153
+ - `StrategicClass::strategy_matcher`: (used in model class body) custom matcher for all strategies (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
154
+ - `StrategicClass#strategy=`: sets strategy
155
+ - `StrategicClass#strategy`: returns current strategy
156
+ - `StrategyClass::strategy_name`: returns parsed strategy name of current strategy class
157
+ - `StrategyClass::strategy_matcher`: (used in model class body) custom matcher for a specific strategy (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
158
+ - `StrategyClass::strategy_exclusion`: (used in model class body) exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
159
+ - `StrategyClass::strategy_alias`: (used in model class body) alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
160
+ - `StrategyClass#context`: returns strategy context (the strategic model instance)
161
+
162
+ Example with customizations via class body methods:
144
163
 
145
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
146
- and constructor parameters
164
+ ```ruby
165
+ class TaxCalculator
166
+ default_strategy 'us'
167
+
168
+ # fuzz matcher
169
+ strategy_matcher do |string_or_class_or_object|
170
+ class_name = self.name # current strategy class name being tested for matching
171
+ strategy_name = class_name.split('::').last.sub(/Strategy$/, '').gsub(/([A-Z])/) {|letter| "_#{letter.downcase}"}[1..-1]
172
+ strategy_name_length = strategy_name.length
173
+ possible_keywords = strategy_name_length.times.map {|n| strategy_name.chars.combination(strategy_name_length - n).to_a}.reduce(:+).map(&:join)
174
+ possible_keywords.include?(string_or_class_or_object)
175
+ end
176
+ # ... more code follows
177
+ end
147
178
 
148
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
149
- strategy.
179
+ class TaxCalculator::UsStrategy
180
+ include Strategic::Strategy
150
181
 
151
- ## API
182
+ strategy_alias 'USA'
183
+ strategy_exclusion 'U'
184
+
185
+ # ... strategy methods follow
186
+ end
152
187
 
153
- - `StrategicClass::strategy_class_for(string_or_class_or_object)`: selects a strategy class based on a string (e.g. 'us' selects USStrategy) or alternatively a class/object if you have a mirror hierarchy for the strategy hierarchy
154
- - `StrategicClass::new_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
155
- - `StrategicClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
156
- - `StrategicClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
157
- - `StrategicClass::strategy_name`: returns parsed strategy name of current strategy class
158
- - `StrategicClass::strategy_matcher`: custom match (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
159
- - `StrategicClass::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
160
- - `StrategicClass::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
188
+ class TaxCalculator::CanadaStrategy
189
+ include Strategic::Strategy
190
+
191
+ # ... strategy methods follow
192
+ end
193
+ ```
161
194
 
162
195
  ## TODO
163
196
 
data/lib/strategic.rb CHANGED
@@ -18,7 +18,7 @@
18
18
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
-
21
+
22
22
  module Strategic
23
23
  def self.included(klass)
24
24
  klass.extend(ClassMethods)
@@ -26,27 +26,19 @@ module Strategic
26
26
  end
27
27
 
28
28
  module ClassMethods
29
- def strategy_alias(alias_string_or_class_or_object)
30
- strategy_aliases << alias_string_or_class_or_object
31
- end
32
-
33
- def strategy_aliases
34
- @strategy_aliases ||= []
35
- end
36
-
37
- def strategy_exclusion(exclusion_string_or_class_or_object)
38
- strategy_exclusions << exclusion_string_or_class_or_object
39
- end
40
-
41
- def strategy_exclusions
42
- @strategy_exclusions ||= []
43
- end
44
-
45
29
  def strategy_matcher(&matcher_block)
46
- if block_given?
30
+ if matcher_block.nil?
31
+ @strategy_matcher
32
+ else
47
33
  @strategy_matcher = matcher_block
34
+ end
35
+ end
36
+
37
+ def default_strategy(string_or_class_or_object = nil)
38
+ if string_or_class_or_object.nil?
39
+ @default_strategy
48
40
  else
49
- @strategy_matcher
41
+ @default_strategy = strategy_class_for(string_or_class_or_object)
50
42
  end
51
43
  end
52
44
 
@@ -65,7 +57,7 @@ module Strategic
65
57
  def strategy_class_for(string_or_class_or_object)
66
58
  strategy_class = strategy_matcher_for_any_strategy? ? strategy_class_with_strategy_matcher(string_or_class_or_object) : strategy_class_without_strategy_matcher(string_or_class_or_object)
67
59
  strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
68
- strategy_class ||= self
60
+ strategy_class ||= default_strategy
69
61
  end
70
62
 
71
63
  def strategy_class_with_strategy_matcher(string_or_class_or_object)
@@ -93,26 +85,49 @@ module Strategic
93
85
  end
94
86
  end
95
87
 
96
- def new_strategy(string_or_class_or_object, *args, &block)
97
- strategy_class_for(string_or_class_or_object).new(*args, &block)
88
+ def new_with_strategy(string_or_class_or_object, *args, &block)
89
+ new(*args, &block).tap do |model|
90
+ model.strategy = string_or_class_or_object
91
+ end
98
92
  end
99
93
 
100
94
  def strategies
101
95
  constants.map do |constant_symbol|
102
96
  const_get(constant_symbol)
103
97
  end.select do |constant|
104
- constant.respond_to?(:ancestors) && constant.ancestors.include?(self)
105
- end
98
+ constant.ancestors.include?(Strategic::Strategy) && constant.name.split('::').last.end_with?('Strategy') && constant.name.split('::').last != 'Strategy' # has to be something like PrefixStrategy
99
+ end.sort_by(&:strategy_name)
106
100
  end
107
101
 
108
102
  def strategy_names
109
103
  strategies.map(&:strategy_name)
110
104
  end
111
105
 
112
- def strategy_name
113
- Strategic.underscore(name.split(':').last).sub(/_strategy$/, '')
106
+ end
107
+
108
+ def strategy=(string_or_class_or_object)
109
+ @strategy = self.class.strategy_class_for(string_or_class_or_object)&.new(self)
110
+ end
111
+
112
+ def strategy
113
+ @strategy
114
+ end
115
+
116
+ def method_missing(method_name, *args, &block)
117
+ if strategy&.respond_to?(method_name, *args, &block)
118
+ strategy.send(method_name, *args, &block)
119
+ else
120
+ begin
121
+ super
122
+ rescue => e
123
+ raise "No strategy is set to handle the method #{method_name} with args #{args.inspect} and block #{block.inspect} / " + e.message
124
+ end
114
125
  end
115
126
  end
127
+
128
+ def respond_to?(method_name, *args, &block)
129
+ strategy&.respond_to?(method_name, *args, &block) || super
130
+ end
116
131
 
117
132
  private
118
133
 
@@ -124,3 +139,5 @@ module Strategic
124
139
  text.chars.reduce('') {|output,c| !output.empty? && c.match(/[A-Z]/) ? output + '_' + c : output + c}.downcase
125
140
  end
126
141
  end
142
+
143
+ require_relative 'strategic/strategy'
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2020-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module Strategic
23
+ module Strategy
24
+ def self.included(klass)
25
+ klass.extend(ClassMethods)
26
+ end
27
+
28
+ module ClassMethods
29
+ def strategy_alias(alias_string_or_class_or_object)
30
+ strategy_aliases << alias_string_or_class_or_object
31
+ end
32
+
33
+ def strategy_aliases
34
+ @strategy_aliases ||= []
35
+ end
36
+
37
+ def strategy_exclusion(exclusion_string_or_class_or_object)
38
+ strategy_exclusions << exclusion_string_or_class_or_object
39
+ end
40
+
41
+ def strategy_exclusions
42
+ @strategy_exclusions ||= []
43
+ end
44
+
45
+ def strategy_matcher(&matcher_block)
46
+ if block_given?
47
+ @strategy_matcher = matcher_block
48
+ else
49
+ @strategy_matcher
50
+ end
51
+ end
52
+
53
+ def strategy_name
54
+ Strategic.underscore(name.split(':').last).sub(/_strategy$/, '')
55
+ end
56
+ end
57
+
58
+ attr_reader :context
59
+
60
+ def initialize(context)
61
+ @context = context
62
+ end
63
+
64
+ end
65
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strategic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-17 00:00:00.000000000 Z
11
+ date: 2021-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -147,6 +147,7 @@ files:
147
147
  - LICENSE.txt
148
148
  - README.md
149
149
  - lib/strategic.rb
150
+ - lib/strategic/strategy.rb
150
151
  homepage: http://github.com/AndyObtiva/strategic
151
152
  licenses:
152
153
  - MIT