strategic 0.8.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +1 -1
- data/README.md +161 -86
- data/lib/strategic/strategy.rb +65 -0
- data/lib/strategic.rb +99 -9
- metadata +25 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5d1b5e48f559fa9befbe74660a5894d73aa99e9acb9ab31daf6d592e50d5200e
|
4
|
+
data.tar.gz: b0064808d6119cd5e860762c82483d457c30c4a5d22b703060fbef323a781086
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 351e5f342baf23c732723b97e0040f85d65378bf4e5fdfcde2385a9b16870a303f638da34765569d0392180295997088e1f7730b3a970210ebdd22bb388b712a
|
7
|
+
data.tar.gz: b5b269f698ae134d412dc9d8ff5e16d397da76480c88f9b92e984079f5edbaed3002f409e71ac4686e010fbd0a0bdcc460b70a42a7c503ea95e2001b665dd7a5
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
## 1.0.1
|
4
|
+
|
5
|
+
- Fix error "undefined method `new' for Strategic::Strategy:Module" that occurs when setting an empty string strategy (must return nil or default strategy)
|
6
|
+
- Fix issue with `ancestors` method not available on all constants (only ones that are classes/modules)
|
7
|
+
|
8
|
+
## 1.0.0
|
9
|
+
|
10
|
+
- Improve design to better match the authentic Gang of Four Strategy Pattern with `Strategic::Strategy` module, removing the need for inheritance.
|
11
|
+
- `#strategy=`/`#strategy` enable setting/getting strategy on model
|
12
|
+
- `#context` enables getting strategic model instance on strategy just as per the GOF Design Pattern
|
13
|
+
- `default_strategy` class body method to set default strategy
|
14
|
+
- Filter strategies by ones ending with `Strategy` in class name
|
15
|
+
|
16
|
+
## 0.9.1
|
17
|
+
|
18
|
+
- `strategy_name` returns parsed strategy name of current strategy class
|
19
|
+
- `strategy_matcher` ignores a strategy if it found another strategy already matching by strategy_alias
|
20
|
+
|
21
|
+
## 0.9.0
|
22
|
+
|
23
|
+
- `strategy_matcher` block support that enables any strategy to specify a custom matcher (or the superclass of all strategies instead)
|
24
|
+
- `strategy_exclusion` class method support that enables any strategy to specify exclusions from the custom `strategy_matcher`
|
25
|
+
- `strategy_alias` class method support that enables any strategy to specify extra aliases (used by superclass's `strategy_class_for` method)
|
26
|
+
|
27
|
+
## 0.8.0
|
28
|
+
|
29
|
+
- Initial version with `strategy_class_for`, `new_strategy`, `strategies`, and `strategy_names`
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
-
# Strategic
|
1
|
+
# Strategic 1.0.1
|
2
|
+
## Painless Strategy Pattern in Ruby and Rails
|
2
3
|
[![Gem Version](https://badge.fury.io/rb/strategic.svg)](http://badge.fury.io/rb/strategic)
|
4
|
+
[![rspec](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/strategic/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/strategic?branch=master)
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/0e638e392c21500c4fbe/maintainability)](https://codeclimate.com/github/AndyObtiva/strategic/maintainability)
|
3
7
|
|
4
|
-
if
|
5
|
-
|
6
|
-
logic variations in
|
7
|
-
Thankfully, Strategy Pattern as per the Gang of Four solves the problem by externalizing logic
|
8
|
-
separate classes outside the domain models.
|
8
|
+
`if`/`case` conditionals can get really hairy in highly sophisticated business domains.
|
9
|
+
Object-oriented inheritance helps remedy the problem, but dumping all
|
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.
|
9
12
|
|
10
|
-
Still, there are a number of challenges with repeated implementation of Strategy Pattern:
|
13
|
+
Still, there are a number of challenges with "repeated implementation" of the Strategy Pattern:
|
11
14
|
- Making domain models aware of newly added strategies without touching their
|
12
15
|
code (Open/Closed Principle).
|
13
|
-
- Fetching the right strategy without use of conditionals.
|
16
|
+
- Fetching the right strategy without the use of conditionals.
|
14
17
|
- Avoiding duplication of strategy dispatch code for multiple domain models
|
15
|
-
- Have
|
18
|
+
- Have strategies mirror an existing domain model inheritance hierarchy
|
16
19
|
|
17
20
|
`strategic` solves these problems by offering:
|
18
21
|
- Strategy Pattern support through a Ruby mixin and strategy path/name convention
|
@@ -21,18 +24,99 @@ code (Open/Closed Principle).
|
|
21
24
|
- Ability to fetch a strategy by name or by object type to mirror
|
22
25
|
- Plain Ruby and Ruby on Rails support
|
23
26
|
|
24
|
-
`
|
27
|
+
`Strategic` enables you to make any existing domain model "strategic",
|
25
28
|
externalizing all logic concerning algorithmic variations into separate strategy
|
26
|
-
classes that are easy to find, maintain and extend.
|
29
|
+
classes that are easy to find, maintain and extend while honoring the Open/Closed Principle and avoiding conditionals.
|
27
30
|
|
28
|
-
|
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)`), 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
|
+
|
33
|
+
### Example
|
34
|
+
|
35
|
+
<img src="strategic-example.png"
|
36
|
+
alt="Strategic Example" />
|
37
|
+
|
38
|
+
1. Include `Strategic` module in the Class to strategize: `TaxCalculator`
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class TaxCalculator
|
42
|
+
include Strategic
|
43
|
+
|
44
|
+
# strategies may implement a tax_for(amount) method
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
2. Now, you can add strategies under this directory without having to modify the original class: `tax_calculator`
|
49
|
+
|
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
|
+
|
54
|
+
```ruby
|
55
|
+
class TaxCalculator::UsStrategy
|
56
|
+
include Strategic::Strategy
|
57
|
+
|
58
|
+
def tax_for(amount)
|
59
|
+
amount * state_rate(context.state)
|
60
|
+
end
|
61
|
+
# ... other strategy methods follow
|
62
|
+
end
|
63
|
+
|
64
|
+
class TaxCalculator::CanadaStrategy
|
65
|
+
include Strategic::Strategy
|
66
|
+
|
67
|
+
def tax_for(amount)
|
68
|
+
amount * (gst(context.province) + qst(context.province))
|
69
|
+
end
|
70
|
+
# ... other strategy methods follow
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
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
|
+
|
78
|
+
```ruby
|
79
|
+
tax_calculator = TaxCalculator.new(args)
|
80
|
+
tax_calculator.strategy = 'us'
|
81
|
+
```
|
82
|
+
|
83
|
+
4a. Alternatively, instantiate the strategic model with a strategy to begin with:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
tax_calculator = TaxCalculator.new_with_strategy('us', args)
|
87
|
+
```
|
88
|
+
|
89
|
+
5. Invoke the strategy implemented method:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
tax = tax_calculator.tax_for(39.78)
|
93
|
+
```
|
94
|
+
|
95
|
+
Default strategy for a strategy name that has no strategy class is `nil`
|
96
|
+
|
97
|
+
You may set a default strategy on a strategic model via class method `default_strategy`
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class TaxCalculator
|
101
|
+
include Strategic
|
102
|
+
|
103
|
+
default_strategy 'canada'
|
104
|
+
end
|
105
|
+
|
106
|
+
tax_calculator = TaxCalculator.new(args)
|
107
|
+
tax = tax_calculator.tax_for(39.78)
|
108
|
+
```
|
109
|
+
|
110
|
+
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).
|
111
|
+
|
112
|
+
## Setup
|
29
113
|
|
30
114
|
### Option 1: Bundler
|
31
115
|
|
32
116
|
Add the following to bundler's `Gemfile`.
|
33
117
|
|
34
118
|
```ruby
|
35
|
-
gem 'strategic', '~> 0.
|
119
|
+
gem 'strategic', '~> 1.0.1'
|
36
120
|
```
|
37
121
|
|
38
122
|
### Option 2: Manual
|
@@ -40,7 +124,7 @@ gem 'strategic', '~> 0.8.0'
|
|
40
124
|
Or manually install and require library.
|
41
125
|
|
42
126
|
```bash
|
43
|
-
gem install strategic -
|
127
|
+
gem install strategic -v1.0.1
|
44
128
|
```
|
45
129
|
|
46
130
|
```ruby
|
@@ -50,104 +134,94 @@ require 'strategic'
|
|
50
134
|
### Usage
|
51
135
|
|
52
136
|
Steps:
|
53
|
-
1. Have the original class you'd like to strategize include Strategic
|
54
|
-
2. Create a directory matching the class underscored file name minus the '.rb' extension
|
55
|
-
3. Create a strategy class under that directory, which:
|
137
|
+
1. Have the original class you'd like to strategize include `Strategic` (e.g. `def TaxCalculator; include Strategic; end`
|
138
|
+
2. Create a directory matching the class underscored file name minus the '.rb' extension (e.g. `tax_calculator/`)
|
139
|
+
3. Create a strategy class under that directory (e.g. `tax_calculator/us_strategy.rb`), which:
|
56
140
|
- Lives under the original class namespace
|
57
|
-
-
|
141
|
+
- Includes the `Strategic::Strategy` module
|
58
142
|
- Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
|
59
|
-
4.
|
60
|
-
5. Instantiate strategy with needed constructor parameters
|
143
|
+
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)
|
61
144
|
6. Invoke strategy method needed
|
62
145
|
|
63
|
-
|
146
|
+
## API
|
64
147
|
|
65
|
-
|
66
|
-
and constructor parameters
|
148
|
+
### Strategic model
|
67
149
|
|
68
|
-
|
69
|
-
strategy.
|
150
|
+
#### Class Body Methods
|
70
151
|
|
71
|
-
|
152
|
+
These methods can be delcared in a strategic model class body.
|
72
153
|
|
73
|
-
|
154
|
+
- `::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
|
155
|
+
- `::strategy_matcher`: custom matcher for all strategies (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
|
74
156
|
|
75
|
-
|
76
|
-
class TaxCalculator
|
77
|
-
include Strategic
|
78
|
-
|
79
|
-
def tax_for(amount)
|
80
|
-
amount * 0.09
|
81
|
-
end
|
82
|
-
end
|
83
|
-
```
|
157
|
+
#### Class Methods
|
84
158
|
|
85
|
-
|
159
|
+
- `::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
|
160
|
+
- `::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
|
161
|
+
- `::new_with_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
|
162
|
+
- `::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
|
86
163
|
|
87
|
-
|
164
|
+
#### Instance Methods
|
88
165
|
|
89
|
-
|
90
|
-
|
91
|
-
def initialize(state)
|
92
|
-
@state = state
|
93
|
-
end
|
94
|
-
def tax_for(amount)
|
95
|
-
amount * state_rate
|
96
|
-
end
|
97
|
-
# ... more code follows
|
98
|
-
end
|
166
|
+
- `#strategy=`: sets strategy
|
167
|
+
- `#strategy`: returns current strategy
|
99
168
|
|
100
|
-
|
101
|
-
def initialize(province)
|
102
|
-
@province = province
|
103
|
-
end
|
104
|
-
def tax_for(amount)
|
105
|
-
amount * (gst + qst)
|
106
|
-
end
|
107
|
-
# ... more code follows
|
108
|
-
end
|
109
|
-
```
|
169
|
+
### Strategy
|
110
170
|
|
111
|
-
|
171
|
+
#### Class Body Methods
|
112
172
|
|
113
|
-
|
114
|
-
|
115
|
-
|
173
|
+
- `::strategy_matcher`: custom matcher for a specific strategy (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
|
174
|
+
- `::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
|
175
|
+
- `::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
|
116
176
|
|
117
|
-
|
177
|
+
#### Class Methods
|
118
178
|
|
119
|
-
|
120
|
-
tax_calculator_strategy = strategy_class.new('IL')
|
121
|
-
```
|
179
|
+
- `::strategy_name`: returns parsed strategy name of current strategy class
|
122
180
|
|
123
|
-
|
181
|
+
#### Instance Methods
|
124
182
|
|
125
|
-
|
126
|
-
tax = tax_calculator_strategy.tax_for(39.78)
|
127
|
-
```
|
183
|
+
- `#context`: returns strategy context (the strategic model instance)
|
128
184
|
|
129
|
-
|
185
|
+
### Example with Customizations via Class Body Methods
|
130
186
|
|
131
187
|
```ruby
|
132
|
-
|
133
|
-
|
134
|
-
|
188
|
+
class TaxCalculator
|
189
|
+
default_strategy 'us'
|
190
|
+
|
191
|
+
# fuzz matcher
|
192
|
+
strategy_matcher do |string_or_class_or_object|
|
193
|
+
class_name = self.name # current strategy class name being tested for matching
|
194
|
+
strategy_name = class_name.split('::').last.sub(/Strategy$/, '').gsub(/([A-Z])/) {|letter| "_#{letter.downcase}"}[1..-1]
|
195
|
+
strategy_name_length = strategy_name.length
|
196
|
+
possible_keywords = strategy_name_length.times.map {|n| strategy_name.chars.combination(strategy_name_length - n).to_a}.reduce(:+).map(&:join)
|
197
|
+
possible_keywords.include?(string_or_class_or_object)
|
198
|
+
end
|
199
|
+
# ... more code follows
|
200
|
+
end
|
135
201
|
|
136
|
-
|
202
|
+
class TaxCalculator::UsStrategy
|
203
|
+
include Strategic::Strategy
|
137
204
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
205
|
+
strategy_alias 'USA'
|
206
|
+
strategy_exclusion 'U'
|
207
|
+
|
208
|
+
# ... strategy methods follow
|
209
|
+
end
|
210
|
+
|
211
|
+
class TaxCalculator::CanadaStrategy
|
212
|
+
include Strategic::Strategy
|
213
|
+
|
214
|
+
# ... strategy methods follow
|
215
|
+
end
|
142
216
|
```
|
143
217
|
|
144
|
-
##
|
218
|
+
## TODO
|
145
219
|
|
146
|
-
|
220
|
+
[TODO.md](TODO.md)
|
147
221
|
|
148
|
-
##
|
222
|
+
## Change Log
|
149
223
|
|
150
|
-
|
224
|
+
[CHANGELOG.md](CHANGELOG.md)
|
151
225
|
|
152
226
|
## Contributing
|
153
227
|
|
@@ -161,7 +235,8 @@ None
|
|
161
235
|
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
162
236
|
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
163
237
|
|
164
|
-
##
|
238
|
+
## License
|
239
|
+
|
240
|
+
[MIT](LICENSE.txt)
|
165
241
|
|
166
|
-
Copyright (c) 2020 Andy Maleh.
|
167
|
-
further details.
|
242
|
+
Copyright (c) 2020-2021 Andy Maleh.
|
@@ -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
@@ -1,3 +1,24 @@
|
|
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
|
+
|
1
22
|
module Strategic
|
2
23
|
def self.included(klass)
|
3
24
|
klass.extend(ClassMethods)
|
@@ -5,6 +26,26 @@ module Strategic
|
|
5
26
|
end
|
6
27
|
|
7
28
|
module ClassMethods
|
29
|
+
def strategy_matcher(&matcher_block)
|
30
|
+
if matcher_block.nil?
|
31
|
+
@strategy_matcher
|
32
|
+
else
|
33
|
+
@strategy_matcher = matcher_block
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_strategy(string_or_class_or_object = nil)
|
38
|
+
if string_or_class_or_object.nil?
|
39
|
+
@default_strategy
|
40
|
+
else
|
41
|
+
@default_strategy = strategy_class_for(string_or_class_or_object)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def strategy_matcher_for_any_strategy?
|
46
|
+
!!(strategy_matcher || strategies.any?(&:strategy_matcher))
|
47
|
+
end
|
48
|
+
|
8
49
|
def require_strategies
|
9
50
|
klass_path = caller[1].split(':').first
|
10
51
|
strategy_path = File.expand_path(File.join(klass_path, '..', Strategic.underscore(self.name), '**', '*.rb'))
|
@@ -14,6 +55,21 @@ module Strategic
|
|
14
55
|
end
|
15
56
|
|
16
57
|
def strategy_class_for(string_or_class_or_object)
|
58
|
+
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)
|
59
|
+
strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
|
60
|
+
strategy_class ||= default_strategy
|
61
|
+
end
|
62
|
+
|
63
|
+
def strategy_class_with_strategy_matcher(string_or_class_or_object)
|
64
|
+
strategies.detect do |strategy|
|
65
|
+
match = strategy.strategy_aliases.include?(string_or_class_or_object)
|
66
|
+
match ||= strategy&.strategy_matcher&.call(string_or_class_or_object) || (strategy_matcher && strategy.instance_exec(string_or_class_or_object, &strategy_matcher))
|
67
|
+
# match unless excluded or included by another strategy as an alias
|
68
|
+
match unless strategy.strategy_exclusions.include?(string_or_class_or_object) || (strategies - [strategy]).map(&:strategy_aliases).flatten.include?(string_or_class_or_object)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def strategy_class_without_strategy_matcher(string_or_class_or_object)
|
17
73
|
if string_or_class_or_object.is_a?(String)
|
18
74
|
strategy_class_name = string_or_class_or_object.downcase
|
19
75
|
elsif string_or_class_or_object.is_a?(Class)
|
@@ -21,27 +77,59 @@ module Strategic
|
|
21
77
|
else
|
22
78
|
strategy_class_name = string_or_class_or_object.class.name
|
23
79
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
80
|
+
return nil if strategy_class_name.to_s.strip.empty?
|
81
|
+
begin
|
82
|
+
class_name = "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
|
83
|
+
class_eval(class_name)
|
84
|
+
rescue NameError
|
85
|
+
# No Op
|
86
|
+
end
|
28
87
|
end
|
29
88
|
|
30
|
-
def
|
31
|
-
|
89
|
+
def new_with_strategy(string_or_class_or_object, *args, &block)
|
90
|
+
new(*args, &block).tap do |model|
|
91
|
+
model.strategy = string_or_class_or_object
|
92
|
+
end
|
32
93
|
end
|
33
94
|
|
34
95
|
def strategies
|
35
96
|
constants.map do |constant_symbol|
|
36
97
|
const_get(constant_symbol)
|
37
98
|
end.select do |constant|
|
38
|
-
constant.respond_to?(:ancestors)
|
39
|
-
end
|
99
|
+
constant.respond_to?(:ancestors)
|
100
|
+
end.select do |constant|
|
101
|
+
constant.ancestors.include?(Strategic::Strategy) && constant.name.split('::').last.end_with?('Strategy') && constant.name.split('::').last != 'Strategy' # has to be something like PrefixStrategy
|
102
|
+
end.sort_by(&:strategy_name)
|
40
103
|
end
|
41
104
|
|
42
105
|
def strategy_names
|
43
|
-
strategies.map(&:
|
106
|
+
strategies.map(&:strategy_name)
|
44
107
|
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def strategy=(string_or_class_or_object)
|
112
|
+
@strategy = self.class.strategy_class_for(string_or_class_or_object)&.new(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
def strategy
|
116
|
+
@strategy
|
117
|
+
end
|
118
|
+
|
119
|
+
def method_missing(method_name, *args, &block)
|
120
|
+
if strategy&.respond_to?(method_name, *args, &block)
|
121
|
+
strategy.send(method_name, *args, &block)
|
122
|
+
else
|
123
|
+
begin
|
124
|
+
super
|
125
|
+
rescue => e
|
126
|
+
raise "No strategy is set to handle the method #{method_name} with args #{args.inspect} and block #{block.inspect} / " + e.message
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def respond_to?(method_name, *args, &block)
|
132
|
+
strategy&.respond_to?(method_name, *args, &block) || super
|
45
133
|
end
|
46
134
|
|
47
135
|
private
|
@@ -54,3 +142,5 @@ module Strategic
|
|
54
142
|
text.chars.reduce('') {|output,c| !output.empty? && c.match(/[A-Z]/) ? output + '_' + c : output + c}.downcase
|
55
143
|
end
|
56
144
|
end
|
145
|
+
|
146
|
+
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.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -42,28 +42,28 @@ 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
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '1.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
@@ -81,47 +81,47 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 2.3.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: simplecov
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
89
|
+
version: 0.16.1
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
96
|
+
version: 0.16.1
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: coveralls
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.8.23
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.8.23
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: puts_debuggerer
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - "
|
115
|
+
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.8.
|
117
|
+
version: 0.8.1
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - "
|
122
|
+
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.8.
|
124
|
+
version: 0.8.1
|
125
125
|
description: |
|
126
126
|
if/case conditionals can get really hairy in highly sophisticated business domains.
|
127
127
|
Domain model inheritance can help remedy the problem, but you don't want to dump all
|
@@ -139,17 +139,20 @@ email: andy.am@gmail.com
|
|
139
139
|
executables: []
|
140
140
|
extensions: []
|
141
141
|
extra_rdoc_files:
|
142
|
+
- CHANGELOG.md
|
142
143
|
- LICENSE.txt
|
143
144
|
- README.md
|
144
145
|
files:
|
146
|
+
- CHANGELOG.md
|
145
147
|
- LICENSE.txt
|
146
148
|
- README.md
|
147
149
|
- lib/strategic.rb
|
150
|
+
- lib/strategic/strategy.rb
|
148
151
|
homepage: http://github.com/AndyObtiva/strategic
|
149
152
|
licenses:
|
150
153
|
- MIT
|
151
154
|
metadata: {}
|
152
|
-
post_install_message:
|
155
|
+
post_install_message:
|
153
156
|
rdoc_options: []
|
154
157
|
require_paths:
|
155
158
|
- lib
|
@@ -164,9 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
167
|
- !ruby/object:Gem::Version
|
165
168
|
version: '0'
|
166
169
|
requirements: []
|
167
|
-
|
168
|
-
|
169
|
-
signing_key:
|
170
|
+
rubygems_version: 3.2.22
|
171
|
+
signing_key:
|
170
172
|
specification_version: 4
|
171
173
|
summary: Painless Strategy Pattern for Ruby and Rails
|
172
174
|
test_files: []
|