strategic 0.9.1 → 1.0.0

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
2
  SHA256:
3
- metadata.gz: 2d5c5c2a7b15c1c3e826a37c64b0b4b90f8b4a1dd8d93aaeb5f6c81c796faa93
4
- data.tar.gz: ef054a37db19f752d92e2344f7da9f6269732bcf897babcce4c756e426ca90e2
3
+ metadata.gz: f8ddc7e63dc608ac0a7e97ebb9110431c9a490e7ec07abed6936bb347b6105e8
4
+ data.tar.gz: 4ca98f38d3933fc5f33ac460dc2767a2769cfbe0a870c9f48bfb427ca2a4c1c4
5
5
  SHA512:
6
- metadata.gz: 18fe37a88abab52119d44e03402ea2c005c3235d8709b3802dca8cebaf863af712bbc62c68bed47940ca7846d310135a685e416bc94c4c4f7d6984094ab5071b
7
- data.tar.gz: 959739994722aa145afa46180cad9ef556fe27e57f39e9b49b631074dd788b2cfcc60dda26f564f8d09a2ce94c1100dc6d5994718d3b1421599dbc0665c1bbee
6
+ metadata.gz: '067224647109cf8198006ddce287755a1ec9997ff67fc8a3099d4f172ba6549a8e9ef4f8bf285d44996aa3a5d6ef4949892dffab5a1d26918d17cdc27cbd6e0b'
7
+ data.tar.gz: 1046857c6fd46e0615ab559bbb4e84bc8ba70f7d7995bd621e7d182316d603d22e4d99e2fefc1adce3d2682769519e0cc9189bef7ce614d0aca8354271994e05
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Improve design to better match the authentic Gang of Four Strategy Pattern with `Strategic::Strategy` module, removing the need for inheritance.
6
+ - `#strategy=`/`#strategy` enable setting/getting strategy on model
7
+ - `#context` enables getting strategic model instance on strategy just as per the GOF Design Pattern
8
+ - `default_strategy` class body method to set default strategy
9
+ - Filter strategies by ones ending with `Strategy` in class name
10
+
3
11
  ## 0.9.1
4
12
 
