strategic 0.9.0 → 1.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
  SHA256:
3
- metadata.gz: d4031c3d360c08e12ac58e42d6e56b97c177c342e64a1680275f50ad0add7e08
4
- data.tar.gz: 5b4b783eca5301aa05f6043729531c7458b40806ed3b768c7b341b283d77ccda
3
+ metadata.gz: 60db744a19fb4a7e6ac36c0fc5d36a6868a801d296c9331b4e865e087dd93c31
4
+ data.tar.gz: a88167c7f3a97fe47be46abef4ccd840597e237bc84812a1da586db501cc7868
5
5
  SHA512:
6
- metadata.gz: b5edee9e7e0f491e5ecda04465372bb9e45274ecc906d07798e627a4485f8776f1625c8175ff403b3f8a25a8e16781f20dd6d02c76b0600cc41b7a9c4354b920
7
- data.tar.gz: 1c7ebfb4ce224c80d3181b55e4c9137ac7d1015733211592ec95218a581cedafd56abe235fab07de92c2cdf2343873eb2ad5a7789ebc18d97f05f84b88528c13
6
+ metadata.gz: a0dd52c3176aa17bab03d9de1affdfbd8b90156278277ad3b2ade3a5d4401982a83208b5d04b54c9df39faf66845bd374f0eff9cd71cc6977382382fc7f4138a
7
+ data.tar.gz: 966b2e8d07234cf47e0d6da3c622628d777970d99f7c5b6725760b53f7958e013b206aa6eb287d1da2c30a869373648a004db528d40647b89422cf812a4e7a90
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Generate `strategy_name` attribute on `Strategic` class if it does not already exist like in the case of a Rails migration column
6
+ - Automatically set `strategy_name` attribute when setting `strategy` attribute (either `strategy_name` attribute in Ruby or column in Rails)
7
+ - Load `strategy` attribute from `strategy_name` attribute on `after_initialize` in Rails
8
+
9
+ ## 1.0.1
10
+
11
+ - Fix error "undefined method `new' for Strategic::Strategy:Module" that occurs when setting an empty string strategy (must return nil or default strategy)
12
+ - Fix issue with `ancestors` method not available on all constants (only ones that are classes/modules)
13
+
14
+ ## 1.0.0
15
+
16
+ - Improve design to better match the authentic Gang of Four Strategy Pattern with `Strategic::Strategy` module, removing the need for inheritance.
17
+ - `#strategy=`/`#strategy` enable setting/getting strategy on model
18
+ - `#context` enables getting strategic model instance on strategy just as per the GOF Design Pattern
19
+ - `default_strategy` class body method to set default strategy
20
+ - Filter strategies by ones ending with `Strategy` in class name
21
+
22
+ ## 0.9.1
23
+
24
+ - `strategy_name` returns parsed strategy name of current strategy class
25
+ - `strategy_matcher` ignores a strategy if it found another strategy already matching by strategy_alias
26
+
3
27
  ## 0.9.0
4
28
 
5
29
  - `strategy_matcher` block support that enables any strategy to specify a custom matcher (or the superclass of all strategies instead)
