cacheable 1.0.4 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba379f0770988ff42a99397f056206bbbb9b60a59b51a5fee51d687263adb96e
4
- data.tar.gz: ef68f492cf221457515452df0324ff87d634914ac67e286a4e698c096e5a2a3e
3
+ metadata.gz: 9f7c4005f6683ae5c61de0d34d3795ac4607e6bb001e7f73065b947559720662
4
+ data.tar.gz: '0439c9b90265170df3706406c52c896fb8953c7e5737c9027ce9d140dcef6aed'
5
5
  SHA512:
6
- metadata.gz: 79e94e9ec720e03d2be5370ac8554c2c1635589b5cfffec5a56f27fe228944e659da1e5aa52b94836559fc06301a6f1e41d0df418c7305db925a175754ce73d8
7
- data.tar.gz: e56db78b1855a5cb4bcba7b1f408d4c4cff9cbef03f05641596e721c0b332bfc7878769632ef2eda6e6d2476505bdaf38b477fa32abc6513170b88806d21209b
6
+ metadata.gz: c08c9cdc5d6b3a9811b072df3feae84cf87dc7cadccee8fafc654b15de24d240f0ddb2c4a1bbee879765e11fef04e69cd6b81d126ffe96e9410eea8436fb239a
7
+ data.tar.gz: 384da8f24a3cdd069c9f634d31a2ef77bb1b83083aa9ca4f5d0adce364c172cf04efd0ad235373b66cb648e196cb04804d0ada2ac78400b7b5cdf75b214c1db6
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # Cacheable
2
2
 
