strategic 0.9.1 → 1.2.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: d76d0987f4534352de4fb52f025547a0ef37cad16a9452f1fd78da71606a3bae
4
+ data.tar.gz: 389a399d74c94352cf2165a8975b0b8d5886c782d08614c8b674ae0bd329c2a3
5
5
  SHA512:
6
- metadata.gz: 18fe37a88abab52119d44e03402ea2c005c3235d8709b3802dca8cebaf863af712bbc62c68bed47940ca7846d310135a685e416bc94c4c4f7d6984094ab5071b
7
- data.tar.gz: 959739994722aa145afa46180cad9ef556fe27e57f39e9b49b631074dd788b2cfcc60dda26f564f8d09a2ce94c1100dc6d5994718d3b1421599dbc0665c1bbee
6
+ metadata.gz: 5aa4d886c61f6daf162ac9a7dbd19635fb20cb0c9c9cef437998d8496e0e75f4c0381a7abe2c887ba794398ca4522489702cfa644cd18d180bb9cf95813a8725
7
+ data.tar.gz: 2243df9a574cbf09d708f16ef5d6c2bbc2d621e1932c50e68b081402ad19c49ab341dfdd07b5fd5461cbe1ad0ed17cf1898546d62e09b4e254b4ed3d9fbf8d89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.2.0
4
+
5
+ - `default_strategy` default value will be `'default'`, assuming a `model_namespace/default_strategy.rb` file with `DefaultStrategy` class
6
+ - `new_with_default_strategy` class method (instantiating with `'default'` strategy if not configured or `default_strategy` class method value if configured)
7
+
8
+ ## 1.1.0
9
+
10
+ - Generate `strategy_name` attribute on `Strategic` class if it does not already exist like in the case of a Rails migration column
11
+ - Automatically set `strategy_name` attribute when setting `strategy` attribute (either `strategy_name` attribute in Ruby or column in Rails)
12
+ - Load `strategy` attribute from `strategy_name` attribute on `after_initialize` in Rails
13
+
14
+ ## 1.0.1
15
+
16
+ - Fix error "undefined method `new' for Strategic::Strategy:Module" that occurs when setting an empty string strategy (must return nil or default strategy)
17
+ - Fix issue with `ancestors` method not available on all constants (only ones that are classes/modules)
18
+
19
+ ## 1.0.0
20
+
21
+ - Improve design to better match the authentic Gang of Four Strategy Pattern with `Strategic::Strategy` module, removing the need for inheritance.
22
+ - `#strategy=`/`#strategy` enable setting/getting strategy on model
23
+ - `#context` enables getting strategic model instance on strategy just as per the GOF Design Pattern
24
+ - `default_strategy` class body method to set default strategy
25
+ - Filter strategies by ones ending with `Strategy` in class name
26
+
3
27
  ## 0.9.1
4
28
 
5
29
  - `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.2.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)` or `#strategy_name=(strategy_name)`), and/or instantiate `TaxCalculator` directly (`.new(*initialize_args)`), with default strategy (`.new_with_default_strategy(*initialize_args)`), or 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,80 @@ 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)
75
+
76
+ 4. In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
76
77
 
77
78
  ```ruby
78
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
79
+ tax_calculator = TaxCalculator.new(args)
80
+ tax_calculator.strategy = 'us'
79
81
  ```
80
82
 
81
- 5. Instantiate the strategy object:
83
+ 4a. Alternatively, instantiate the strategic model with a strategy to begin with:
82
84
 
83
85
  ```ruby
84
- tax_calculator_strategy = strategy_class.new('IL')
86
+ tax_calculator = TaxCalculator.new_with_strategy('us', args)
85
87
  ```
86
88
 
87
- 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`):
88
90
 
89
91
  ```ruby
90
- tax = tax_calculator_strategy.tax_for(39.78)
92
+ tax_calculator = TaxCalculator.create(args) # args include strategy_name
91
93
  ```
92
94
 
93
- **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
95
+ 5. Invoke the strategy implemented method:
94
96
 
95
97
  ```ruby
96
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
97
- tax = tax_calculator_strategy.tax_for(39.78)
98
+ tax = tax_calculator.tax_for(39.78)
98
99
  ```
99
100
 
100
- **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` unless `DefaultStrategy` class exists under the model class namespace or `default_strategy` class attribute is set.
102
+
103
+ This is how to set a default strategy on a strategic model via class method `default_strategy`:
101
104
 
102
105
  ```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
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)
106
114
  ```
107
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
+
108
118
  ## Setup
109
119
 
110
120
  ### Option 1: Bundler
@@ -112,7 +122,7 @@ tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
112
122
  Add the following to bundler's `Gemfile`.
113
123
 
114
124
  ```ruby
115
- gem 'strategic', '~> 0.9.1'
125
+ gem 'strategic', '~> 1.2.0'
116
126
  ```
