memo_wise 0.4.0 → 1.3.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.
data/lib/memo_wise.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
5
+ require "memo_wise/internal_api"
3
6
  require "memo_wise/version"
4
7
 
5
8
  # MemoWise is the wise choice for memoization in Ruby.
@@ -22,7 +25,7 @@ require "memo_wise/version"
22
25
  # - {.memo_wise} for API and usage examples.
23
26
  # - {file:README.md} for general project information.
24
27
  #
25
- module MemoWise # rubocop:disable Metrics/ModuleLength
28
+ module MemoWise
26
29
  # Constructor to set up memoization state before
27
30
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
28
31
  # constructor.
@@ -53,170 +56,26 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
53
56
  # :nocov:
54
57
  all_args = RUBY_VERSION < "2.7" ? "*" : "..."
55
58
  # :nocov:
56
- class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
59
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
57
60
  # On Ruby 2.7 or greater:
58
61
  #
59
62
  # def initialize(...)
60
- # MemoWise.create_memo_wise_state!(self)
63
+ # MemoWise::InternalAPI.create_memo_wise_state!(self)
61
64
  # super
62
65
  # end
63
66
  #
64
67
  # On Ruby 2.6 or lower:
65
68
  #
66
69
  # def initialize(*)
67
- # MemoWise.create_memo_wise_state!(self)
70
+ # MemoWise::InternalAPI.create_memo_wise_state!(self)
68
71
  # super
69
72
  # end
70
73
 
