active_remote-cached 1.1.1 → 1.3.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.
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # 5.minutes below. The library does not require this itself.
6
+ require 'active_support/core_ext/numeric/time'
7
+
8
+ class ConfigurationClass
9
+ include ::ActiveRemote::Cached
10
+
11
+ def self.find(*)
12
+ :find_result
13
+ end
14
+
15
+ def self.search(*)
16
+ [:search_result]
17
+ end
18
+
19
+ cached_finders_for :guid
20
+ cached_finders_for %i[alpha beta]
21
+
22
+ # Declared twice on purpose, to prove cached_methods does not repeat itself.
23
+ cached_finders_for :guid
24
+ end
25
+
26
+ class AliasFinderClass
27
+ include ::ActiveRemote::Cached
28
+
29
+ def self.find(*)
30
+ :find_result
31
+ end
32
+
33
+ def self.search(*)
34
+ [:search_result]
35
+ end
36
+
37
+ cached_finders :guid
38
+ end
39
+
40
+ class ChildFinderClass < ConfigurationClass; end
41
+
42
+ class DurationOptionClass
43
+ include ::ActiveRemote::Cached
44
+
45
+ def self.find(*)
46
+ :find_result
47
+ end
48
+
49
+ def self.search(*)
50
+ [:search_result]
51
+ end
52
+
53
+ # 5.minutes has no literal form. The options must not be interpolated into
54
+ # the generated source.
55
+ cached_finders_for :guid, :expires_in => 5.minutes
56
+ end
57
+
58
+ class DurationChildClass < DurationOptionClass; end
59
+
60
+ describe ::ActiveRemote::Cached do
61
+ let(:versioned_prefix) { ::ActiveRemote::Cached::RUBY_AND_ACTIVE_SUPPORT_VERSION }
62
+
63
+ before do
64
+ ::ActiveRemote::Cached.cache(HashCache.new)
65
+ end
66
+
67
+ after do
68
+ ::ActiveRemote::Cached.default_options_overwrite({})
69
+ end
70
+
71
+ describe '.cache' do
72
+ it 'wraps the cache provider in a cache' do
73
+ expect(::ActiveRemote::Cached.cache).to be_a(::ActiveRemote::Cached::Cache)
74
+ end
75
+
76
+ it 'returns the current cache when no cache provider is given' do
77
+ cache = ::ActiveRemote::Cached.cache
78
+
79
+ expect(::ActiveRemote::Cached.cache).to be(cache)
80
+ end
81
+ end
82
+
83
+ describe '.default_options' do
84
+ it 'returns an empty hash when no options are set' do
85
+ expect(::ActiveRemote::Cached.default_options).to eq({})
86
+ end
87
+
88
+ it 'returns the options that were set' do
89
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
90
+
91
+ expect(::ActiveRemote::Cached.default_options).to eq(:expires_in => 100)
92
+ end
93
+
94
+ it 'keeps the current options when nil is given' do
95
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
96
+
97
+ expect(::ActiveRemote::Cached.default_options(nil)).to eq(:expires_in => 100)
98
+ end
99
+
100
+ it 'keeps the current options when an empty hash is given' do
101
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
102
+
103
+ expect(::ActiveRemote::Cached.default_options({})).to eq(:expires_in => 100)
104
+ end
105
+
106
+ # The Rails railtie sets :expires_in and :race_condition_ttl. An app
107
+ # initializer that sets one more option must not remove them.
108
+ it 'merges the given options into the current options' do
109
+ ::ActiveRemote::Cached.default_options(:expires_in => 100, :race_condition_ttl => 5)
110
+ ::ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true)
111
+
112
+ expect(::ActiveRemote::Cached.default_options).to eq(
113
+ :expires_in => 100,
114
+ :race_condition_ttl => 5,
115
+ :active_remote_cached_replace_characters => true
116
+ )
117
+ end
118
+
119
+ it 'lets the given options win over the current options' do
120
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
121
+ ::ActiveRemote::Cached.default_options(:expires_in => 200)
122
+
123
+ expect(::ActiveRemote::Cached.default_options).to eq(:expires_in => 200)
124
+ end
125
+
126
+ it 'does not change a hash that the caller passed' do
127
+ first = { :expires_in => 100 }
128
+ ::ActiveRemote::Cached.default_options(first)
129
+ ::ActiveRemote::Cached.default_options(:race_condition_ttl => 5)
130
+
131
+ expect(first).to eq(:expires_in => 100)
132
+ end
133
+
134
+ it 'gives each finder call the merged options' do
135
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
136
+ ::ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true)
137
+
138
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
139
+ [versioned_prefix, ConfigurationClass.name, '#find', 'guid.guid'],
140
+ { :expires_in => 100, :active_remote_cached_replace_characters => true }
141
+ ).and_return(:find_result)
142
+
143
+ ConfigurationClass.cached_find_by_guid(:guid)
144
+ end
145
+ end
146
+
147
+ describe '.default_options_overwrite' do
148
+ it 'replaces the current options' do
149
+ ::ActiveRemote::Cached.default_options(:expires_in => 100, :race_condition_ttl => 5)
150
+ ::ActiveRemote::Cached.default_options_overwrite(:active_remote_cached_replace_characters => true)
151
+
152
+ expect(::ActiveRemote::Cached.default_options).to eq(:active_remote_cached_replace_characters => true)
153
+ end
154
+
155
+ it 'clears the current options when an empty hash is given' do
156
+ ::ActiveRemote::Cached.default_options(:expires_in => 100)
157
+ ::ActiveRemote::Cached.default_options_overwrite({})
158
+
159
+ expect(::ActiveRemote::Cached.default_options).to eq({})
160
+ end
161
+
162
+ it 'returns the new options' do
163
+ expect(::ActiveRemote::Cached.default_options_overwrite(:expires_in => 100)).to eq(:expires_in => 100)
164
+ end
165
+ end
166
+
167
+ describe 'RUBY_AND_ACTIVE_SUPPORT_VERSION' do
168
+ it 'prefixes every cache key' do
169
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
170
+ [versioned_prefix, ConfigurationClass.name, '#find', 'guid.guid'], {}
171
+ ).and_return(:find_result)
172
+
173
+ ConfigurationClass.cached_find_by_guid(:guid)
174
+ end
175
+ end
176
+
177
+ describe '.cached_find' do
178
+ it 'calls find with the given arguments' do
179
+ expect(ConfigurationClass).to receive(:find).and_return(:hello)
180
+
181
+ expect(ConfigurationClass.cached_find(:guid => :guid)).to eq(:hello)
182
+ end
183
+
184
+ it 'sorts the argument keys before it builds the cache key' do
185
+ expect(ConfigurationClass).to receive(:cached_find_by_alpha_and_beta).with('A', 'B', {})
186
+
187
+ ConfigurationClass.cached_find(:beta => 'B', :alpha => 'A')
188
+ end
189
+
190
+ it 'yields to the block instead of calling find' do
191
+ expect(ConfigurationClass).not_to receive(:find)
192
+
193
+ expect(ConfigurationClass.cached_find(:guid => :guid) { :block_result }).to eq(:block_result)
194
+ end
195
+
196
+ it 'passes the options through to the fetch call' do
197
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
198
+ [versioned_prefix, ConfigurationClass.name, '#find', 'guid.guid'], { :expires_in => 200 }
199
+ ).and_return(:hello)
200
+
201
+ expect(ConfigurationClass.cached_find({ :guid => :guid }, :expires_in => 200)).to eq(:hello)
202
+ end
203
+ end
204
+
205
+ describe '.cached_search' do
206
+ it 'calls search with the given arguments' do
207
+ expect(ConfigurationClass).to receive(:search).and_return([:hello])
208
+
209
+ expect(ConfigurationClass.cached_search(:guid => :guid)).to eq([:hello])
210
+ end
211
+
212
+ it 'sorts the argument keys before it builds the cache key' do
213
+ expect(ConfigurationClass).to receive(:cached_search_by_alpha_and_beta).with('A', 'B', {})
214
+
215
+ ConfigurationClass.cached_search(:beta => 'B', :alpha => 'A')
216
+ end
217
+ end
218
+
219
+ describe '.cached_methods' do
220
+ it 'does not repeat a method when the same finder is declared twice' do
221
+ expect(ConfigurationClass.cached_methods).to eq(ConfigurationClass.cached_methods.uniq)
222
+ end
223
+ end
224
+
225
+ describe '.cached_finders' do
226
+ it 'creates the same methods as cached_finders_for' do
227
+ expect(AliasFinderClass).to respond_to(:cached_find_by_guid)
228
+ expect(AliasFinderClass).to respond_to(:cached_search_by_guid)
229
+ end
230
+ end
231
+
232
+ describe '.respond_to_missing?' do
233
+ it 'is true for a finder that was defined' do
234
+ expect(ConfigurationClass.respond_to?(:cached_find_by_alpha_and_beta)).to eq(true)
235
+ end
236
+
237
+ it 'is true for a finder whose arguments are in a different order' do
238
+ expect(ConfigurationClass.respond_to?(:cached_find_by_beta_and_alpha)).to eq(true)
239
+ end
240
+
241
+ it 'is false for a finder that was not defined' do
242
+ expect(ConfigurationClass.respond_to?(:cached_find_by_missing)).to eq(false)
243
+ end
244
+
245
+ it 'returns a method object for a finder in a different argument order' do
246
+ expect(ConfigurationClass.method(:cached_find_by_beta_and_alpha)).to be_a(::Method)
247
+ end
248
+ end
249
+
250
+ describe '.method_missing' do
251
+ it 'raises NoMethodError for a method that is not a finder' do
252
+ expect { ConfigurationClass.not_a_finder }.to raise_error(::NoMethodError)
253
+ end
254
+
255
+ it 'raises NoMethodError for a finder that was not defined' do
256
+ expect { ConfigurationClass.cached_find_by_missing(:value) }.to raise_error(::NoMethodError)
257
+ end
258
+ end
259
+
260
+ describe '._args_in_sorted_order' do
261
+ it 'reorders the arguments to match the defined method' do
262
+ expect(ConfigurationClass).to receive(:cached_find_by_alpha_and_beta).with('A', 'B')
263
+
264
+ ConfigurationClass.cached_find_by_beta_and_alpha('B', 'A')
265
+ end
266
+
267
+ it 'passes the options hash through as the last argument' do
268
+ expect(ConfigurationClass).to receive(:cached_find_by_alpha_and_beta).with('A', 'B', { :expires_in => 10 })
269
+
270
+ ConfigurationClass.cached_find_by_beta_and_alpha('B', 'A', { :expires_in => 10 })
271
+ end
272
+
273
+ # A missing argument used to become nil, and the finder then cached a
274
+ # record under a key built from that nil.
275
+ it 'raises ArgumentError when an argument is missing' do
276
+ expect do
277
+ ConfigurationClass.cached_find_by_beta_and_alpha('B')
278
+ end.to raise_error(::ArgumentError, /given 1, expected 2/)
279
+ end
280
+ end
281
+
282
+ describe '._method_missing_name' do
283
+ it 'returns nil when the method name is not a finder' do
284
+ expect(ConfigurationClass._method_missing_name(:not_a_finder)).to be_nil
285
+ end
286
+
287
+ it 'sorts the argument names in the method name' do
288
+ expect(ConfigurationClass._method_missing_name(:cached_find_by_beta_and_alpha)).to eq(
289
+ :cached_find_by_alpha_and_beta
290
+ )
291
+ end
292
+
293
+ # The regex has no anchors, so it matches a finder name inside a longer
294
+ # name. This records the current behavior. See the note in the README.
295
+ it 'matches a finder name that is a suffix of a longer method name' do
296
+ expect(ConfigurationClass._method_missing_name(:not_cached_find_by_guid)).to eq(
297
+ :cached_find_by_guid
298
+ )
299
+ end
300
+ end
301
+
302
+ describe 'a finder whose definition fails' do
303
+ let(:broken_class) do
304
+ Class.new do
305
+ include ::ActiveRemote::Cached
306
+
307
+ def self.find(*)
308
+ :find_result
309
+ end
310
+
311
+ def self.search(*)
312
+ [:search_result]
313
+ end
314
+ end
315
+ end
316
+
317
+ # cached_methods used to be written before class_eval ran, so a failure
318
+ # registered a name with no method behind it. method_missing then
319
+ # dispatched to that same missing name and recursed until the stack ran out.
320
+ it 'registers no method name' do
321
+ expect { broken_class.cached_finders_for :'bad-name' }.to raise_error(::SyntaxError)
322
+
323
+ expect(broken_class.cached_methods).to eq([])
324
+ end
325
+
326
+ it 'raises NoMethodError rather than recursing' do
327
+ begin
328
+ broken_class.cached_finders_for :'bad-name'
329
+ rescue ::SyntaxError # rubocop:disable Lint/SuppressedException
330
+ end
331
+
332
+ expect { broken_class.cached_delete_by_guid('x') }.to raise_error(::NoMethodError)
333
+ end
334
+ end
335
+
336
+ describe 'a finder declared with an option that has no literal form' do
337
+ it 'defines the finder methods' do
338
+ expect(DurationOptionClass).to respond_to(:cached_find_by_guid)
339
+ expect(DurationOptionClass).to respond_to(:cached_search_by_guid)
340
+ end
341
+
342
+ it 'passes the option through to the fetch call' do
343
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
344
+ [versioned_prefix, DurationOptionClass.name, '#find', 'guid.guid'], { :expires_in => 5.minutes }
345
+ ).and_return(:hello)
346
+
347
+ expect(DurationOptionClass.cached_find_by_guid(:guid)).to eq(:hello)
348
+ end
349
+
350
+ it 'lets a local option override the declared option' do
351
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
352
+ [versioned_prefix, DurationOptionClass.name, '#find', 'guid.guid'], { :expires_in => 200 }
353
+ ).and_return(:hello)
354
+
355
+ expect(DurationOptionClass.cached_find_by_guid(:guid, :expires_in => 200)).to eq(:hello)
356
+ end
357
+
358
+ it 'applies the declared option in a subclass' do
359
+ expect(::ActiveRemote::Cached.cache).to receive(:fetch).with(
360
+ [versioned_prefix, DurationChildClass.name, '#find', 'guid.guid'], { :expires_in => 5.minutes }
361
+ ).and_return(:hello)
362
+
363
+ expect(DurationChildClass.cached_find_by_guid(:guid)).to eq(:hello)
364
+ end
365
+ end
366
+
367
+ describe 'a subclass of a class with cached finders' do
368
+ it 'responds to the finders the parent defined' do
369
+ expect(ChildFinderClass).to respond_to(:cached_find_by_alpha_and_beta)
370
+ end
371
+
372
+ it 'calls the finders the parent defined' do
373
+ expect(ChildFinderClass).to receive(:find).and_return(:hello)
374
+
375
+ expect(ChildFinderClass.cached_find_by_alpha_and_beta('A', 'B')).to eq(:hello)
376
+ end
377
+
378
+ it 'has an empty cached_methods list of its own' do
379
+ expect(ChildFinderClass.cached_methods).to eq([])
380
+ end
381
+
382
+ # method_missing reads cached_methods, which is empty on the subclass, so
383
+ # the subclass cannot reorder the arguments the way the parent can.
384
+ it 'does not accept the finder arguments in a different order' do
385
+ expect(ConfigurationClass.cached_find_by_beta_and_alpha('B', 'A')).to eq(:find_result)
386
+
387
+ expect { ChildFinderClass.cached_find_by_beta_and_alpha('B', 'A') }.to raise_error(::NoMethodError)
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+
6
+ # `gem build` evals the gemspec inside Gem::Specification, but Bundler evals it
7
+ # at the top level. Bundler has already loaded the gemspec in this process, so
8
+ # a top-level def or constant from that load would hide the bug here. Load it
9
+ # in a fresh ruby without Bundler instead.
10
+ describe 'active_remote-cached.gemspec' do
11
+ let(:root) { File.expand_path('..', __dir__) }
12
+ let(:script) do
13
+ <<~RUBY
14
+ spec = Gem::Specification.load('active_remote-cached.gemspec')
15
+ abort 'Gem::Specification.load returned nil' unless spec
16
+ spec.validate
17
+ puts spec.full_name
18
+ RUBY
19
+ end
20
+
21
+ it 'loads and validates with Rubygems alone' do
22
+ stdout, stderr, status = ::Bundler.with_unbundled_env do
23
+ ::Open3.capture3(::RbConfig.ruby, '-e', script, :chdir => root)
24
+ end
25
+
26
+ expect(status).to be_success, stderr
27
+ expect(stdout).to eq("active_remote-cached-#{::ActiveRemote::Cached::VERSION}\n")
28
+ end
29
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ostruct'
3
4
  require 'rubygems'
