strategic 0.9.0 → 1.1.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 -58
- data/lib/strategic/strategy.rb +65 -0
- data/lib/strategic.rb +106 -44
- metadata +9 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60db744a19fb4a7e6ac36c0fc5d36a6868a801d296c9331b4e865e087dd93c31
|
4
|
+
data.tar.gz: a88167c7f3a97fe47be46abef4ccd840597e237bc84812a1da586db501cc7868
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
1
|
+
# Strategic 1.1.0
|
2
2
|
## Painless Strategy Pattern in Ruby and Rails
|
3
3
|
[](http://badge.fury.io/rb/strategic)
|
4
|
-
[](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
|
5
5
|
[](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
|
+
[](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
|
-
|
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
|
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
|
56
|
-
|
57
|
-
|
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
|
-
# ...
|
61
|
+
# ... other strategy methods follow
|
63
62
|
end
|
64
63
|
|
65
|
-
class TaxCalculator::CanadaStrategy
|
66
|
-
|
67
|
-
|
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
|
-
# ...
|
70
|
+
# ... other strategy methods follow
|
73
71
|
end
|
74
72
|
```
|
75
73
|
|
76
|
-
|
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
|
-
|
79
|
+
tax_calculator = TaxCalculator.new(args)
|
80
|
+
tax_calculator.strategy = 'us'
|
80
81
|
```
|
81
82
|
|
82
|
-
|
83
|
+
4a. Alternatively, instantiate the strategic model with a strategy to begin with:
|
83
84
|
|
84
85
|
```ruby
|
85
|
-
|
86
|
+
tax_calculator = TaxCalculator.new_with_strategy('us', args)
|
86
87
|
```
|
87
88
|
|
88
|
-
|
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
|
-
|
92
|
+
tax_calculator = TaxCalculator.create(args) # args include strategy_name
|
92
93
|
```
|
93
94
|
|
94
|
-
|
95
|
+
5. Invoke the strategy implemented method:
|
95
96
|
|
96
97
|
```ruby
|
97
|
-
|
98
|
-
tax = tax_calculator_strategy.tax_for(39.78)
|
98
|
+
tax = tax_calculator.tax_for(39.78)
|
99
99
|
```
|
100
100
|
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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', '~>
|
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 -
|
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
|
-
-
|
147
|
+
- Includes the `Strategic::Strategy` module
|
139
148
|
- Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
|
140
|
-
4.
|
141
|
-
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.
|
142
151
|
6. Invoke strategy method needed
|
143
152
|
|
144
|
-
|
153
|
+
## API
|
145
154
|
|
146
|
-
|
147
|
-
and constructor parameters
|
155
|
+
### Strategic model
|
148
156
|
|
149
|
-
|
150
|
-
strategy.
|
157
|
+
#### Class Body Methods
|
151
158
|
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
-
|
157
|
-
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
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
|
+
end
|
36
34
|
|
37
|
-
|
38
|
-
|
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
|
42
|
-
@
|
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
|
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
|
-
@
|
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 =
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
89
|
-
|
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)
|
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(&:
|
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:
|
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-
|
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:
|
98
|
+
name: coveralls
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
101
|
- - "~>"
|
116
102
|
- !ruby/object:Gem::Version
|
117
|
-
version: 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.
|
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.
|
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
|