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