4
5
  require 'bundler'
5
6
  Bundler.require(:default, :development, :test)
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_remote-cached
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Dewitt
8
8
  - MXDevExperience
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-06-10 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: active_remote
@@ -31,9 +30,23 @@ dependencies:
31
30
  requirements:
32
31
  - - ">="
33
32
  - !ruby/object:Gem::Version
34
- version: '0'
33
+ version: '6.1'
35
34
  type: :runtime
36
35
  prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: appraisal
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
37
50
  version_requirements: !ruby/object:Gem::Requirement
38
51
  requirements:
39
52
  - - ">="
@@ -67,6 +80,20 @@ dependencies:
67
80
  - - ">="
68
81
  - !ruby/object:Gem::Version
69
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ostruct
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
70
97
  - !ruby/object:Gem::Dependency
71
98
  name: pry
72
99
  requirement: !ruby/object:Gem::Requirement
@@ -113,16 +140,16 @@ dependencies:
113
140
  name: rubocop
114
141
  requirement: !ruby/object:Gem::Requirement
115
142
  requirements:
116
- - - ">="
143
+ - - "~>"
117
144
  - !ruby/object:Gem::Version
118
- version: '0'
145
+ version: '1.80'
119
146
  type: :development
120
147
  prerelease: false
121
148
  version_requirements: !ruby/object:Gem::Requirement
