memo_wise 0.3.0 → 1.2.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.
@@ -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