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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5b79636eeccc8c6b813d95c05e0aba4e864d8cbc
4
- data.tar.gz: 4fb40967c29ffa5c788db5432c0a80444ab5c3db
2
+ SHA256:
3
+ metadata.gz: 5d1b5e48f559fa9befbe74660a5894d73aa99e9acb9ab31daf6d592e50d5200e
4
+ data.tar.gz: b0064808d6119cd5e860762c82483d457c30c4a5d22b703060fbef323a781086
5
5
  SHA512:
6
- metadata.gz: 654d0e603dbe10f748ba3025664bf0480a4833d62be091ffb3f6b807754487a8284f2687f7f9c40658d499e99342f408c2c282c8590962664c16cf6504453aa9
7
- data.tar.gz: 74f1272d40abb8e22520914614bf99a80455f99480007f4b7bc54b0b785065afcf8762822cf34bae977108e42895971c4bf9ac822d18f81620f9914925f461bd
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017 Andy Maleh
1
+ Copyright (c) 2020-2021 Andy Maleh
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,18 +1,21 @@
1
- # Strategic (Painless Strategy Pattern in Ruby and Rails)
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/case conditionals can get really hairy in highly sophisticated business domains.
5
- Domain model inheritance can help remedy the problem, but dumping all
6
- logic variations in the same domain models can cause a maintenance nightmare.
7
- Thankfully, Strategy Pattern as per the Gang of Four solves the problem by externalizing logic variations to
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 different strategies mirror an existing domain model hierarchy
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
- `strategic` enables you to make any existing domain model "strategic",
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
- ## Instructions
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.8.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 -v0.8.0
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
- - Extends the original class to strategize
141
+ - Includes the `Strategic::Strategy` module
58
142
  - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
59
- 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)
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
- Alternative approach:
146
+ ## API
64
147
 
65
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
66
- and constructor parameters
148
+ ### Strategic model
67
149
 
68
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
69
- strategy.
150
+ #### Class Body Methods
70
151
 
71
- ### Example
152
+ These methods can be delcared in a strategic model class body.
72
153
 
73
- 1. Class to strategize is: `TaxCalculator`
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
- ```ruby
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
- 2. Directory to create strategies under: `tax_calculator`
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
- 3. Strategy class:
164
+ #### Instance Methods
88
165
 
89
- ```ruby
90
- class TaxCalculator::UsStrategy < TaxCalculator
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
- class TaxCalculator::CanadaStrategy < TaxCalculator
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
- 4. Get needed strategy:
171
+ #### Class Body Methods
112
172
 
113
- ```ruby
114
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
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
- 5. Instantiate strategy:
177
+ #### Class Methods
118
178
 
119
- ```ruby
120
- tax_calculator_strategy = strategy_class.new('IL')
121
- ```
179
+ - `::strategy_name`: returns parsed strategy name of current strategy class
122
180
 
123
- 6. Invoke strategy method:
181
+ #### Instance Methods
124
182
 
125
- ```ruby
126
- tax = tax_calculator_strategy.tax_for(39.78)
127
- ```
183
+ - `#context`: returns strategy context (the strategic model instance)
128
184
 
129
- **Alternative approach using `new_strategy`:**
185
+ ### Example with Customizations via Class Body Methods
130
186
 
131
187
  ```ruby
132
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
133
- tax = tax_calculator_strategy.tax_for(39.78)
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
- **Default strategy for a strategy name that has no strategy class is TaxCalculator**
202
+ class TaxCalculator::UsStrategy
203
+ include Strategic::Strategy
137
204
 
138
- ```ruby
139
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
140
- tax_calculator_strategy = tax_calculator_strategy_class.new
141
- tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
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
- ## Release Notes
218
+ ## TODO
145
219
 
146
- **0.8.0:** Initial version with `strategy_class_for`, `new_strategy`, `strategies`, and `strategy_names`
220
+ [TODO.md](TODO.md)
147
221
 
148
- ## TODO
222
+ ## Change Log
149
223
 
150
- None
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
- ## Copyright
238
+ ## License
239
+
240
+ [MIT](LICENSE.txt)
165
241
 
166
- Copyright (c) 2020 Andy Maleh. See LICENSE.txt for
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
- class_name ||= "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
25
- class_eval(class_name)
26
- rescue NameError
27
- self
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 new_strategy(string_or_class_or_object, *args, &block)
31
- strategy_class_for(string_or_class_or_object).new(*args, &block)
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) && constant.ancestors.include?(self)
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(&:name).map { |class_name| Strategic.underscore(class_name.split(':').last).sub(/_strategy$/, '') }
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.8.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: 2020-01-27 00:00:00.000000000 Z
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: coveralls
84
+ name: simplecov
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '='
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.8.5
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.8.5
96
+ version: 0.16.1
97
97
  - !ruby/object:Gem::Dependency
98
- name: simplecov
98
+ name: coveralls
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.10.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.10.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.0
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.0
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
- rubyforge_project:
168
- rubygems_version: 2.6.10
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: []