117
127
 
118
128
  ### Option 2: Manual
@@ -120,7 +130,7 @@ gem 'strategic', '~> 0.9.1'
120
130
  Or manually install and require library.
121
131
 
122
132
  ```bash
123
- gem install strategic -v0.9.1
133
+ gem install strategic -v1.2.0
124
134
  ```
125
135
 
126
136
  ```ruby
@@ -130,34 +140,89 @@ require 'strategic'
130
140
  ### Usage
131
141
 
132
142
  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:
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`) (default is assumed as `tax_calculator/default_strategy.rb` unless customized with `default_strategy` class method):
136
146
  - Lives under the original class namespace
137
- - Extends the original class to strategize
147
+ - Includes the `Strategic::Strategy` module
138
148
  - 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
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.
141
151
  6. Invoke strategy method needed
142
152
 
143
- Alternative approach:
153
+ ## API
144
154
 
145
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
146
- and constructor parameters
155
+ ### Strategic model
147
156
 
148
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
149
- strategy.
157
+ #### Class Body Methods
150
158
 
151
- ## API
159
+ These methods can be delcared in a strategic model class body.
160
+
161
+ - `::default_strategy(strategy_name)`: 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
+ - `::default_strategy`: returns default strategy (default: `'default'` as in `DefaultStrategy`)
163
+ - `::strategy_matcher`: custom matcher for all strategies (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
164
+
165
+ #### Class Methods
166
+
167
+ - `::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
168
+ - `::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
169
+ - `::new_with_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
170
+ - `::new_with_default_strategy(*args, &block)`: instantiates with default strategy
171
+ - `::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
172
 
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`)
173
+ #### Instance Methods
174
+
175
+ - `#strategy=`: sets strategy
176
+ - `#strategy`: returns current strategy
177
+
178
+ ### Strategy
179
+
180
+ #### Class Body Methods
181
+
182
+ - `::strategy_matcher`: custom matcher for a specific strategy (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
183
+ - `::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
184
+ - `::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
185
+
186
+ #### Class Methods
187
+
188
+ - `::strategy_name`: returns parsed strategy name of current strategy class
189
+
190
+ #### Instance Methods
191
+
192
+ - `#context`: returns strategy context (the strategic model instance)
193
+
194
+ ### Example with Customizations via Class Body Methods
195
+
196
+ ```ruby
197
+ class TaxCalculator
198
+ default_strategy 'us'
199
+
200
+ # fuzz matcher
201
+ strategy_matcher do |string_or_class_or_object|
202
+ class_name = self.name # current strategy class name being tested for matching
203
+ strategy_name = class_name.split('::').last.sub(/Strategy$/, '').gsub(/([A-Z])/) {|letter| "_#{letter.downcase}"}[1..-1]
204
+ strategy_name_length = strategy_name.length
205
+ possible_keywords = strategy_name_length.times.map {|n| strategy_name.chars.combination(strategy_name_length - n).to_a}.reduce(:+).map(&:join)
206
+ possible_keywords.include?(string_or_class_or_object)
207
+ end
208
+ # ... more code follows
209
+ end
210
+
211
+ class TaxCalculator::UsStrategy
212
+ include Strategic::Strategy
213
+
214
+ strategy_alias 'USA'
215
+ strategy_exclusion 'U'
216
+
217
+ # ... strategy methods follow
218
+ end
219
+
220
+ class TaxCalculator::CanadaStrategy
221
+ include Strategic::Strategy
222
+
223
+ # ... strategy methods follow
224
+ end
225
+ ```
161
226
 
162
227
  ## TODO
163
228
 
@@ -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,35 +18,53 @@
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
+ klass.default_strategy 'default'
34
+ end
36
35
 
37
- def strategy_exclusion(exclusion_string_or_class_or_object)
38
- strategy_exclusions << exclusion_string_or_class_or_object
36
+ module ExtraRailsMethods
37
+ def strategy_name=(string)
38
+ self['strategy_name'] = string
39
+ strategy_class = self.class.strategy_class_for(string)
40
+ @strategy = strategy_class&.new(self)
39
41
  end
42
+ end
43
+
44
+ module ExtraRubyMethods
45
+ attr_reader :strategy_name
40
46
 
41
- def strategy_exclusions
42
- @strategy_exclusions ||= []
47
+ def strategy_name=(string)
48
+ @strategy_name = string
49
+ strategy_class = self.class.strategy_class_for(string)
50
+ @strategy = strategy_class&.new(self)
43
51
  end
44
-
52
+ end
53
+
54
+ module ClassMethods
45
55
  def strategy_matcher(&matcher_block)
46
- if block_given?
56
+ if matcher_block.nil?
57
+ @strategy_matcher
58
+ else
47
59
  @strategy_matcher = matcher_block
60
+ end
61
+ end
62
+
63
+ def default_strategy(string_or_class_or_object = nil)
64
+ if string_or_class_or_object.nil?
65
+ @default_strategy
48
66
  else
49
- @strategy_matcher
67
+ @default_strategy = strategy_class_for(string_or_class_or_object)
50
68
  end
51
69
  end
52
70
 
@@ -65,7 +83,7 @@ module Strategic
65
83
  def strategy_class_for(string_or_class_or_object)
66
84
  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
85
  strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
68
- strategy_class ||= self
86
+ strategy_class ||= default_strategy
69
87
  end
70
88
 
71
89
  def strategy_class_with_strategy_matcher(string_or_class_or_object)
@@ -85,6 +103,7 @@ module Strategic
85
103
  else
86
104
  strategy_class_name = string_or_class_or_object.class.name
87
105
  end
106
+ return nil if strategy_class_name.to_s.strip.empty?
88
107
  begin
89
108
  class_name = "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
90
109
  class_eval(class_name)
@@ -93,26 +112,62 @@ module Strategic
93
112
  end
94
113
  end
95
114
 
96
- def new_strategy(string_or_class_or_object, *args, &block)
97
- strategy_class_for(string_or_class_or_object).new(*args, &block)
115
+ def new_with_default_strategy(*args, &block)
116
+ new(*args, &block).tap do |model|
117
+ model.strategy = nil
118
+ end
119
+ end
120
+
121
+ def new_with_strategy(string_or_class_or_object, *args, &block)
122
+ new(*args, &block).tap do |model|
123
+ model.strategy = string_or_class_or_object
124
+ end
98
125
  end
99
126
 
100
127
  def strategies
101
128
  constants.map do |constant_symbol|
102
129
  const_get(constant_symbol)
103
130
  end.select do |constant|
104
- constant.respond_to?(:ancestors) && constant.ancestors.include?(self)
105
- end
131
+ constant.respond_to?(:ancestors)
132
+ end.select do |constant|
133
+ constant.ancestors.include?(Strategic::Strategy) && constant.name.split('::').last.end_with?('Strategy') && constant.name.split('::').last != 'Strategy' # has to be something like PrefixStrategy
134
+ end.sort_by(&:strategy_name)
106
135
  end
107
136
 
108
137
  def strategy_names
109
138
  strategies.map(&:strategy_name)
110
139
  end
111
140
 
112
- def strategy_name
113
- Strategic.underscore(name.split(':').last).sub(/_strategy$/, '')
141
+ end
142
+
143
+ def strategy=(string_or_class_or_object)
144
+ strategy_class = self.class.strategy_class_for(string_or_class_or_object)
145
+ self.strategy_name = strategy_class&.strategy_name
146
+ end
147
+
148
+ def strategy
149
+ @strategy
150
+ end
151
+
152
+ def reload_strategy
153
+ self.strategy = strategy_name
154
+ end
155
+
156
+ def method_missing(method_name, *args, &block)
157
+ if strategy&.respond_to?(method_name, *args, &block)
158
+ strategy.send(method_name, *args, &block)
159
+ else
160
+ begin
161
+ super
162
+ rescue => e
163
+ raise "No strategy is set to handle the method #{method_name} with args #{args.inspect} and block #{block.inspect} / " + e.message
164
+ end
114
165
  end
115
166
  end
167
+
168
+ def respond_to?(method_name, *args, &block)
169
+ strategy&.respond_to?(method_name, *args, &block) || super
170
+ end
116
171
 
117
172
  private
118
173
 
@@ -124,3 +179,5 @@ module Strategic
124
179
  text.chars.reduce('') {|output,c| !output.empty? && c.match(/[A-Z]/) ? output + '_' + c : output + c}.downcase
125
180
  end
126
181
  end
182
+
183
+ 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.1
4
+ version: 1.2.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: 2022-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.8.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake-tui
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">"
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">"
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  description: |
126
140
  if/case conditionals can get really hairy in highly sophisticated business domains.
127
141
  Domain model inheritance can help remedy the problem, but you don't want to dump all
@@ -147,6 +161,7 @@ files:
147
161
  - LICENSE.txt
148
162
  - README.md
149
163
  - lib/strategic.rb
164
+ - lib/strategic/strategy.rb
150
165
  homepage: http://github.com/AndyObtiva/strategic
151
166
  licenses:
152
167
  - MIT
@@ -166,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
181
  - !ruby/object:Gem::Version
167
182
  version: '0'
168
183
  requirements: []
169
- rubygems_version: 3.2.3
184
+ rubygems_version: 3.3.1
170
185
  signing_key:
171
186
  specification_version: 4
172
187
  summary: Painless Strategy Pattern for Ruby and Rails