data/README.md CHANGED
@@ -1,16 +1,14 @@
1
- # Strategic 0.9.0
1
+ # Strategic 1.1.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
- [![Build Status](https://travis-ci.com/AndyObtiva/strategic.svg?branch=master)](https://travis-ci.com/AndyObtiva/strategic?branch=master)
4
+ [![rspec](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/strategic/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/strategic?branch=master)
6
-
7
- (Note: this gem is a very early alpha work in progress and may change API in the future)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/0e638e392c21500c4fbe/maintainability)](https://codeclimate.com/github/AndyObtiva/strategic/maintainability)
8
7
 
9
8
  `if`/`case` conditionals can get really hairy in highly sophisticated business domains.
10
9
  Object-oriented inheritance helps remedy the problem, but dumping all
11
- logic variations in subclasses can cause a maintenance nightmare.
12
- 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
13
- 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.
14
12
 
15
13
  Still, there are a number of challenges with "repeated implementation" of the Strategy Pattern:
16
14
  - Making domain models aware of newly added strategies without touching their
@@ -28,7 +26,9 @@ code (Open/Closed Principle).
28
26
 
29
27
  `Strategic` enables you to make any existing domain model "strategic",
30
28
  externalizing all logic concerning algorithmic variations into separate strategy
31
- 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)` or `#strategy_name=(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)`).
32
32
 
33
33
  ### Example
34
34
 
@@ -41,71 +41,80 @@ alt="Strategic Example" />
41
41
  class TaxCalculator
42
42
  include Strategic
43
43
 
44
- def tax_for(amount)
45
- amount * 0.09
46
- end
44
+ # strategies may implement a tax_for(amount) method
47
45
  end
48
46
  ```
49
47
 
50
48
  2. Now, you can add strategies under this directory without having to modify the original class: `tax_calculator`
51
49
 
52
- 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.
53
53
 
54
54
  ```ruby
55
- class TaxCalculator::UsStrategy < TaxCalculator
56
- def initialize(state)
57
- @state = state
58
- end
55
+ class TaxCalculator::UsStrategy
56
+ include Strategic::Strategy
57
+
59
58
  def tax_for(amount)
60
- amount * state_rate
59
+ amount * state_rate(context.state)
61
60
  end
62
- # ... more code follows
61
+ # ... other strategy methods follow
63
62
  end
64
63
 
65
- class TaxCalculator::CanadaStrategy < TaxCalculator
66
- def initialize(province)
67
- @province = province
68
- end
64
+ class TaxCalculator::CanadaStrategy
65
+ include Strategic::Strategy
66
+
69
67
  def tax_for(amount)
70
- amount * (gst + qst)
68
+ amount * (gst(context.province) + qst(context.province))
71
69
  end
72
- # ... more code follows
70
+ # ... other strategy methods follow
73
71
  end
74
72
  ```
75
73
 
76
- 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)
75
+
76
+ 4. In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
77
77
 
78
78
  ```ruby
79
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
79
+ tax_calculator = TaxCalculator.new(args)
80
+ tax_calculator.strategy = 'us'
80
81
  ```
81
82
 
82
- 5. Instantiate the strategy object:
83
+ 4a. Alternatively, instantiate the strategic model with a strategy to begin with:
83
84
 
84
85
  ```ruby
85
- tax_calculator_strategy = strategy_class.new('IL')
86
+ tax_calculator = TaxCalculator.new_with_strategy('us', args)
86
87
  ```
87
88
 
88
- 6. Invoke the strategy overridden method:
89
+ 4b. Alternatively in Rails, instantiate or create an ActiveRecord model with `strategy_name` column attribute included in args (you may generate migration for `strategy_name` column via `rails g migration add_strategy_name_to_resources strategy_name:string`):
89
90
 
90
91
  ```ruby
91
- tax = tax_calculator_strategy.tax_for(39.78)
92
+ tax_calculator = TaxCalculator.create(args) # args include strategy_name
92
93
  ```
93
94
 
94
- **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
95
+ 5. Invoke the strategy implemented method:
95
96
 
96
97
  ```ruby
97
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
98
- tax = tax_calculator_strategy.tax_for(39.78)
98
+ tax = tax_calculator.tax_for(39.78)
99
99
  ```
100
100
 
101
- **Default strategy for a strategy name that has no strategy class is the superclass: `TaxCalculator`**
101
+ Default strategy for a strategy name that has no strategy class is `nil`
102
+
103
+ You may set a default strategy on a strategic model via class method `default_strategy`
102
104
 
103
105
  ```ruby
104
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
105
- tax_calculator_strategy = tax_calculator_strategy_class.new
106
- tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
106
+ class TaxCalculator
107
+ include Strategic
108
+
109
+ default_strategy 'canada'
110
+ end
111
+
112
+ tax_calculator = TaxCalculator.new(args)
113
+ tax = tax_calculator.tax_for(39.78)
107
114
  ```
108
115
 
116
+ 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).
117
+
109
118
  ## Setup
110
119
 
111
120
  ### Option 1: Bundler
@@ -113,7 +122,7 @@ tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
113
122
  Add the following to bundler's `Gemfile`.
114
123
 
115
124
  ```ruby
116
- gem 'strategic', '~> 0.9.0'
125
+ gem 'strategic', '~> 1.1.0'
117
126
  ```
118
127
 
119
128
  ### Option 2: Manual
@@ -121,7 +130,7 @@ gem 'strategic', '~> 0.9.0'
121
130
  Or manually install and require library.
122
131
 
123
132
  ```bash
124
- gem install strategic -v0.9.0
133
+ gem install strategic -v1.1.0
125
134
  ```
126
135
 
127
136
  ```ruby
@@ -131,33 +140,87 @@ require 'strategic'
131
140
  ### Usage
132
141
 
133
142
  Steps:
134
- 1. Have the original class you'd like to strategize include Strategic
135
- 2. Create a directory matching the class underscored file name minus the '.rb' extension
136
- 3. Create a strategy class under that directory, which:
143
+ 1. Have the original class you'd like to strategize include `Strategic` (e.g. `def TaxCalculator; include Strategic; end`
144
+ 2. Create a directory matching the class underscored file name minus the '.rb' extension (e.g. `tax_calculator/`)
145
+ 3. Create a strategy class under that directory (e.g. `tax_calculator/us_strategy.rb`), which:
137
146
  - Lives under the original class namespace
138
- - Extends the original class to strategize
147
+ - Includes the `Strategic::Strategy` module
139
148
  - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
140
- 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)
141
- 5. Instantiate strategy with needed constructor parameters
149
+ 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)
150
+ 5. Alternatively in Rails, create migration `rails g migration add_strategy_name_to_resources strategy_name:string` and set strategy via `strategy_name` column, storing in database. On load of the model, the right strategy is automatically loaded based on `strategy_name` column.
142
151
  6. Invoke strategy method needed