5
13
  - `strategy_name` returns parsed strategy name of current strategy class
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Strategic 0.9.1
1
+ # Strategic 1.0.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
4
  [![rspec](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml/badge.svg)](https://github.com/AndyObtiva/strategic/actions/workflows/ruby.yml)
@@ -7,9 +7,8 @@
7
7
 
8
8
  `if`/`case` conditionals can get really hairy in highly sophisticated business domains.
9
9
  Object-oriented inheritance helps remedy the problem, but dumping all
10
- logic variations in subclasses can cause a maintenance nightmare.
11
- Thankfully, the Strategy Pattern as per the [Gang of Four book](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) solves the problem by externalizing logic to
12
- separate classes outside the domain models.
10
+ logic variations in domain model subclasses can cause a maintenance nightmare.
11
+ Thankfully, the Strategy Pattern as per the [Gang of Four book](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) solves the problem by externalizing logic via composition to separate classes outside the domain models.
13
12
 
14
13
  Still, there are a number of challenges with "repeated implementation" of the Strategy Pattern:
15
14
  - Making domain models aware of newly added strategies without touching their
@@ -27,7 +26,9 @@ code (Open/Closed Principle).
27
26
 
28
27
  `Strategic` enables you to make any existing domain model "strategic",
29
28
  externalizing all logic concerning algorithmic variations into separate strategy
30
- classes that are easy to find, maintain and extend while honoring the Open/Closed Principle.
29
+ classes that are easy to find, maintain and extend while honoring the Open/Closed Principle and avoiding conditionals.
30
+
31
+ In summary, if you make a class called `TaxCalculator` strategic by including the `Strategic` mixin module, now you are able to drop strategies under the `tax_calculator` directory sitting next to the class (e.g. `tax_calculator/us_strategy.rb`, `tax_calculator/canada_strategy.rb`) while gaining extra [API](#api) methods to grab strategy names to present in a user interface (`.strategy_names`), set a strategy (`#strategy=(strategy_name)`), 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)`).
31
32
 
32
33
  ### Example
33
34
 
@@ -40,71 +41,74 @@ alt="Strategic Example" />
40
41
  class TaxCalculator
41
42
  include Strategic
42
43
 
43
- def tax_for(amount)
44
- amount * 0.09
45
- end
44
+ # strategies may implement a tax_for(amount) method
46
45
  end
47
46
  ```
48
47
 
49
48
  2. Now, you can add strategies under this directory without having to modify the original class: `tax_calculator`
50
49
 
51
- 3. Add strategy classes under the namespace matching the original class name (`TaxCalculator`) 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.
52
53
 
53
54
  ```ruby
54
- class TaxCalculator::UsStrategy < TaxCalculator
55
- def initialize(state)
56
- @state = state
57
- end
55
+ class TaxCalculator::UsStrategy
56
+ include Strategic::Strategy
57
+
58
58
  def tax_for(amount)
59
- amount * state_rate
59
+ amount * state_rate(context.state)
60
60
  end
61
- # ... more code follows
61
+ # ... other strategy methods follow
62
62
  end
63
63
 
64
- class TaxCalculator::CanadaStrategy < TaxCalculator
65
- def initialize(province)
66
- @province = province
67
- end
64
+ class TaxCalculator::CanadaStrategy
65
+ include Strategic::Strategy
66
+
68
67
  def tax_for(amount)
69
- amount * (gst + qst)
68
+ amount * (gst(context.province) + qst(context.province))
70
69
  end
71
- # ... more code follows
70
+ # ... other strategy methods follow
72
71
  end
73
72
  ```
74
73
 
75
- 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)
76
75
 
77
- ```ruby
78
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
79
- ```
80
-
81
- 5. Instantiate the strategy object:
76
+ 4. In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
82
77
 
83
78
  ```ruby
84
- tax_calculator_strategy = strategy_class.new('IL')
79
+ tax_calculator = TaxCalculator.new(args)
80
+ tax_calculator.strategy = 'us'
85
81
  ```
86
82
 
87
- 6. Invoke the strategy overridden method:
83
+ 4a. Alternatively, instantiate the strategic model with a strategy to begin with:
88
84
 
89
85
  ```ruby
90
- tax = tax_calculator_strategy.tax_for(39.78)
86
+ tax_calculator = TaxCalculator.new_with_strategy('us', args)
91
87
  ```
92
88
 
93
- **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
89
+ 5. Invoke the strategy implemented method:
94
90
 
95
91
  ```ruby
96
- tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
97
- tax = tax_calculator_strategy.tax_for(39.78)
92
+ tax = tax_calculator.tax_for(39.78)
98
93
  ```
99
94
 
100
95
  **Default strategy for a strategy name that has no strategy class is the superclass: `TaxCalculator`**
101
96
 
97
+ You may set a default strategy on a strategic model via class method `default_strategy`
98
+
102
99
  ```ruby
103
- tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
104
- tax_calculator_strategy = tax_calculator_strategy_class.new
105
- tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
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)
106
108
  ```
107
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
+
108
112
  ## Setup
109
113
 
110
114
  ### Option 1: Bundler
@@ -112,7 +116,7 @@ tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
112
116
  Add the following to bundler's `Gemfile`.
113
117
 
114
118
  ```ruby
115
- gem 'strategic', '~> 0.9.1'
119
+ gem 'strategic', '~> 1.0.0'
116
120
  ```
117
121
 
118
122
  ### Option 2: Manual
@@ -120,7 +124,7 @@ gem 'strategic', '~> 0.9.1'
120
124
  Or manually install and require library.
121
125
 
122
126
  ```bash
123
- gem install strategic -v0.9.1
127
+ gem install strategic -v1.0.0
124
128
  ```
125
129
 
126
130
  ```ruby
@@ -130,34 +134,63 @@ require 'strategic'
130
134
  ### Usage
131
135
 
132
136
  Steps:
133
- 1. Have the original class you'd like to strategize include Strategic
134
- 2. Create a directory matching the class underscored file name minus the '.rb' extension
135
- 3. Create a strategy class under that directory, 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:
136
140
  - Lives under the original class namespace
137
- - Extends the original class to strategize
141
+ - Includes the `Strategic::Strategy` module
138
142
  - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
139
- 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)
140
- 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)
141
144
  6. Invoke strategy method needed
142
145
 
143
- Alternative approach:
146
+ ## API
147
+
148
+ - `StrategicClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
149
+ - `StrategicClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
150
+ - `StrategicClass::new_with_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
151
+ - `StrategicClass::strategy_class_for(string_or_class_or_object)`: selects a strategy class based on a string (e.g. 'us' selects UsStrategy) or alternatively a class/object if you have a mirror hierarchy for the strategy hierarchy
152
+ - `StrategicClass::default_strategy`: (used in model class body) 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
153
+ - `StrategicClass::strategy_matcher`: (used in model class body) custom matcher for all strategies (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
154
+ - `StrategicClass#strategy=`: sets strategy
155
+ - `StrategicClass#strategy`: returns current strategy
156
+ - `StrategyClass::strategy_name`: returns parsed strategy name of current strategy class
157
+ - `StrategyClass::strategy_matcher`: (used in model class body) custom matcher for a specific strategy (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
158
+ - `StrategyClass::strategy_exclusion`: (used in model class body) exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
159
+ - `StrategyClass::strategy_alias`: (used in model class body) alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
160
+ - `StrategyClass#context`: returns strategy context (the strategic model instance)
161
+
162
+ Example with customizations via class body methods:
144
163
 
145
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
146
- and constructor parameters
164
+ ```ruby
165
+ class TaxCalculator
166
+ default_strategy 'us'
167
+
168
+ # fuzz matcher
169
+ strategy_matcher do |string_or_class_or_object|
170
+ class_name = self.name # current strategy class name being tested for matching
171
+ strategy_name = class_name.split('::').last.sub(/Strategy$/, '').gsub(/([A-Z])/) {|letter| "_#{letter.downcase}"}[1..-1]
172
+ strategy_name_length = strategy_name.length
173
+ possible_keywords = strategy_name_length.times.map {|n| strategy_name.chars.combination(strategy_name_length - n).to_a}.reduce(:+).map(&:join)
174
+ possible_keywords.include?(string_or_class_or_object)
175
+ end
176
+ # ... more code follows
177
+ end
147
178
 
148
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
149
- strategy.
179
+ class TaxCalculator::UsStrategy
180
+ include Strategic::Strategy
150
181
 
151
- ## API
182
+ strategy_alias 'USA'
183
+ strategy_exclusion 'U'
184
+
185
+ # ... strategy methods follow
186
+ end
152
187
 
153
- - `StrategicClass::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
154
- - `StrategicClass::new_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
155
- - `StrategicClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
156
- - `StrategicClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
157
- - `StrategicClass::strategy_name`: returns parsed strategy name of current strategy class
158
- - `StrategicClass::strategy_matcher`: custom match (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
159
- - `StrategicClass::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
160
- - `StrategicClass::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
188
+ class TaxCalculator::CanadaStrategy
189
+ include Strategic::Strategy
190
+
191
+ # ... strategy methods follow
192
+ end
193
+ ```
161
194
 
162
195
  ## TODO
163
196
 
data/lib/strategic.rb CHANGED
@@ -18,7 +18,7 @@
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)
@@ -26,27 +26,19 @@ module Strategic
26
26
  end
