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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4031c3d360c08e12ac58e42d6e56b97c177c342e64a1680275f50ad0add7e08
4
- data.tar.gz: 5b4b783eca5301aa05f6043729531c7458b40806ed3b768c7b341b283d77ccda
3
+ metadata.gz: 60db744a19fb4a7e6ac36c0fc5d36a6868a801d296c9331b4e865e087dd93c31
4
+ data.tar.gz: a88167c7f3a97fe47be46abef4ccd840597e237bc84812a1da586db501cc7868
5
5
  SHA512:
6
- metadata.gz: b5edee9e7e0f491e5ecda04465372bb9e45274ecc906d07798e627a4485f8776f1625c8175ff403b3f8a25a8e16781f20dd6d02c76b0600cc41b7a9c4354b920
7
- data.tar.gz: 1c7ebfb4ce224c80d3181b55e4c9137ac7d1015733211592ec95218a581cedafd56abe235fab07de92c2cdf2343873eb2ad5a7789ebc18d97f05f84b88528c13
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 0.9.0
1
+ # Strategic 1.1.0
2
2
  ## Painless Strategy Pattern in Ruby and Rails
3
3
  [![Gem Version](https://badge.fury.io/rb/strategic.svg)](http://badge.fury.io/rb/strategic)
4
- [![Build Status](https://travis-ci.com/AndyObtiva/strategic.svg?branch=master)](https://travis-ci.com/AndyObtiva/strategic?branch=master)
4
+ [![rspec](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/strategic/badge.svg?branch=master)](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
+ [![Maintainability](https://api.codeclimate.com/v1/badges/0e638e392c21500c4fbe/maintainability)](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
- def tax_for(amount)
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`) and extending the original class (`TaxCalculator`) just to take advantage of default logic in it:
50
+ 3. Add strategy classes having names ending with `Strategy` by convention (e.g. `UsStrategy`) under the namespace matching the original class name (`TaxCalculator::` as in `tax_calculator/us_strategy.rb` representing `TaxCalculator::UsStrategy`) and including the module (`Strategic::Strategy`):
51
+
52
+ All strategies get access to their context (strategic model instance), which they can use in their logic.
53
53
 
54
54
  ```ruby
55
- class TaxCalculator::UsStrategy < TaxCalculator
56
- def initialize(state)
57
- @state = state
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
- # ... more code follows
61
+ # ... other strategy methods follow
63
62
  end
64
63
 
65
- class TaxCalculator::CanadaStrategy < TaxCalculator
66
- def initialize(province)
67
- @province = province
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
- # ... more code follows
70
+ # ... other strategy methods follow
73
71
  end
74
72
  ```
75
73
 
76
- 4. In client code, obtain the needed strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
74
+ (note: if you use strategy inheritance hierarchies, make sure to have strategy base classes end with `StrategyBase` to avoid getting picked up as strategies)
75
+
76
+ 4. In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
77
77
 
78
78
  ```ruby
79
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
79
+ tax_calculator = TaxCalculator.new(args)
80
+ tax_calculator.strategy = 'us'
80
81
  ```
81
82
 
82
- 5. Instantiate the strategy object:
83
+ 4a. Alternatively, instantiate the strategic model with a strategy to begin with:
83
84
 
84
85
  ```ruby
85
- tax_calculator_strategy = strategy_class.new('IL')
86
+ tax_calculator = TaxCalculator.new_with_strategy('us', args)
86
87
  ```
87
88
 
88
- 6. Invoke the strategy overridden method:
89
+ 4b. Alternatively in Rails, instantiate or create an ActiveRecord model with `strategy_name` column attribute included in args (you may generate migration for `strategy_name` column via `rails g migration add_strategy_name_to_resources strategy_name:string`):
89
90
 
90
91
  ```ruby
91
- tax = tax_calculator_strategy.tax_for(39.78)
92
+ tax_calculator = TaxCalculator.create(args) # args include strategy_name
92
93
  ```
93
94
 
94
- **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
95
+ 5. Invoke the strategy implemented method:
95
96
 
96
97
  ```ruby
97
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
98
- tax = tax_calculator_strategy.tax_for(39.78)
98
+ tax = tax_calculator.tax_for(39.78)
99
99
  ```
100
100
 
101
- **Default strategy for a strategy name that has no strategy class is the superclass: `TaxCalculator`**
101
+ Default strategy for a strategy name that has no strategy class is `nil`
102
+
103
+ You may set a default strategy on a strategic model via class method `default_strategy`
102
104
 
103
105
  ```ruby
104
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
105
- tax_calculator_strategy = tax_calculator_strategy_class.new
106
- tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
106
+ class TaxCalculator
107
+ include Strategic
108
+
109
+ default_strategy 'canada'
110
+ end
111
+
112
+ tax_calculator = TaxCalculator.new(args)
113
+ tax = tax_calculator.tax_for(39.78)
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', '~> 0.9.0'
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 -v0.9.0
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
- - Extends the original class to strategize
147
+ - Includes the `Strategic::Strategy` module
139
148
  - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
140
- 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)
141
- 5. Instantiate strategy with needed constructor parameters
149
+ 4. Set strategy on strategic model using `strategy=` attribute writer method or instantiate with `new_with_strategy` class method, which takes a strategy name string (any case), strategy class, or mirror object (having a class matching strategy name minus the word `Strategy`) (note: you can call `::strategy_names` class method to obtain available strategy names or `::stratgies` to obtain available strategy classes)
150
+ 5. Alternatively in Rails, create migration `rails g migration add_strategy_name_to_resources strategy_name:string` and set strategy via `strategy_name` column, storing in database. On load of the model, the right strategy is automatically loaded based on `strategy_name` column.
142
151
  6. Invoke strategy method needed
143
152
 
144
- Alternative approach:
153
+ ## API
145
154
 
146
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
147
- and constructor parameters
155
+ ### Strategic model
148
156
 
149
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
150
- strategy.
157
+ #### Class Body Methods
151
158
 
152
- ## API
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
- - `StrategicSuperClass::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
155
- - `StrategicSuperClass::new_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
156
- - `StrategicSuperClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
157
- - `StrategicSuperClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
158
- - `StrategicSuperClass::strategy_matcher`: custom match (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
159
- - `StrategicSuperClass::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
160
- - `StrategicSuperClass::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
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
- end
27
-
28
- module ClassMethods
29
- def strategy_alias(alias_string_or_class_or_object)
30
- strategy_aliases << alias_string_or_class_or_object
31
- end
32
-
33
- def strategy_aliases
34
- @strategy_aliases ||= []
26
+ rails_mode = klass.respond_to?(:column_names) && klass.column_names.include?('strategy_name')
27
+ if rails_mode
28
+ klass.include(ExtraRailsMethods)
29
+ klass.after_initialize :reload_strategy
30
+ else
31
+ klass.include(ExtraRubyMethods)
35
32
  end
33
+ end
36
34
 
37
- def strategy_exclusion(exclusion_string_or_class_or_object)
38
- strategy_exclusions << exclusion_string_or_class_or_object
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 strategy_exclusions
42
- @strategy_exclusions ||= []
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 block_given?
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
- @strategy_matcher
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 = nil
63
- if strategy_matcher
64
- strategy_class = strategies.detect do |strategy|
65
- match = strategy&.strategy_matcher&.call(string_or_class_or_object)
66
- match ||= strategy.instance_exec(string_or_class_or_object, &strategy_matcher)
67
- match unless strategy.strategy_exclusions.include?(string_or_class_or_object)
68
- end
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
- if string_or_class_or_object.is_a?(String)
71
- strategy_class_name = string_or_class_or_object.downcase
72
- elsif string_or_class_or_object.is_a?(Class)
73
- strategy_class_name = string_or_class_or_object.name
74
- else
75
- strategy_class_name = string_or_class_or_object.class.name
76
- end
77
- begin
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 new_strategy(string_or_class_or_object, *args, &block)
89
- strategy_class_for(string_or_class_or_object).new(*args, &block)
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) && constant.ancestors.include?(self)
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(&:name).map { |class_name| Strategic.underscore(class_name.split(':').last).sub(/_strategy$/, '') }
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: 0.9.0
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-03-15 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,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: simplecov-lcov
98
+ name: coveralls
113
99
  requirement: !ruby/object:Gem::Requirement
114
100
  requirements:
115
101
  - - "~>"
116
102
  - !ruby/object:Gem::Version
117
- version: 0.7.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.7.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.3
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