122
149
  requirements:
123
- - - ">="
150
+ - - "~>"
124
151
  - !ruby/object:Gem::Version
125
- version: '0'
152
+ version: '1.80'
126
153
  description: ' Provides "cached" finders and a DSL to enumerate which finders should
127
154
  have cached versions '
128
155
  email:
@@ -132,8 +159,10 @@ executables: []
132
159
  extensions: []
133
160
  extra_rdoc_files: []
134
161
  files:
162
+ - ".github/workflows/ci.yml"
135
163
  - ".gitignore"
136
164
  - ".rubocop.yml"
165
+ - Appraisals
137
166
  - Gemfile
138
167
  - LICENSE.txt
139
168
  - README.md
@@ -151,11 +180,16 @@ files:
151
180
  - spec/active_remote/cached_exist_methods_spec.rb
152
181
  - spec/active_remote/cached_find_methods_spec.rb
153
182
  - spec/active_remote/cached_search_methods_spec.rb
183
+ - spec/active_remote/cached_spec.rb
184
+ - spec/gemspec_spec.rb
154
185
  - spec/spec_helper.rb
155
- homepage: ''
156
- licenses: []
157
- metadata: {}
158
- post_install_message:
186
+ homepage: https://github.com/mxenabled/active_remote-cached
187
+ licenses:
188
+ - MIT
189
+ metadata:
190
+ homepage_uri: https://github.com/mxenabled/active_remote-cached
191
+ source_code_uri: https://github.com/mxenabled/active_remote-cached
192
+ rubygems_mfa_required: 'true'
159
193
  rdoc_options: []
160
194
  require_paths:
161
195
  - lib
@@ -163,23 +197,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
197
  requirements:
164
198
  - - ">="
165
199
  - !ruby/object:Gem::Version
166
- version: '2.6'
200
+ version: '3.1'
167
201
  required_rubygems_version: !ruby/object:Gem::Requirement
168
202
  requirements:
169
203
  - - ">="
170
204
  - !ruby/object:Gem::Version
171
205
  version: '0'
172
206
  requirements: []
173
- rubygems_version: 3.1.6
174
- signing_key:
207
+ rubygems_version: 4.0.15
175
208
  specification_version: 4
176
209
  summary: Provides a configuration for caching mechanisms and finders on ActiveRemote
177
210
  models
178
- test_files:
179
- - spec/active_remote/cached/argument_keys_spec.rb
180
- - spec/active_remote/cached/cache_spec.rb
181
- - spec/active_remote/cached_delete_methods_spec.rb
182
- - spec/active_remote/cached_exist_methods_spec.rb
183
- - spec/active_remote/cached_find_methods_spec.rb
184
- - spec/active_remote/cached_search_methods_spec.rb
185
- - spec/spec_helper.rb
211
+ test_files: []