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 +4 -4
- data/README.md +113 -40
- data/lib/cacheable/cache_adapter.rb +2 -2
- data/lib/cacheable/cache_adapters/memory_adapter.rb +21 -13
- data/lib/cacheable/cache_adapters.rb +1 -1
- data/lib/cacheable/method_generator.rb +44 -25
- data/lib/cacheable/version.rb +4 -6
- data/lib/cacheable.rb +9 -3
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f7c4005f6683ae5c61de0d34d3795ac4607e6bb001e7f73065b947559720662
|
|
4
|
+
data.tar.gz: '0439c9b90265170df3706406c52c896fb8953c7e5737c9027ce9d140dcef6aed'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c08c9cdc5d6b3a9811b072df3feae84cf87dc7cadccee8fafc654b15de24d240f0ddb2c4a1bbee879765e11fef04e69cd6b81d126ffe96e9410eea8436fb239a
|
|
7
|
+
data.tar.gz: 384da8f24a3cdd069c9f634d31a2ef77bb1b83083aa9ca4f5d0adce364c172cf04efd0ad235373b66cb648e196cb04804d0ada2ac78400b7b5cdf75b214c1db6
|
data/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Cacheable
|
|
2
2
|
|
|
3
|
+
[](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
|
-
=>
|
|
85
|
+
=> 58
|
|
82
86
|
> a.star_count
|
|
83
|
-
=>
|
|
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
|
-
=>
|
|
105
|
+
=> 58
|
|
102
106
|
> a.star_count_without_cache
|
|
103
107
|
Fetching data from GitHub
|
|
104
|
-
=>
|
|
108
|
+
=> 58
|
|
105
109
|
> a.star_count
|
|
106
|
-
=>
|
|
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
|
-
=>
|
|
121
|
+
=> 58
|
|
118
122
|
> a.star_count
|
|
119
|
-
=>
|
|
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
|
-
=>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
=>
|
|
184
|
+
Fetching data from GitHub for cacheable (as of 2026-02-26)
|
|
185
|
+
=> 58
|
|
180
186
|
> a.star_count('cacheable')
|
|
181
|
-
=>
|
|
187
|
+
=> 58
|
|
182
188
|
> a.star_count('tokenautocomplete')
|
|
183
|
-
Fetching data from GitHub for tokenautocomplete
|
|
184
|
-
=>
|
|
189
|
+
Fetching data from GitHub for tokenautocomplete (as of 2026-02-26)
|
|
190
|
+
=> 1309
|
|
185
191
|
> a.star_count('tokenautocomplete')
|
|
186
|
-
=>
|
|
192
|
+
=> 1309
|
|
187
193
|
|
|
188
194
|
# In this example the follow cache keys are generated:
|
|
189
|
-
# GitHubApiAdapter/star_count/cacheable/
|
|
190
|
-
# GitHubApiAdapter/star_count/tokenautocomplete/
|
|
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
|
|
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
|
-
|
|
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
|
-
=>
|
|
236
|
+
Fetching data from GitHub for tokenautocomplete (as of 2026-02-26)
|
|
237
|
+
=> 1309
|
|
231
238
|
a.star_count('tokenautocomplete')
|
|
232
|
-
=>
|
|
239
|
+
=> 1309
|
|
233
240
|
|
|
234
241
|
> a.star_count('cacheable')
|
|
235
|
-
Fetching data from GitHub for cacheable
|
|
236
|
-
=>
|
|
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
|
-
=>
|
|
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
|
-
=>
|
|
374
|
+
=> 58
|
|
302
375
|
> GitHubApiAdapter.star_count_for_cacheable
|
|
303
|
-
=>
|
|
376
|
+
=> 58
|
|
304
377
|
|
|
305
378
|
> GitHubApiAdapter.star_count_for_tokenautocomplete
|
|
306
379
|
Fetching data from GitHub for tokenautocomplete
|
|
307
|
-
=>
|
|
380
|
+
=> 1309
|
|
308
381
|
> GitHubApiAdapter.star_count_for_tokenautocomplete
|
|
309
|
-
=>
|
|
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
|
-
|
|
29
|
+
@monitor.synchronize do
|
|
30
|
+
return @cache[key] if @cache.key?(key)
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
@cache[key] = yield
|
|
33
|
+
end
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def delete(key)
|
|
27
|
-
|
|
37
|
+
@monitor.synchronize do
|
|
38
|
+
return false unless @cache.key?(key)
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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 =
|
|
88
|
+
punctuation = Regexp.last_match(-1)
|
|
70
89
|
|
|
71
90
|
{
|
|
72
91
|
with_cache_method_name: "#{method_name_without_punctuation}_with_cache#{punctuation}",
|
data/lib/cacheable/version.rb
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Cacheable
|
|
4
4
|
module VERSION
|
|
5
|
-
MAJOR =
|
|
6
|
-
MINOR =
|
|
7
|
-
TINY =
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
50
|
-
|
|
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: []
|