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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +121 -56
- data/lib/strategic/strategy.rb +65 -0
- data/lib/strategic.rb +81 -24
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d76d0987f4534352de4fb52f025547a0ef37cad16a9452f1fd78da71606a3bae
|
4
|
+
data.tar.gz: 389a399d74c94352cf2165a8975b0b8d5886c782d08614c8b674ae0bd329c2a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
1
|
+
# Strategic 1.2.0
|
2
2
|
## Painless Strategy Pattern in Ruby and Rails
|
3
3
|
[](http://badge.fury.io/rb/strategic)
|
4
4
|
[](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
|
-
|
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
|
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
|
55
|
-
|
56
|
-
|
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
|
-
# ...
|
61
|
+
# ... other strategy methods follow
|
62
62
|
end
|
63
63
|
|
64
|
-
class TaxCalculator::CanadaStrategy
|
65
|
-
|
66
|
-
|
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
|
-
# ...
|
70
|
+
# ... other strategy methods follow
|
72
71
|
end
|
73
72
|
```
|
74
73
|
|
75
|
-
|
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
|
-
|
79
|
+
tax_calculator = TaxCalculator.new(args)
|
80
|
+
tax_calculator.strategy = 'us'
|
79
81
|
```
|
80
82
|
|
81
|
-
|
83
|
+
4a. Alternatively, instantiate the strategic model with a strategy to begin with:
|
82
84
|
|
83
85
|
```ruby
|
84
|
-
|
86
|
+
tax_calculator = TaxCalculator.new_with_strategy('us', args)
|
85
87
|
```
|
86
88
|
|
87
|
-
|
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
|
-
|
92
|
+
tax_calculator = TaxCalculator.create(args) # args include strategy_name
|
91
93
|
```
|
92
94
|
|
93
|
-
|
95
|
+
5. Invoke the strategy implemented method:
|
94
96
|
|
95
97
|
```ruby
|
96
|
-
|
97
|
-
tax = tax_calculator_strategy.tax_for(39.78)
|
98
|
+
tax = tax_calculator.tax_for(39.78)
|
98
99
|
```
|
99
100
|
|
100
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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', '~>
|
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 -
|
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
|
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
|
-
-
|
147
|
+
- Includes the `Strategic::Strategy` module
|
138
148
|
- Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
|
139
|
-
4.
|
140
|
-
5.
|
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
|
-
|
153
|
+
## API
|
144
154
|
|
145
|
-
|
146
|
-
and constructor parameters
|
155
|
+
### Strategic model
|
147
156
|
|
148
|
-
|
149
|
-
strategy.
|
157
|
+
#### Class Body Methods
|
150
158
|
|
151
|
-
|
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
|
-
|
154
|
-
|
155
|
-
-
|
156
|
-
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
38
|
-
|
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
|
42
|
-
@
|
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
|
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
|
-
@
|
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 ||=
|
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
|
97
|
-
|
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)
|
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
|
-
|
113
|
-
|
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:
|
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:
|
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.
|
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
|