strategic 0.9.1 → 1.2.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: 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