3
+ [![CI](https://github.com/splitwise/cacheable/actions/workflows/ci.yml/badge.svg)](https://github.com/splitwise/cacheable/actions/workflows/ci.yml)
4
+
3
5
  By [Splitwise](https://www.splitwise.com)
4
6
 
7
+ Requires Ruby >= 3.3
8
+
5
9
  Cacheable is a gem which adds method caching in Ruby following an [aspect-oriented programming (AOP)](https://en.wikipedia.org/wiki/Aspect-oriented_programming) paradigm. Its core goals are:
6
10
 
7
11
  * ease of use (method annotation)
@@ -78,9 +82,9 @@ end
78
82
  > a = GitHubApiAdapter.new
79
83
  > a.star_count
80
84
  Fetching data from GitHub
81
- => 19
85
+ => 58
82
86
  > a.star_count
83
- => 19
87
+ => 58
84
88
 
85
89
  # Notice that "Fetching data from GitHub" was not output the 2nd time the method was invoked.
86
90
  # The network call and result parsing would also not be performed again.
@@ -98,12 +102,12 @@ The cache can intentionally be skipped by appending `_without_cache` to the meth
98
102
  > a = GitHubApiAdapter.new
99
103
  > a.star_count
100
104
  Fetching data from GitHub
101
- => 19
105
+ => 58
102
106
  > a.star_count_without_cache
103
107
  Fetching data from GitHub
104
- => 19
108
+ => 58
105
109
  > a.star_count
106
- => 19
110
+ => 58
107
111
  ```
108
112
 
109
113
  #### Remove the Value via `clear_#{method}_cache`
@@ -114,15 +118,15 @@ The cached value can be cleared at any time by calling `clear_#{your_method_name
114
118
  > a = GitHubApiAdapter.new
115
119
  > a.star_count
116
120
  Fetching data from GitHub
117
- => 19
121
+ => 58
118
122
  > a.star_count
119
- => 19
123
+ => 58
120
124
 
121
125
  > a.clear_star_count_cache
122
126
  => true
123
127
  > a.star_count
124
128
  Fetching data from GitHub
125
- => 19
129
+ => 58
126
130
  ```
127
131
 
128
132
  ## Additional Configuration
@@ -131,7 +135,7 @@ Fetching data from GitHub
131
135
 
132
136
  #### Default
133
137
 
134
- By default, Cacheable will construct key a key in the format `[cache_key || class_name, method_name]` without using method arguments.
138
+ By default, Cacheable will construct a key in the format `[cache_key || class_name, method_name]` without using method arguments. If a cached method is called with arguments while using the default key format, Cacheable will emit a warning to stderr since different arguments will return the same cached value. To silence the warning, provide a `:key_format` proc that includes the arguments in the cache key.
135
139
 
136
140
  If the object responds to `cache_key` its return value will be the first element in the array. `ActiveRecord` provides [`cache_key`](https://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-i-cache_key) but it can be added to any Ruby object or overwritten. If the object does not respond to it, the name of the class will be used instead. The second element will be the name of the method as a symbol.
137
141
 
@@ -151,12 +155,13 @@ require 'net/http'
151
155
  class GitHubApiAdapter
152
156
  include Cacheable
153
157
 
154
- cacheable :star_count, key_format: ->(target, method_name, method_args) do
155
- [target.class, method_name, method_args.first, Time.now.strftime('%Y-%m-%d')].join('/')
158
+ cacheable :star_count, key_format: ->(target, method_name, method_args, **kwargs) do
159
+ date = kwargs.fetch(:date, Time.now.strftime('%Y-%m-%d'))
160
+ [target.class, method_name, method_args.first, date].join('/')
156
161
  end
157
162
 
158
- def star_count(repo)
159
- puts "Fetching data from GitHub for #{repo}"
163
+ def star_count(repo, date: Time.now.strftime('%Y-%m-%d'))
164
+ puts "Fetching data from GitHub for #{repo} (as of #{date})"
160
165
  url = "https://api.github.com/repos/splitwise/#{repo}"
161
166
 
162
167
  JSON.parse(Net::HTTP.get(URI.parse(url)))['stargazers_count']
@@ -166,33 +171,34 @@ end
166
171
 
167
172
  * `target` is the object the method is being called on (`#<GitHubApiAdapter:0x0…0>`)
168
173
  * `method_name` is the name of the method being cached (`:star_count`)
169
- * `method_args` is an array of arguments being passed to the method (`[params]`)
174
+ * `method_args` is an array of positional arguments being passed to the method (`[params]`)
175
+ * `**kwargs` are the keyword arguments being passed to the method
170
176
 
171
177
  Including the method argument(s) allows you to cache different calls to the same method. Without the arguments in the cache key, a call to `star_count('cacheable')` would populate the cache and `star_count('tokenautocomplete')` would return the number of stars for Cacheable instead of what you want.
172
178
 
173
- In addition, we're including the current date in the cache key so calling this method tomorrow will return an updated value.
179
+ **Note:** The `key_format` proc only receives keyword arguments that the caller explicitly passes — method defaults are not included. That's why the proc uses `kwargs.fetch(:date, Time.now.strftime('%Y-%m-%d'))` to compute its own default when `date:` is omitted. This ensures the cache key always varies by date.
174
180
 
175
181
  ```irb
176
182
  > a = GitHubApiAdapter.new
177
183
  > a.star_count('cacheable')
178
- Fetching data from GitHub for cacheable
179
- => 19
184
+ Fetching data from GitHub for cacheable (as of 2026-02-26)
185
+ => 58
180
186
  > a.star_count('cacheable')
181
- => 19
187
+ => 58
182
188
  > a.star_count('tokenautocomplete')
183
- Fetching data from GitHub for tokenautocomplete
184
- => 1164
189
+ Fetching data from GitHub for tokenautocomplete (as of 2026-02-26)
190
+ => 1309
185
191
  > a.star_count('tokenautocomplete')
186
- => 1164
192
+ => 1309
187
193
 
188
194
  # In this example the follow cache keys are generated:
189
- # GitHubApiAdapter/star_count/cacheable/2018-09-21
190
- # GitHubApiAdapter/star_count/tokenautocomplete/2018-09-21
195
+ # GitHubApiAdapter/star_count/cacheable/2026-02-26
196
+ # GitHubApiAdapter/star_count/tokenautocomplete/2026-02-26
191
197
  ```
192
198
 
193
199
  ### Conditional Caching
194
200
 
195
- You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:`. This logic can be defined in a method on the class and the name of the method as a symbol can be passed as well. **Note**: When using a symbol, the first argument, `target`, will not be passed but will be available as `self`.
201
+ You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:` (`target, method_name, method_args, **kwargs`). This logic can be defined in a method on the class and the name of the method as a symbol can be passed as well. **Note**: When using a symbol, the first argument, `target`, will not be passed but will be available as `self`.
196
202
 
197
203
  ```ruby
198
204
  # From examples/conditional_example.rb
@@ -204,18 +210,19 @@ require 'net/http'
204
210
  class GitHubApiAdapter
205
211
  include Cacheable
206
212
 
207
- cacheable :star_count, unless: :growing_fast?, key_format: ->(target, method_name, method_args) do
208
- [target.class, method_name, method_args.first].join('/')
213
+ cacheable :star_count, unless: :growing_fast?, key_format: ->(target, method_name, method_args, **kwargs) do
214
+ date = kwargs.fetch(:date, Time.now.strftime('%Y-%m-%d'))
215
+ [target.class, method_name, method_args.first, date].join('/')
209
216
  end
210
217
 
211
- def star_count(repo)
212
- puts "Fetching data from GitHub for #{repo}"
218
+ def star_count(repo, date: Time.now.strftime('%Y-%m-%d'))
219
+ puts "Fetching data from GitHub for #{repo} (as of #{date})"
213
220
  url = "https://api.github.com/repos/splitwise/#{repo}"
214
221
 
215
222
  JSON.parse(Net::HTTP.get(URI.parse(url)))['stargazers_count']
216
223
  end
217
224
 
218
- def growing_fast?(_method_name, method_args)
225
+ def growing_fast?(_method_name, method_args, **)
219
226
  method_args.first == 'cacheable'
220
227
  end
221
228
  end
@@ -226,17 +233,17 @@ Cacheable is new so we don't want to cache the number of stars it has as we expe
226
233
  ```irb
227
234
  > a = GitHubApiAdapter.new
228
235
  > a.star_count('tokenautocomplete')
229
- Fetching data from GitHub for tokenautocomplete
230
- => 1164
236
+ Fetching data from GitHub for tokenautocomplete (as of 2026-02-26)
237
+ => 1309
231
238
  a.star_count('tokenautocomplete')
232
- => 1164
239
+ => 1309
233
240
 
234
241
  > a.star_count('cacheable')
235
- Fetching data from GitHub for cacheable
236
- => 19
242
+ Fetching data from GitHub for cacheable (as of 2026-02-26)
243
+ => 58
237
244
  > a.star_count('cacheable')
238
- Fetching data from GitHub for cacheable
239
- => 19
245
+ Fetching data from GitHub for cacheable (as of 2026-02-26)
246
+ => 58
240
247
  ```
241
248
 
242
249
  ### Cache Options
@@ -247,6 +254,72 @@ If your cache backend supports options, you can pass them as the `cache_options:
247
254
  cacheable :with_options, cache_options: {expires_in: 3_600}
248
255
  ```
249
256
 
257
+ ### Memoization
258
+
259
+ By default, every call to a cached method hits the cache adapter, which includes deserialization. For methods where the deserialized object is expensive to reconstruct (e.g., large ActiveRecord collections), you can enable per-instance memoization so that repeated calls on the **same object** skip the adapter entirely:
260
+
261
+ ```ruby
262
+ # From examples/memoize_example.rb
263
+
264
+ class ExpensiveService
265
+ include Cacheable
266
+
267
+ cacheable :without_memoize
268
+
269
+ cacheable :with_memoize, memoize: true
270
+
271
+ def without_memoize
272
+ puts ' [method] computing value'
273
+ 42
274
+ end
275
+
276
+ def with_memoize
277
+ puts ' [method] computing value'
278
+ 42
279
+ end
280
+ end
281
+ ```
282
+
283
+ Using a logging adapter wrapper (see `examples/memoize_example.rb` for the full setup), the difference becomes clear:
284
+
285
+ ```
286
+ --- without memoize ---
287
+ [cache] fetch ["ExpensiveService", :without_memoize]
288
+ [method] computing value
289
+ [cache] fetch ["ExpensiveService", :without_memoize] <-- adapter hit again (deserialization cost)
290
+
291
+ --- with memoize: true ---
292
+ [cache] fetch ["ExpensiveService", :with_memoize]
293
+ [method] computing value
294
+ <-- no adapter hit on second call
295
+
296
+ --- after clearing ---
297
+ [cache] fetch ["ExpensiveService", :with_memoize] <-- adapter hit again after clear
298
+ [method] computing value
299
+ ```
300
+
301
+ **Important**: Memoized values persist for the lifetime of the object instance, and after the first call they bypass the cache adapter entirely. This means adapter-driven expiration (`expires_in`) and other backend invalidation mechanisms will **not** be re-checked while the instance stays alive. If your cache key changes (e.g., `cache_key` based on `updated_at`), the memoized value will also **not** automatically update. This is especially important for class-method memoization (where the "instance" is the class itself), because the memo can effectively outlive the cache TTL. Use `memoize: true` only when you know the value will not change for the lifetime of the instance (or class), or call `clear_#{method}_cache` explicitly when needed.
302
+
303
+ ### Per-Class Cache Adapter
304
+
305
+ By default, all classes use the global adapter set via `Cacheable.cache_adapter`. If you need a specific class to use a different cache backend, you can set one directly on the class:
306
+
307
+ ```ruby
308
+ class FrequentlyAccessedModel
309
+ include Cacheable
310
+
311
+ self.cache_adapter = MyFasterCache.new
312
+
313
+ cacheable :expensive_lookup
314
+
315
+ def expensive_lookup
316
+ # ...
317
+ end
318
+ end
319
+ ```
320
+
321
+ The class-level adapter takes precedence over the global adapter. Classes without their own adapter fall back to `Cacheable.cache_adapter` as usual.
322
+
250
323
  ### Flexible Options
251
324
 
252
325
  You can use the same options with multiple cache methods or limit them only to specific methods:
@@ -298,15 +371,15 @@ end
298
371
  ```irb
299
372
  > GitHubApiAdapter.star_count_for_cacheable
300
373
  Fetching data from GitHub for cacheable
301
- => 19
374
+ => 58
302
375
  > GitHubApiAdapter.star_count_for_cacheable
303
- => 19
376
+ => 58
304
377
 
305
378
  > GitHubApiAdapter.star_count_for_tokenautocomplete
306
379
  Fetching data from GitHub for tokenautocomplete
307
- => 1164
380
+ => 1309
308
381
  > GitHubApiAdapter.star_count_for_tokenautocomplete
309
- => 1164
382
+ => 1309
310
383
  ```
311
384
 
312
385
  ### Other Notes / Frequently Asked Questions
@@ -7,11 +7,11 @@ module Cacheable
7
7
 
8
8
  def self.extended(base)
9
9
  base.instance_variable_set(:@_cache_adapter, nil)
10
- base.cache_adapter = DEFAULT_ADAPTER
10
+ base.cache_adapter = DEFAULT_ADAPTER if base == Cacheable
11
11
  end
12
12
 
13
13
  def cache_adapter
14
- @_cache_adapter
14
+ @_cache_adapter || (self == Cacheable ? nil : Cacheable.cache_adapter)
15
15
  end
16
16
 
17
17
  def cache_adapter=(name_or_adapter)
@@ -1,42 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'monitor'
4
+
1
5
  module Cacheable
2
6
  module CacheAdapters
3
7
  class MemoryAdapter
4
8
  def initialize
9
+ @monitor = Monitor.new
5
10
  clear
6
11
  end
7
12
 
8
13
  def read(key)
9
- cache[key]
14
+ @monitor.synchronize { @cache[key] }
10
15
  end
11
16
 
12
17
  def write(key, value)
13
- cache[key] = value
18
+ @monitor.synchronize { @cache[key] = value }
14
19
  end
15
20
 
16
21
  def exist?(key)
17
- cache.key?(key)
22
+ @monitor.synchronize { @cache.key?(key) }
18
23
  end
19
24
 
25
+ # NOTE: yield is intentionally called inside the lock to prevent thundering herd — only one thread
26
+ # computes a missing value while others wait. This is acceptable for a simple in-memory adapter;
27
+ # production use cases needing high concurrency should use a real cache backend via CacheAdapter.
20
28
  def fetch(key, _options = {})
21
- return read(key) if exist?(key)
29
+ @monitor.synchronize do
30
+ return @cache[key] if @cache.key?(key)
22
31
 
23
- write(key, Proc.new.call)
32
+ @cache[key] = yield
33
+ end
24
34
  end
25
35
 
26
36
  def delete(key)
27
- return false unless exist?(key)
37
+ @monitor.synchronize do
38
+ return false unless @cache.key?(key)
28
39
 
29
- cache.delete key
30
- true
40
+ @cache.delete(key)
41
+ true
42
+ end
31
43
  end
32
44
 
33
45
  def clear
34
- @cache = {}
46
+ @monitor.synchronize { @cache = {} }
35
47
  end
36
-
37
- private
38
-
39
- attr_reader :cache
40
48
  end
41
49
  end
42
50
  end
@@ -14,7 +14,7 @@ module Cacheable
14
14
  private
15
15
 
16
16
  def class_name_for(string)
17
- string.split('_').map { |name_part| "#{name_part[0].upcase}#{name_part[1..-1].downcase}" }.join
17
+ string.split('_').map { |name_part| "#{name_part[0].upcase}#{name_part[1..].downcase}" }.join
18
18
  end
19
19
  end
20
20
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
-
5
3
  module Cacheable
6
4
  module MethodGenerator
7
5
  def cacheable(*original_method_names, **opts)
@@ -13,51 +11,72 @@ module Cacheable
13
11
  private
14
12
 
15
13
  def method_interceptor_module_name
16
- class_name = name&.gsub(/:/, '') || to_s.gsub(/[^a-zA-Z_0-9]/, '')
14
+ class_name = name&.gsub(':', '') || to_s.gsub(/[^a-zA-Z_0-9]/, '')
17
15
  "#{class_name}Cacher"
18
16
  end
19
17
 
20
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
18
+ # rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
21
19
  def create_cacheable_methods(original_method_name, opts = {})
22
20
  method_names = create_method_names(original_method_name)
23
21
  key_format_proc = opts[:key_format] || default_key_format
24
22
 
25
- const_get(method_interceptor_module_name).class_eval do
26
- define_method(method_names[:key_format_method_name]) do |*args|
27
- key_format_proc.call(self, original_method_name, args)
23
+ unless_proc = opts[:unless].is_a?(Symbol) ? opts[:unless].to_proc : opts[:unless]
24
+
25
+ @_cacheable_interceptor.class_eval do
26
+ define_method(method_names[:key_format_method_name]) do |*args, **kwargs|
27
+ key_format_proc.call(self, original_method_name, args, **kwargs)
28
28
  end
29
29
 
30
- define_method(method_names[:clear_cache_method_name]) do |*args|
31
- Cacheable.cache_adapter.delete(__send__(method_names[:key_format_method_name], *args))
30
+ define_method(method_names[:clear_cache_method_name]) do |*args, **kwargs|
31
+ cache_key = __send__(method_names[:key_format_method_name], *args, **kwargs)
32
+ @_cacheable_memoized&.dig(original_method_name)&.delete(cache_key) if opts[:memoize]
33
+ adapter = (is_a?(Module) ? singleton_class : self.class).cache_adapter
34
+ adapter.delete(cache_key)
32
35
  end
33
36
 
34
- define_method(method_names[:without_cache_method_name]) do |*args|
35
- original_method = method(original_method_name).super_method
36
- original_method.call(*args)
37
+ define_method(method_names[:without_cache_method_name]) do |*args, **kwargs, &block|
38
+ method(original_method_name).super_method.call(*args, **kwargs, &block)
37
39
  end
38
40
 
39
- define_method(method_names[:with_cache_method_name]) do |*args|
40
- Cacheable.cache_adapter.fetch(__send__(method_names[:key_format_method_name], *args), opts[:cache_options]) do
41
- __send__(method_names[:without_cache_method_name], *args)
41
+ define_method(method_names[:with_cache_method_name]) do |*args, **kwargs, &block|
42
+ cache_key = __send__(method_names[:key_format_method_name], *args, **kwargs)
43
+
44
+ if opts[:memoize]
45
+ method_memo = ((@_cacheable_memoized ||= {})[original_method_name] ||= {})
46
+ cached = method_memo.fetch(cache_key, Cacheable::MEMOIZE_NOT_SET)
47
+ return cached unless cached.equal?(Cacheable::MEMOIZE_NOT_SET)
42
48
  end
43
- end
44
49
 
45
- define_method(original_method_name) do |*args|
46
- unless_proc = opts[:unless].is_a?(Symbol) ? opts[:unless].to_proc : opts[:unless]
50
+ adapter = (is_a?(Module) ? singleton_class : self.class).cache_adapter
51
+ result = adapter.fetch(cache_key, opts[:cache_options]) do # rubocop:disable Lint/UselessDefaultValueArgument -- not Hash#fetch; second arg is cache options (e.g. expires_in) passed to the adapter
52
+ __send__(method_names[:without_cache_method_name], *args, **kwargs, &block)
53
+ end
54
+ method_memo[cache_key] = result if opts[:memoize]
55
+ result
56
+ end
47
57
 
48
- if unless_proc&.call(self, original_method_name, args)
49
- __send__(method_names[:without_cache_method_name], *args)
58
+ define_method(original_method_name) do |*args, **kwargs, &block|
59
+ if unless_proc&.call(self, original_method_name, args, **kwargs)
60
+ __send__(method_names[:without_cache_method_name], *args, **kwargs, &block)
50
61
  else
51
- __send__(method_names[:with_cache_method_name], *args)
62
+ __send__(method_names[:with_cache_method_name], *args, **kwargs, &block)
52
63
  end
53
64
  end
54
65
  end
55
66
  end
56
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
67
+ # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
57
68
 
58
69
  def default_key_format
59
- proc do |target, method_name, _method_args|
60
- # By default, we omit the _method_args from the cache key because there is no acceptable default behavior
70
+ warned = false
71
+
72
+ proc do |target, method_name, method_args, **kwargs|
73
+ if !warned && (!method_args.empty? || !kwargs.empty?)
74
+ warn "Cacheable WARNING: '#{method_name}' is using the default key format but was called with " \
75
+ 'arguments. Arguments are NOT included in the cache key, so different arguments will return ' \
76
+ 'the same cached value. Provide a :key_format proc to include arguments in the cache key.'
77
+ warned = true
78
+ end
79
+
61
80
  class_name = (target.is_a?(Module) ? target.name : target.class.name)
62
81
  cache_key = target.respond_to?(:cache_key) ? target.cache_key : class_name
63
82
  [cache_key, method_name].compact
@@ -66,7 +85,7 @@ module Cacheable
66
85
 
67
86
  def create_method_names(original_method_name)
68
87
  method_name_without_punctuation = original_method_name.to_s.sub(/([?!=])$/, '')
69
- punctuation = $LAST_PAREN_MATCH
88
+ punctuation = Regexp.last_match(-1)
70
89
 
71
90
  {
72
91
  with_cache_method_name: "#{method_name_without_punctuation}_with_cache#{punctuation}",
@@ -2,15 +2,13 @@
2
2
 
3
3
  module Cacheable
4
4
  module VERSION
5
- MAJOR = 1
6
- MINOR = 0
7
- TINY = 4
5
+ MAJOR = 2
6
+ MINOR = 1
7
+ TINY = 0
8
8
  PRE = nil
9
9
 
10
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.').freeze
11
-
12
10
  def self.to_s
13
- STRING
11
+ [MAJOR, MINOR, TINY, PRE].compact.join('.').freeze
14
12
  end
15
13
  end
16
14
  end
data/lib/cacheable.rb CHANGED
@@ -31,12 +31,18 @@ require 'cacheable/version'
31
31
  module Cacheable
32
32
  extend CacheAdapter
33
33
 
34
+ # Sentinel value to distinguish "not yet memoized" from a memoized nil/false.
35
+ MEMOIZE_NOT_SET = Object.new.freeze
36
+
34
37
  def self.included(base)
38
+ base.extend(Cacheable::CacheAdapter)
35
39
  base.extend(Cacheable::MethodGenerator)
36
40
 
37
41
  interceptor_name = base.send(:method_interceptor_module_name)
38
- remove_const(interceptor_name) if const_defined?(interceptor_name)
39
-
40
- base.prepend const_set(interceptor_name, Module.new)
42
+ interceptor = Module.new
43
+ interceptor.define_singleton_method(:to_s) { interceptor_name }
44
+ interceptor.define_singleton_method(:inspect) { interceptor_name }
45
+ base.instance_variable_set(:@_cacheable_interceptor, interceptor)
46
+ base.prepend interceptor
41
47
  end
42
48
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cacheable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jess Hottenstein
8
8
  - Ryan Laughlin
9
9
  - Aaron Rosenberg
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-07-31 00:00:00.000000000 Z
13
+ date: 2026-03-02 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Add caching simply without modifying your existing code. Includes configurable
16
16
  options for simple cache invalidation. See README on github for more information.
@@ -30,8 +30,9 @@ files:
30
30
  homepage: https://github.com/splitwise/cacheable
31
31
  licenses:
32
32
  - MIT
33
- metadata: {}
34
- post_install_message:
33
+ metadata:
34
+ rubygems_mfa_required: 'true'
35
+ post_install_message:
35
36
  rdoc_options: []
36
37
  require_paths:
37
38
  - lib
@@ -39,16 +40,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
40
  requirements:
40
41
  - - ">="
41
42
  - !ruby/object:Gem::Version
42
- version: 2.3.0
43
+ version: '3.3'
43
44
  required_rubygems_version: !ruby/object:Gem::Requirement
44
45
  requirements:
45
46
  - - ">="
46
47
  - !ruby/object:Gem::Version
47
48
  version: '0'
48
49
  requirements: []
49
- rubyforge_project:
50
- rubygems_version: 2.7.8
51
- signing_key:
50
+ rubygems_version: 3.5.22
51
+ signing_key:
52
52
  specification_version: 4
53
53
  summary: Add caching to any Ruby method in a aspect orientated programming approach.
54
54
  test_files: []