71
74
  def initialize(#{all_args})
72
- MemoWise.create_memo_wise_state!(self)
75
+ MemoWise::InternalAPI.create_memo_wise_state!(self)
73
76
  super
74
77
  end
75
- END_OF_METHOD
76
-
77
- # @private
78
- #
79
- # Determine whether `method` takes any *positional* args.
80
- #
81
- # These are the types of positional args:
82
- #
83
- # * *Required* -- ex: `def foo(a)`
84
- # * *Optional* -- ex: `def foo(b=1)`
85
- # * *Splatted* -- ex: `def foo(*c)`
86
- #
87
- # @param method [Method, UnboundMethod]
88
- # Arguments of this method will be checked
89
- #
90
- # @return [Boolean]
91
- # Return `true` if `method` accepts one or more positional arguments
92
- #
93
- # @example
94
- # class Example
95
- # def no_args
96
- # end
97
- #
98
- # def position_arg(a)
99
- # end
100
- # end
101
- #
102
- # MemoWise.has_arg?(Example.instance_method(:no_args)) #=> false
103
- #
104
- # MemoWise.has_arg?(Example.instance_method(:position_arg)) #=> true
105
- #
106
- def self.has_arg?(method) # rubocop:disable Naming/PredicateName
107
- method.parameters.any? do |(param, _)|
108
- param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
109
- end
110
- end
111
-
112
- # @private
113
- #
114
- # Determine whether `method` takes any *keyword* args.
115
- #
116
- # These are the types of keyword args:
117
- #
118
- # * *Keyword Required* -- ex: `def foo(a:)`
119
- # * *Keyword Optional* -- ex: `def foo(b: 1)`
120
- # * *Keyword Splatted* -- ex: `def foo(**c)`
121
- #
122
- # @param method [Method, UnboundMethod]
123
- # Arguments of this method will be checked
124
- #
125
- # @return [Boolean]
126
- # Return `true` if `method` accepts one or more keyword arguments
127
- #
128
- # @example
129
- # class Example
130
- # def position_args(a, b=1)
131
- # end
132
- #
133
- # def keyword_args(a:, b: 1)
134
- # end
135
- # end
136
- #
137
- # MemoWise.has_kwarg?(Example.instance_method(:position_args)) #=> false
138
- #
139
- # MemoWise.has_kwarg?(Example.instance_method(:keyword_args)) #=> true
140
- #
141
- def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
142
- method.parameters.any? do |(param, _)|
143
- param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
144
- end
145
- end
146
-
147
- # @private
148
- #
149
- # Returns visibility of an instance method defined on a class.
150
- #
151
- # @param klass [Class]
152
- # Class in which to find the visibility of an existing *instance* method.
153
- #
154
- # @param method_name [Symbol]
155
- # Name of existing *instance* method find the visibility of.
156
- #
157
- # @return [:private, :protected, :public]
158
- # Visibility of existing instance method of the class.
159
- #
160
- # @raise ArgumentError
161
- # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
162
- # to an existing **instance** method defined on `klass`.
163
- #
164
- def self.method_visibility(klass, method_name)
165
- if klass.private_method_defined?(method_name)
166
- :private
167
- elsif klass.protected_method_defined?(method_name)
168
- :protected
169
- elsif klass.public_method_defined?(method_name)
170
- :public
171
- else
172
- raise ArgumentError, "#{method_name.inspect} must be a method on #{klass}"
173
- end
174
- end
175
-
176
- # @private
177
- #
178
- # Find the original class for which the given class is the corresponding
179
- # "singleton class".
180
- #
181
- # See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
182
- #
183
- # @param klass [Class]
184
- # Singleton class to find the original class of
185
- #
186
- # @return Class
187
- # Original class for which `klass` is the singleton class.
188
- #
189
- # @raise ArgumentError
190
- # Raises if `klass` is not a singleton class.
191
- #
192
- def self.original_class_from_singleton(klass)
193
- unless klass.singleton_class?
194
- raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
195
- end
196
-
197
- # Search ObjectSpace
198
- # * 1:1 relationship of singleton class to original class is documented
199
- # * Performance concern: searches all Class objects
200
- # But, only runs at load time
201
- ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
202
- end
203
-
204
- # @private
205
- #
206
- # Create initial mutable state to store memoized values if it doesn't
207
- # already exist
208
- #
209
- # @param [Object] obj
210
- # Object in which to create mutable state to store future memoized values
211
- #
212
- # @return [Object] the passed-in obj
213
- def self.create_memo_wise_state!(obj)
214
- unless obj.instance_variables.include?(:@_memo_wise)
215
- obj.instance_variable_set(:@_memo_wise, {})
216
- end
217
-
218
- obj
219
- end
78
+ HEREDOC
220
79
 
221
80
  # @private
222
81
  #
@@ -232,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
232
91
  # prepend MemoWise
233
92
  # end
234
93
  #
235
- def self.prepended(target) # rubocop:disable Metrics/PerceivedComplexity
94
+ def self.prepended(target)
236
95
  class << target
237
96
  # Allocator to set up memoization state before
238
97
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
@@ -248,21 +107,38 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
248
107
  # `Class#allocate`, so we need to override both.
249
108
  #
250
109
  def allocate
251
- MemoWise.create_memo_wise_state!(super)
110
+ MemoWise::InternalAPI.create_memo_wise_state!(super)
252
111
  end
253
112
 
254
113
  # NOTE: See YARD docs for {.memo_wise} directly below this method!
255
- def memo_wise(method_name_or_hash) # rubocop:disable Metrics/PerceivedComplexity
114
+ def memo_wise(method_name_or_hash)
256
115
  klass = self
257
116
  case method_name_or_hash
258
117
  when Symbol
259
118
  method_name = method_name_or_hash
260
119
 
261
120
  if klass.singleton_class?
262
- MemoWise.create_memo_wise_state!(
263
- MemoWise.original_class_from_singleton(klass)
121
+ MemoWise::InternalAPI.create_memo_wise_state!(
122
+ MemoWise::InternalAPI.original_class_from_singleton(klass)
264
123
  )
265
124
  end
125
+
126
+ # Ensures a module extended by another class/module still works
127
+ # e.g. rails `ClassMethods` module
128
+ if klass.is_a?(Module) && !klass.is_a?(Class)
129
+ # Using `extended` without `included` & `prepended`
130
+ # As a call to `create_memo_wise_state!` is already included in
131
+ # `.allocate`/`#initialize`
132
+ #
133
+ # But a module/class extending another module with memo_wise
134
+ # would not call `.allocate`/`#initialize` before calling methods
135
+ #
136
+ # On method call `@_memo_wise` would still be `nil`
137
+ # causing error when fetching cache from `@_memo_wise`
138
+ def klass.extended(base)
139
+ MemoWise::InternalAPI.create_memo_wise_state!(base)
140
+ end
141
+ end
266
142
  when Hash
267
143
  unless method_name_or_hash.keys == [:self]
268
144
  raise ArgumentError,
@@ -271,7 +147,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
271
147
 
272
148
  method_name = method_name_or_hash[:self]
273
149
 
274
- MemoWise.create_memo_wise_state!(self)
150
+ MemoWise::InternalAPI.create_memo_wise_state!(self)
275
151
 
276
152
  # In Ruby, "class methods" are implemented as normal instance methods
277
153
  # on the "singleton class" of a given Class object, found via
@@ -280,78 +156,137 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
280
156
  klass = klass.singleton_class
281
157
  end
282
158
 
283
- unless method_name.is_a?(Symbol)
284
- raise ArgumentError, "#{method_name.inspect} must be a Symbol"
159
+ if klass.singleton_class?
160
+ # This ensures that a memoized method defined on a parent class can
161
+ # still be used in a child class.
162
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
163
+ def inherited(subclass)
164
+ super
165
+ MemoWise::InternalAPI.create_memo_wise_state!(subclass)
166
+ end
167
+ HEREDOC
285
168
  end
286
169
 
287
- visibility = MemoWise.method_visibility(klass, method_name)
170
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
171
+
172
+ api = MemoWise::InternalAPI.new(klass)
173
+ visibility = api.method_visibility(method_name)
174
+ original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
288
175
  method = klass.instance_method(method_name)
289
176
 
290
- original_memo_wised_name = :"_memo_wise_original_#{method_name}"
291
177
  klass.send(:alias_method, original_memo_wised_name, method_name)
292
178
  klass.send(:private, original_memo_wised_name)
293
179
 
294
- # Zero-arg methods can use simpler/more performant logic because the
295
- # hash key is just the method name.
296
- if method.arity.zero?
297
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
298
- # def foo
299
- # @_memo_wise.fetch(:foo) do
300
- # @_memo_wise[:foo] = _memo_wise_original_foo
301
- # end
302
- # end
180
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
181
+ index = MemoWise::InternalAPI.next_index!(klass, method_name)
303
182
 
183
+ case method_arguments
184
+ when MemoWise::InternalAPI::NONE
185
+ # Zero-arg methods can use simpler/more performant logic because the
186
+ # hash key is just the method name.
187
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
304
188
  def #{method_name}
305
- @_memo_wise.fetch(:#{method_name}) do
306
- @_memo_wise[:#{method_name}] = #{original_memo_wised_name}
189
+ if @_memo_wise_sentinels[#{index}]
190
+ @_memo_wise[#{index}]
191
+ else
192
+ ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
193
+ @_memo_wise_sentinels[#{index}] = true
194
+ ret
307
195
  end
308
196
  end
309
- END_OF_METHOD
310
- else
311
- # If our method has arguments, we need to separate out our handling of
312
- # normal args vs. keyword args due to the changes in Ruby 3.
313
- # See: <link>
314
- # By only including logic for *args or **kwargs when they are used in
315
- # the method, we can avoid allocating unnecessary arrays and hashes.
316
- has_arg = MemoWise.has_arg?(method)
317
-
318
- if has_arg && MemoWise.has_kwarg?(method)
319
- args_str = "(*args, **kwargs)"
320
- fetch_key = "[args, kwargs].freeze"
321
- elsif has_arg
322
- args_str = "(*args)"
323
- fetch_key = "args"
324
- else
325
- args_str = "(**kwargs)"
326
- fetch_key = "kwargs"
327
- end
328
-
329
- # Note that we don't need to freeze args before using it as a hash key
330
- # because Ruby always copies argument arrays when splatted.
331
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
332
- # def foo(*args, **kwargs)
333
- # hash = @_memo_wise.fetch(:foo) do
334
- # @_memo_wise[:foo] = {}
335
- # end
336
- # hash.fetch([args, kwargs].freeze) do
337
- # hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
338
- # end
339
- # end
340
-
341
- def #{method_name}#{args_str}
342
- hash = @_memo_wise.fetch(:#{method_name}) do
343
- @_memo_wise[:#{method_name}] = {}
197
+ HEREDOC
198
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
199
+ key = method.parameters.first.last
200
+
201
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
202
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
203
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
204
+ _memo_wise_output = _memo_wise_hash[#{key}]
205
+ if _memo_wise_output || _memo_wise_hash.key?(#{key})
206
+ _memo_wise_output
207
+ else
208
+ _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
344
209
  end
345
- hash.fetch(#{fetch_key}) do
346
- hash[#{fetch_key}] = #{original_memo_wised_name}#{args_str}
210
+ end
211
+ HEREDOC
212
+ # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
213
+ # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
214
+ else
215
+ # NOTE: When benchmarking this implementation against something like:
216
+ #
217
+ # @_memo_wise.fetch(key) do
218
+ # ...
219
+ # end
220
+ #
221
+ # this implementation may sometimes perform worse than the above. This
222
+ # is because this case uses a more complex hash key (see
223
+ # `MemoWise::InternalAPI.key_str`), and hashing that key has less
224
+ # consistent performance. In general, this should still be faster for
225
+ # truthy results because `Hash#[]` generally performs hash lookups
226
+ # faster than `Hash#fetch`.
227
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
228
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
229
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
230
+ _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
231
+ _memo_wise_output = _memo_wise_hash[_memo_wise_key]
232
+ if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
233
+ _memo_wise_output
234
+ else
235
+ _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
347
236
  end
348
237
  end
349
- END_OF_METHOD
238
+ HEREDOC
350
239
  end
351
240
 
352
241
  klass.send(visibility, method_name)
353
242
  end
354
243
  end
244
+
245
+ unless target.singleton_class?
246
+ # Create class methods to implement .preset_memo_wise and .reset_memo_wise
247
+ %i[preset_memo_wise reset_memo_wise].each do |method_name|
248
+ # Like calling 'module_function', but original method stays public
249
+ target.define_singleton_method(
250
+ method_name,
251
+ MemoWise.instance_method(method_name)
252
+ )
253
+ end
254
+
255
+ # Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
256
+ # to proxy the original `UnboundMethod#parameters` results. We want the
257
+ # parameters to reflect the original method in order to support callers
258
+ # who want to use Ruby reflection to process the method parameters,
259
+ # because our overridden `#initialize` method, and in some cases the
260
+ # generated memoized methods, will have a generic set of parameters
261
+ # (`...` or `*args, **kwargs`), making reflection on method parameters
262
+ # useless without this.
263
+ def target.instance_method(symbol)
264
+ original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
265
+
266
+ super.tap do |curr_method|
267
+ # Start with calling the original `instance_method` on `symbol`,
268
+ # which returns an `UnboundMethod`.
269
+ # IF it was replaced by MemoWise,
270
+ # THEN find the original method's parameters, and modify current
271
+ # `UnboundMethod#parameters` to return them.
272
+ if symbol == :initialize
273
+ # For `#initialize` - because `prepend MemoWise` overrides the same
274
+ # method in the module ancestors, use `UnboundMethod#super_method`
275
+ # to find the original method.
276
+ orig_method = curr_method.super_method
277
+ orig_params = orig_method.parameters
278
+ curr_method.define_singleton_method(:parameters) { orig_params }
279
+ elsif private_method_defined?(original_memo_wised_name)
280
+ # For any memoized method - because the original method was renamed,
281
+ # call the original `instance_method` again to find the renamed
282
+ # original method.
283
+ orig_method = super(original_memo_wised_name)
284
+ orig_params = orig_method.parameters
285
+ curr_method.define_singleton_method(:parameters) { orig_params }
286
+ end
287
+ end
288
+ end
289
+ end
355
290
  end
356
291
 
357
292
  ##
@@ -392,6 +327,66 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
392
327
  # ex.method_to_memoize("b") #=> 2
393
328
  ##
394
329
 
330
+ ##
331
+ # @!method self.preset_memo_wise(method_name, *args, **kwargs)
332
+ # Implementation of {#preset_memo_wise} for class methods.
333
+ #
334
+ # @example
335
+ # class Example
336
+ # prepend MemoWise
337
+ #
338
+ # def self.method_called_times
339
+ # @method_called_times
340
+ # end
341
+ #
342
+ # def self.method_to_preset
343
+ # @method_called_times = (@method_called_times || 0) + 1
344
+ # "A"
345
+ # end
346
+ # memo_wise self: :method_to_preset
347
+ # end
348
+ #
349
+ # Example.preset_memo_wise(:method_to_preset) { "B" }
350
+ #
351
+ # Example.method_to_preset #=> "B"
352
+ #
353
+ # Example.method_called_times #=> nil
354
+ ##
355
+
356
+ ##
357
+ # @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
358
+ # Implementation of {#reset_memo_wise} for class methods.
359
+ #
360
+ # @example
361
+ # class Example
362
+ # prepend MemoWise
363
+ #
364
+ # def self.method_to_reset(x)
365
+ # @method_called_times = (@method_called_times || 0) + 1
366
+ # end
367
+ # memo_wise self: :method_to_reset
368
+ # end
369
+ #
370
+ # Example.method_to_reset("a") #=> 1
371
+ # Example.method_to_reset("a") #=> 1
372
+ # Example.method_to_reset("b") #=> 2
373
+ # Example.method_to_reset("b") #=> 2
374
+ #
375
+ # Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
376
+ #
377
+ # Example.method_to_reset("a") #=> 3
378
+ # Example.method_to_reset("a") #=> 3
379
+ # Example.method_to_reset("b") #=> 2
380
+ # Example.method_to_reset("b") #=> 2
381
+ #
382
+ # Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
383
+ #
384
+ # Example.method_to_reset("a") #=> 4
385
+ # Example.method_to_reset("b") #=> 5
386
+ #
387
+ # Example.reset_memo_wise # reset "all methods" mode
388
+ ##
389
+
395
390
  # Presets the memoized result for the given method to the result of the given
396
391
  # block.
397
392
  #
@@ -443,22 +438,35 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
443
438
  # ex.method_called_times #=> nil
444
439
  #
445
440
  def preset_memo_wise(method_name, *args, **kwargs)
446
- validate_memo_wised!(method_name)
441
+ raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
442
+
443
+ api = MemoWise::InternalAPI.new(self)
444
+ api.validate_memo_wised!(method_name)
447
445
 
448
- unless block_given?
449
- raise ArgumentError,
450
- "Pass a block as the value to preset for #{method_name}, #{args}"
446
+ method = method(method_name)
447
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
448
+ index = api.index(method_name)
449
+
450
+ if method_arguments == MemoWise::InternalAPI::NONE
451
+ @_memo_wise_sentinels[index] = true
452
+ @_memo_wise[index] = yield
453
+ return
451
454
  end
452
455
 
453
- validate_params!(method_name, args)
456
+ hash = (@_memo_wise[index] ||= {})
454
457
 
455
- if method(method_name).arity.zero?
456
- @_memo_wise[method_name] = yield
457
- else
458
- hash = @_memo_wise.fetch(method_name) do
459
- @_memo_wise[method_name] = {}
458
+ case method_arguments
459
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
460
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
461
+ when MemoWise::InternalAPI::SPLAT then hash[args] = yield
462
+ when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
463
+ when MemoWise::InternalAPI::MULTIPLE_REQUIRED
464
+ key = method.parameters.map.with_index do |(type, name), idx|
465
+ type == :req ? args[idx] : kwargs[name]
460
466
  end
461
- hash[fetch_key(method_name, *args, **kwargs)] = yield
467
+ hash[key] = yield
468
+ else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
469
+ hash[[args, kwargs]] = yield
462
470
  end
463
471
  end
464
472
 
@@ -510,7 +518,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
510
518
  #
511
519
  # ex.method_to_reset("a") #=> 1
512
520
  # ex.method_to_reset("a") #=> 1
513
- #
514
521
  # ex.method_to_reset("b") #=> 2
515
522
  # ex.method_to_reset("b") #=> 2
516
523
  #
@@ -518,7 +525,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
518
525
  #
519
526
  # ex.method_to_reset("a") #=> 3
520
527
  # ex.method_to_reset("a") #=> 3
521
- #
522
528
  # ex.method_to_reset("b") #=> 2
523
529
  # ex.method_to_reset("b") #=> 2
524
530
  #
@@ -531,59 +537,65 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
531
537
  #
532
538
  def reset_memo_wise(method_name = nil, *args, **kwargs)
533
539
  if method_name.nil?
534
- unless args.empty?
535
- raise ArgumentError, "Provided args when method_name = nil"
536
- end
540
+ raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
541
+ raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
537
542
 
538
- unless kwargs.empty?
539
- raise ArgumentError, "Provided kwargs when method_name = nil"
540
- end
541
-
542
- return @_memo_wise.clear
543
- end
544
-
545
- unless method_name.is_a?(Symbol)
546
- raise ArgumentError, "#{method_name.inspect} must be a Symbol"
543
+ @_memo_wise.clear
544
+ @_memo_wise_sentinels.clear
545
+ return
547
546
  end
548
547
 
549
- unless respond_to?(method_name, true)
550
- raise ArgumentError, "#{method_name} is not a defined method"
551
- end
552
-
553
- validate_memo_wised!(method_name)
554
-
555
- if args.empty? && kwargs.empty?
556
- @_memo_wise.delete(method_name)
557
- else
558
- @_memo_wise[method_name]&.delete(fetch_key(method_name, *args, **kwargs))
559
- end
560
- end
561
-
562
- private
563
-
564
- # Validates that {.memo_wise} has already been called on `method_name`.
565
- def validate_memo_wised!(method_name)
566
- original_memo_wised_name = :"_memo_wise_original_#{method_name}"
567
-
568
- unless self.class.private_method_defined?(original_memo_wised_name)
569
- raise ArgumentError, "#{method_name} is not a memo_wised method"
570
- end
571
- end
572
-
573
- # Returns arguments key to lookup memoized results for given `method_name`.
574
- def fetch_key(method_name, *args, **kwargs)
575
- method = self.class.instance_method(method_name)
576
- has_arg = MemoWise.has_arg?(method)
577
-
578
- if has_arg && MemoWise.has_kwarg?(method)
579
- [args, kwargs].freeze
580
- elsif has_arg
581
- args
582
- else
583
- kwargs
548
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
549
+ raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
550
+
551
+ api = MemoWise::InternalAPI.new(self)
552
+ api.validate_memo_wised!(method_name)
553
+
554
+ method = method(method_name)
555
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
556
+ index = api.index(method_name)
557
+
558
+ case method_arguments
559
+ when MemoWise::InternalAPI::NONE
560
+ @_memo_wise_sentinels[index] = nil
561
+ @_memo_wise[index] = nil
562
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
563
+ if args.empty?
564
+ @_memo_wise[index]&.clear
565
+ else
566
+ @_memo_wise[index]&.delete(args.first)
567
+ end
568
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
569
+ if kwargs.empty?
570
+ @_memo_wise[index]&.clear
571
+ else
572
+ @_memo_wise[index]&.delete(kwargs.first.last)
573
+ end
574
+ when MemoWise::InternalAPI::SPLAT
575
+ if args.empty?
576
+ @_memo_wise[index]&.clear
577
+ else
578
+ @_memo_wise[index]&.delete(args)
579
+ end
580
+ when MemoWise::InternalAPI::DOUBLE_SPLAT
581
+ if kwargs.empty?
582
+ @_memo_wise[index]&.clear
583
+ else
584
+ @_memo_wise[index]&.delete(kwargs)
585
+ end
586
+ else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
587
+ if args.empty? && kwargs.empty?
588
+ @_memo_wise[index]&.clear
589
+ else
590
+ key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
591
+ [args, kwargs]
592
+ else
593
+ method.parameters.map.with_index do |(type, name), i|
594
+ type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
595
+ end
596
+ end
597
+ @_memo_wise[index]&.delete(key)
598
+ end
584
599
  end
585
600
  end
586
-
587
- # TODO: Parameter validation for presetting values
588
- def validate_params!(method_name, args); end
589
601
  end