memo_wise 0.4.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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