27
27
 
28
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
29
  def strategy_matcher(&matcher_block)
46
- if block_given?
30
+ if matcher_block.nil?
31
+ @strategy_matcher
32
+ else
47
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
48
40
  else
49
- @strategy_matcher
41
+ @default_strategy = strategy_class_for(string_or_class_or_object)
50
42
  end
51
43
  end
52
44
 
@@ -65,7 +57,7 @@ module Strategic
65
57
  def strategy_class_for(string_or_class_or_object)
66
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)
67
59
  strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
68
- strategy_class ||= self
60
+ strategy_class ||= default_strategy
69
61
  end
70
62
 
71
63
  def strategy_class_with_strategy_matcher(string_or_class_or_object)
@@ -93,26 +85,49 @@ module Strategic
93
85
  end
94
86
  end
95
87
 
96
- def new_strategy(string_or_class_or_object, *args, &block)
97
- strategy_class_for(string_or_class_or_object).new(*args, &block)
88
+ def new_with_strategy(string_or_class_or_object, *args, &block)
89
+ new(*args, &block).tap do |model|
90
+ model.strategy = string_or_class_or_object
91
+ end
98
92
  end
99
93
 
100
94
  def strategies
101
95
  constants.map do |constant_symbol|
102
96
  const_get(constant_symbol)
103
97
  end.select do |constant|
104
- constant.respond_to?(:ancestors) && constant.ancestors.include?(self)
105
- end
98
+ constant.ancestors.include?(Strategic::Strategy) && constant.name.split('::').last.end_with?('Strategy') && constant.name.split('::').last != 'Strategy' # has to be something like PrefixStrategy
99
+ end.sort_by(&:strategy_name)
106
100
  end
107
101
 
108
102
  def strategy_names
109
103
  strategies.map(&:strategy_name)
110
104
  end
111
105
 
112
- def strategy_name
113
- Strategic.underscore(name.split(':').last).sub(/_strategy$/, '')
106
+ end
107
+
108
+ def strategy=(string_or_class_or_object)
109
+ @strategy = self.class.strategy_class_for(string_or_class_or_object)&.new(self)
110
+ end
111
+
112
+ def strategy
113
+ @strategy
114
+ end
115
+
116
+ def method_missing(method_name, *args, &block)
117
+ if strategy&.respond_to?(method_name, *args, &block)
118
+ strategy.send(method_name, *args, &block)
119
+ else
120
+ begin
121
+ super
122
+ rescue => e
123
+ raise "No strategy is set to handle the method #{method_name} with args #{args.inspect} and block #{block.inspect} / " + e.message
124
+ end
114
125
  end
115
126
  end
127
+
128
+ def respond_to?(method_name, *args, &block)
129
+ strategy&.respond_to?(method_name, *args, &block) || super
130
+ end
116
131
 
117
132
  private
118
133
 
@@ -124,3 +139,5 @@ module Strategic
124
139
  text.chars.reduce('') {|output,c| !output.empty? && c.match(/[A-Z]/) ? output + '_' + c : output + c}.downcase
125
140
  end
126
141
  end
142
+
143
+ require_relative 'strategic/strategy'
@@ -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
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.1
4
+ version: 1.0.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-17 00:00:00.000000000 Z
11
+ date: 2021-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -147,6 +147,7 @@ files:
147
147
  - LICENSE.txt
148
148
  - README.md
149
149
  - lib/strategic.rb
150
+ - lib/strategic/strategy.rb
150
151
  homepage: http://github.com/AndyObtiva/strategic
151
152
  licenses:
152
153
  - MIT