143
152
 
144
- Alternative approach:
153
+ ## API
145
154
 
146
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
147
- and constructor parameters
155
+ ### Strategic model
148
156
 
149
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
150
- strategy.
157
+ #### Class Body Methods
151
158
 
152
- ## API
159
+ These methods can be delcared in a strategic model class body.
160
+
161
+ - `::default_strategy`: 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
162
+ - `::strategy_matcher`: custom matcher for all strategies (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
163
+
164
+ #### Class Methods
165
+
166
+ - `::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
167
+ - `::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
168
+ - `::new_with_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
169
+ - `::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
153
170
 
154
- - `StrategicSuperClass::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
155
- - `StrategicSuperClass::new_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
156
- - `StrategicSuperClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
157
- - `StrategicSuperClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
158
- - `StrategicSuperClass::strategy_matcher`: custom match (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
159
- - `StrategicSuperClass::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
160
- - `StrategicSuperClass::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
171
+ #### Instance Methods
172
+
173
+ - `#strategy=`: sets strategy
174
+ - `#strategy`: returns current strategy
175
+
176
+ ### Strategy
177
+
178
+ #### Class Body Methods
179
+
180
+ - `::strategy_matcher`: custom matcher for a specific strategy (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
181
+ - `::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
182
+ - `::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
183
+
184
+ #### Class Methods
185
+
186
+ - `::strategy_name`: returns parsed strategy name of current strategy class
187
+
188
+ #### Instance Methods
189
+
190
+ - `#context`: returns strategy context (the strategic model instance)
191
+
192
+ ### Example with Customizations via Class Body Methods
193
+
194
+ ```ruby
195
+ class TaxCalculator
196
+ default_strategy 'us'
197
+
198
+ # fuzz matcher
199
+ strategy_matcher do |string_or_class_or_object|
200
+ class_name = self.name # current strategy class name being tested for matching
201
+ strategy_name = class_name.split('::').last.sub(/Strategy$/, '').gsub(/([A-Z])/) {|letter| "_#{letter.downcase}"}[1..-1]
202
+ strategy_name_length = strategy_name.length
203
+ possible_keywords = strategy_name_length.times.map {|n| strategy_name.chars.combination(strategy_name_length - n).to_a}.reduce(:+).map(&:join)
204
+ possible_keywords.include?(string_or_class_or_object)
205
+ end
206
+ # ... more code follows
207
+ end
208
+
209
+ class TaxCalculator::UsStrategy
210
+ include Strategic::Strategy
211
+
212
+ strategy_alias 'USA'
213
+ strategy_exclusion 'U'
214
+
215
+ # ... strategy methods follow
216
+ end
217
+
218
+ class TaxCalculator::CanadaStrategy
219
+ include Strategic::Strategy
220
+
221
+ # ... strategy methods follow
222
+ end
223
+ ```
161
224
 
162
225
  ## TODO
163
226
 
@@ -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
data/lib/strategic.rb CHANGED
@@ -18,37 +18,58 @@
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)
25
25
  klass.require_strategies
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 ||= []
26
+ rails_mode = klass.respond_to?(:column_names) && klass.column_names.include?('strategy_name')
27
+ if rails_mode
28
+ klass.include(ExtraRailsMethods)
29
+ klass.after_initialize :reload_strategy
30
+ else
31
+ klass.include(ExtraRubyMethods)
35
32
  end
33
+ end
36
34
 
37
- def strategy_exclusion(exclusion_string_or_class_or_object)
38
- strategy_exclusions << exclusion_string_or_class_or_object
35
+ module ExtraRailsMethods
36
+ def strategy_name=(string)
37
+ self['strategy_name'] = string
38
+ strategy_class = self.class.strategy_class_for(string)
39
+ @strategy = strategy_class&.new(self)
39
40
  end
41
+ end
42
+
43
+ module ExtraRubyMethods
44
+ attr_reader :strategy_name
40
45
 
41
- def strategy_exclusions
42
- @strategy_exclusions ||= []
46
+ def strategy_name=(string)
47
+ @strategy_name = string
48
+ strategy_class = self.class.strategy_class_for(string)
49
+ @strategy = strategy_class&.new(self)
43
50
  end
44
-
51
+ end
52
+
53
+ module ClassMethods
45
54
  def strategy_matcher(&matcher_block)
46
- if block_given?
55
+ if matcher_block.nil?
56
+ @strategy_matcher
57
+ else
47
58
  @strategy_matcher = matcher_block
59
+ end
60
+ end
61
+
62
+ def default_strategy(string_or_class_or_object = nil)
63
+ if string_or_class_or_object.nil?
64
+ @default_strategy
48
65
  else
49
- @strategy_matcher
66
+ @default_strategy = strategy_class_for(string_or_class_or_object)
50
67
  end
51
68
  end
69
+
70
+ def strategy_matcher_for_any_strategy?
71
+ !!(strategy_matcher || strategies.any?(&:strategy_matcher))
72
+ end
52
73
 
53
74
  def require_strategies
54
75
  klass_path = caller[1].split(':').first
@@ -59,47 +80,86 @@ module Strategic
59
80
  end
60
81
 
61
82
  def strategy_class_for(string_or_class_or_object)
62
- strategy_class = nil
63
- if strategy_matcher
64
- strategy_class = strategies.detect do |strategy|
65
- match = strategy&.strategy_matcher&.call(string_or_class_or_object)
66
- match ||= strategy.instance_exec(string_or_class_or_object, &strategy_matcher)
67
- match unless strategy.strategy_exclusions.include?(string_or_class_or_object)
68
- end
83
+ 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)
84
+ strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
85
+ strategy_class ||= default_strategy
86
+ end
87
+
88
+ def strategy_class_with_strategy_matcher(string_or_class_or_object)
89
+ strategies.detect do |strategy|
90
+ match = strategy.strategy_aliases.include?(string_or_class_or_object)
91
+ match ||= strategy&.strategy_matcher&.call(string_or_class_or_object) || (strategy_matcher && strategy.instance_exec(string_or_class_or_object, &strategy_matcher))
92
+ # match unless excluded or included by another strategy as an alias
93
+ match unless strategy.strategy_exclusions.include?(string_or_class_or_object) || (strategies - [strategy]).map(&:strategy_aliases).flatten.include?(string_or_class_or_object)
94
+ end
95
+ end
96
+
97
+ def strategy_class_without_strategy_matcher(string_or_class_or_object)
98
+ if string_or_class_or_object.is_a?(String)
99
+ strategy_class_name = string_or_class_or_object.downcase
100
+ elsif string_or_class_or_object.is_a?(Class)
101
+ strategy_class_name = string_or_class_or_object.name
69
102
  else
70
- if string_or_class_or_object.is_a?(String)
71
- strategy_class_name = string_or_class_or_object.downcase
72
- elsif string_or_class_or_object.is_a?(Class)
73
- strategy_class_name = string_or_class_or_object.name
74
- else
75
- strategy_class_name = string_or_class_or_object.class.name
76
- end
77
- begin
78
- class_name = "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
79
- strategy_class = class_eval(class_name)
80
- rescue NameError
81
- # No Op
82
- end
103
+ strategy_class_name = string_or_class_or_object.class.name
104
+ end
105
+ return nil if strategy_class_name.to_s.strip.empty?
106
+ begin
107
+ class_name = "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
108
+ class_eval(class_name)
109
+ rescue NameError
110
+ # No Op
83
111
  end
84
- strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
85
- strategy_class ||= self
86
112
  end
87
113
 
88
- def new_strategy(string_or_class_or_object, *args, &block)
89
- strategy_class_for(string_or_class_or_object).new(*args, &block)
114
+ def new_with_strategy(string_or_class_or_object, *args, &block)
115
+ new(*args, &block).tap do |model|
116
+ model.strategy = string_or_class_or_object
117
+ end
90
118
  end
91
119
 
92
120
  def strategies
93
121
  constants.map do |constant_symbol|
94
122
  const_get(constant_symbol)
95
123
  end.select do |constant|
96
- constant.respond_to?(:ancestors) && constant.ancestors.include?(self)
97
- end
124
+ constant.respond_to?(:ancestors)
125
+ end.select do |constant|
126
+ constant.ancestors.include?(Strategic::Strategy) && constant.name.split('::').last.end_with?('Strategy') && constant.name.split('::').last != 'Strategy' # has to be something like PrefixStrategy
127
+ end.sort_by(&:strategy_name)
98
128
  end
99
129
 
100
130
  def strategy_names
101
- strategies.map(&:name).map { |class_name| Strategic.underscore(class_name.split(':').last).sub(/_strategy$/, '') }
131
+ strategies.map(&:strategy_name)
102
132
  end
133
+
134
+ end
135
+
136
+ def strategy=(string_or_class_or_object)
137
+ strategy_class = self.class.strategy_class_for(string_or_class_or_object)
138
+ self.strategy_name = strategy_class&.strategy_name
139
+ end
140
+
141
+ def strategy
142
+ @strategy
143
+ end
144
+
145
+ def reload_strategy
146
+ self.strategy = strategy_name
147
+ end
148
+
149
+ def method_missing(method_name, *args, &block)
150
+ if strategy&.respond_to?(method_name, *args, &block)
151
+ strategy.send(method_name, *args, &block)
152
+ else
153
+ begin
154
+ super
155
+ rescue => e
156
+ raise "No strategy is set to handle the method #{method_name} with args #{args.inspect} and block #{block.inspect} / " + e.message
157
+ end
158
+ end
159
+ end
160
+
161
+ def respond_to?(method_name, *args, &block)
162
+ strategy&.respond_to?(method_name, *args, &block) || super
103
163
  end
104
164
 
105
165
  private
@@ -112,3 +172,5 @@ module Strategic
112
172
  text.chars.reduce('') {|output,c| !output.empty? && c.match(/[A-Z]/) ? output + '_' + c : output + c}.downcase
113
173
  end
114
174
  end
175
+
176
+ require_relative 'strategic/strategy'
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.0
4
+ version: 1.1.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-15 00:00:00.000000000 Z
11
+ date: 2021-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -42,14 +42,14 @@ dependencies:
42
42
  name: rdoc
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.12'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.12'
55
55
  - !ruby/object:Gem::Dependency
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 2.3.0
83
- - !ruby/object:Gem::Dependency
84
- name: coveralls
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - '='
88
- - !ruby/object:Gem::Version
89
- version: 0.8.23
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - '='
95
- - !ruby/object:Gem::Version
96
- version: 0.8.23
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: simplecov
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -109,19 +95,19 @@ dependencies:
109
95
  - !ruby/object:Gem::Version
110
96
  version: 0.16.1
111
97
  - !ruby/object:Gem::Dependency
112
- name: simplecov-lcov
98
+ name: coveralls
113
99
  requirement: !ruby/object:Gem::Requirement
114
100
  requirements:
115
101
  - - "~>"
116
102
  - !ruby/object:Gem::Version
117
- version: 0.7.0
103
+ version: 0.8.23
118
104
  type: :development
119
105
  prerelease: false
120
106
  version_requirements: !ruby/object:Gem::Requirement
121
107
  requirements:
122
108
  - - "~>"
123
109
  - !ruby/object:Gem::Version
124
- version: 0.7.0
110
+ version: 0.8.23
125
111
  - !ruby/object:Gem::Dependency
126
112
  name: puts_debuggerer
127
113
  requirement: !ruby/object:Gem::Requirement
@@ -161,6 +147,7 @@ files:
161
147
  - LICENSE.txt
162
148
  - README.md
163
149
  - lib/strategic.rb
150
+ - lib/strategic/strategy.rb
164
151
  homepage: http://github.com/AndyObtiva/strategic
165
152
  licenses:
166
153
  - MIT
@@ -180,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
167
  - !ruby/object:Gem::Version
181
168
  version: '0'
182
169
  requirements: []
183
- rubygems_version: 3.2.3
170
+ rubygems_version: 3.2.22
184
171
  signing_key:
185
172
  specification_version: 4
186
173
  summary: Painless Strategy Pattern for Ruby and Rails