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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support'
2
4
  require 'active_support/cache'
3
5
  require 'active_support/concern'
@@ -19,23 +21,60 @@ module ActiveRemote
19
21
  RUBY_AND_ACTIVE_SUPPORT_VERSION = "#{RUBY_ENGINE_VERSION}:#{ActiveSupport::VERSION::STRING}".freeze
20
22
 
21
23
  def self.cache(cache_provider = nil)
22
- @cache_provider = ::ActiveRemote::Cached::Cache.new(cache_provider) if cache_provider
24
+ if cache_provider
25
+ nested_caching = @cache_provider&.nested_caching?
26
+ @cache_provider = ::ActiveRemote::Cached::Cache.new(cache_provider)
27
+ # A new Cache starts with nested caching off, so carry the setting over.
28
+ @cache_provider.enable_nested_caching! if nested_caching
29
+ end
23
30
 
24
31
  @cache_provider
25
32
  end
26
33
 
34
+ # Merges the given options into the current options, and returns the
35
+ # result. A later call adds to the options, so an app initializer keeps the
36
+ # :expires_in and :race_condition_ttl that the Rails railtie set before it.
27
37
  def self.default_options(options = nil)
28
- @default_options = options if options
38
+ @default_options = default_options.merge(options) if options
29
39
 
30
40
  @default_options || {}
31
41
  end
32
42
 
43
+ # Replaces the current options with the given options, and returns them.
44
+ # Pass an empty hash to clear the options.
45
+ def self.default_options_overwrite(options)
46
+ @default_options = options.dup
47
+ end
48
+
33
49
  module ClassMethods
34
50
  def cached_methods
35
51
  @cached_methods ||= []
36
52
  @cached_methods
37
53
  end
38
54
 
55
+ # The options each cached finder was declared with, keyed by method name.
56
+ # The generated methods read this at call time. Interpolating the options
57
+ # into the generated source instead would require every value to have a
58
+ # literal form, and a value such as 5.minutes does not.
59
+ def cached_finder_options
60
+ @cached_finder_options ||= {}
61
+ end
62
+
63
+ # A subclass inherits the finder methods its parent defined, but not the
64
+ # parent registry, so the lookup walks up the superclass chain.
65
+ def _cached_finder_options_for(method_name)
66
+ klass = self
67
+
68
+ while klass.respond_to?(:cached_finder_options)
69
+ options = klass.cached_finder_options[method_name]
70
+ return options if options
71
+
72
+ klass = klass.superclass
73
+ end
74
+
75
+ {}
76
+ end
77
+
39
78
  def cached_finders_for(*cached_finder_keys)
40
79
  options = cached_finder_keys.extract_options!
41
80
 
@@ -103,37 +142,38 @@ module ActiveRemote
103
142
  "#{::Regexp.last_match(1)}#{params.sort.join('_and_')}#{::Regexp.last_match(3)}".to_sym
104
143
  end
105
144
 
106
- # rubocop:disable Metrics/AbcSize
107
145
  def _args_in_sorted_order(m, args)
108
146
  regex = /cached_(?:delete|exist_search|search|exist_find|find)_by_([0-9a-zA-Z_]*)(!|\?)?/
109
147
 
110
- method_name = _method_missing_name(m)
148
+ called_match = m.match(regex)
149
+ sorted_match = _method_missing_name(m).match(regex)
111
150
 
112
- match_1 = m.match(regex)
113
- match_2 = method_name.match(regex)
151
+ return args unless called_match[1] && sorted_match[1]
114
152
 
115
- args_in_order = []
153
+ called_field_names = called_match[1].split('_and_')
154
+ sorted_field_names = sorted_match[1].split('_and_')
116
155
 
117
- if match_1[1] && match_2[1]
118
- orignal_args_name = match_1[1].split('_and_')
119
- args_names_in_order = match_2[1].split('_and_')
156
+ _reorder_args(m, args, called_field_names, sorted_field_names)
157
+ end
120
158
 
121
- args_names_in_order.each do |arg_name|
122
- index = orignal_args_name.index(arg_name)
123
- args_in_order << args[index]
124
- end
159
+ def _reorder_args(m, args, called_field_names, sorted_field_names)
160
+ if args.size < called_field_names.size
161
+ raise ::ArgumentError,
162
+ "wrong number of arguments to #{m} (given #{args.size}, expected #{called_field_names.size})"
163
+ end
125
164
 
126
- if args.size > args_in_order.size
127
- # Add options if passed
128
- args_in_order << args.last
129
- end
165
+ args_in_order = sorted_field_names.map do |field_name|
166
+ index = called_field_names.index(field_name)
167
+ raise ::ArgumentError, "#{m} has no argument named #{field_name}" if index.nil?
130
168
 
131
- args_in_order
132
- else
133
- args
169
+ args[index]
134
170
  end
171
+
172
+ # Add the options hash if the caller passed one.
173
+ args_in_order << args.last if args.size > called_field_names.size
174
+
175
+ args_in_order
135
176
  end
136
- # rubocop:enable Metrics/AbcSize
137
177
 
138
178
  # rubocop:disable Metrics/AbcSize
139
179
  def _create_cached_finder_for(cached_finder_key, options = {})
@@ -192,25 +232,41 @@ module ActiveRemote
192
232
  "cached_search_by_#{arguments.sort.join('_and_')}"
193
233
  end
194
234
 
195
- def _define_cached_delete_method(method_name, *method_arguments, cached_finder_options)
235
+ def _expanded_search_args(method_arguments)
236
+ method_arguments.map { |method_argument| ":#{method_argument} => #{method_argument}," }.join
237
+ end
238
+
239
+ # Returns the source text that builds the cache key for a generated
240
+ # method. The key names each field, so that two finders that take the
241
+ # same values do not share one cache entry.
242
+ def _expanded_cache_key_args(method_arguments)
243
+ sorted_arguments = method_arguments.sort
244
+ field_names = sorted_arguments.map { |argument| ":#{argument}" }.join(',')
245
+
246
+ '::ActiveRemote::Cached::ArgumentKeys.for_fields(' \
247
+ "[#{field_names}], [#{sorted_arguments.join(',')}], __active_remote_cached_options" \
248
+ ').cache_key'
249
+ end
250
+
251
+ def _define_cached_delete_method(method_name, *method_arguments, cached_finder_options_hash)
196
252
  method_arguments.flatten!
197
253
  expanded_method_args = method_arguments.join(',')
198
- sorted_method_args = method_arguments.sort.join(',')
199
- cached_methods << method_name
254
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
200
255
 
201
256
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
202
257
  # def self.cached_delete_by_user_guid(user_guid, options = {})
203
258
  # ::ActiveRemote::Cached.cache.delete([name, user_guid])
204
259
  # end
205
260
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
206
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
261
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
207
262
  namespace = __active_remote_cached_options.delete(:namespace)
263
+ argument_cache_key = #{expanded_cache_key_args}
208
264
  find_cache_key = [
209
265
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
210
266
  namespace,
211
267
  name,
212
268
  "#find",
213
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
269
+ argument_cache_key
214
270
  ].compact
215
271
 
216
272
  search_cache_key = [
@@ -218,35 +274,36 @@ module ActiveRemote
218
274
  namespace,
219
275
  name,
220
276
  "#search",
221
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
277
+ argument_cache_key
222
278
  ].compact
223
279
 
224
280
  ::ActiveRemote::Cached.cache.delete(find_cache_key)
225
281
  ::ActiveRemote::Cached.cache.delete(search_cache_key)
226
282
  end
227
283
  RUBY
284
+
285
+ cached_finder_options[method_name] = cached_finder_options_hash
286
+ cached_methods << method_name
228
287
  end
229
288
 
230
- def _define_cached_exist_find_method(method_name, *method_arguments, cached_finder_options)
289
+ def _define_cached_exist_find_method(method_name, *method_arguments, cached_finder_options_hash)
231
290
  method_arguments.flatten!
232
291
  expanded_method_args = method_arguments.join(',')
233
- sorted_method_args = method_arguments.sort.join(',')
234
- cached_methods << method_name
235
- cached_methods << "#{method_name}?"
292
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
236
293
 
237
294
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
238
295
  # def self.cached_exist_find_by_user_guid(user_guid, options = {})
239
296
  # ::ActiveRemote::Cached.cache.exist?([name, user_guid])
240
297
  # end
241
298
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
242
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
299
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
243
300
  namespace = __active_remote_cached_options.delete(:namespace)
244
301
  cache_key = [
245
302
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
246
303
  namespace,
247
304
  name,
248
305
  "#find",
249
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
306
+ #{expanded_cache_key_args}
250
307
  ].compact
251
308
 
252
309
  ::ActiveRemote::Cached.cache.exist?(cache_key)
@@ -254,28 +311,30 @@ module ActiveRemote
254
311
  RUBY
255
312
 
256
313
  singleton_class.send(:alias_method, "#{method_name}?", method_name)
314
+
315
+ cached_finder_options[method_name] = cached_finder_options_hash
316
+ cached_methods << method_name
317
+ cached_methods << "#{method_name}?"
257
318
  end
258
319
 
259
- def _define_cached_exist_search_method(method_name, *method_arguments, cached_finder_options)
320
+ def _define_cached_exist_search_method(method_name, *method_arguments, cached_finder_options_hash)
260
321
  method_arguments.flatten!
261
322
  expanded_method_args = method_arguments.join(',')
262
- sorted_method_args = method_arguments.sort.join(',')
263
- cached_methods << method_name
264
- cached_methods << "#{method_name}?"
323
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
265
324
 
266
325
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
267
326
  # def self.cached_exist_search_by_user_guid(user_guid, options = {})
268
327
  # ::ActiveRemote::Cached.cache.exist?([namespace, name, "#search", user_guid])
269
328
  # end
270
329
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
271
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
330
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
272
331
  namespace = __active_remote_cached_options.delete(:namespace)
273
332
  cache_key = [
274
333
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
275
334
  namespace,
276
335
  name,
277
336
  "#search",
278
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
337
+ #{expanded_cache_key_args}
279
338
  ].compact
280
339
 
281
340
  ::ActiveRemote::Cached.cache.exist?(cache_key)
@@ -283,18 +342,18 @@ module ActiveRemote
283
342
  RUBY
284
343
 
285
344
  singleton_class.send(:alias_method, "#{method_name}?", method_name)
345
+
346
+ cached_finder_options[method_name] = cached_finder_options_hash
347
+ cached_methods << method_name
348
+ cached_methods << "#{method_name}?"
286
349
  end
287
350
 
288
- def _define_cached_find_method(method_name, *method_arguments, cached_finder_options)
351
+ def _define_cached_find_method(method_name, *method_arguments, cached_finder_options_hash)
289
352
  method_arguments.flatten!
290
353
  expanded_method_args = method_arguments.join(',')
291
- sorted_method_args = method_arguments.sort.join(',')
292
- cached_methods << method_name
354
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
293
355
 
294
- expanded_search_args = ''
295
- method_arguments.each do |method_argument|
296
- expanded_search_args << ":#{method_argument} => #{method_argument},"
297
- end
356
+ expanded_search_args = _expanded_search_args(method_arguments)
298
357
 
299
358
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
300
359
  # def self.cached_find_by_user_guid(user_guid, options = {})
@@ -309,14 +368,14 @@ module ActiveRemote
309
368
  # of the result object is maintained for requests/responses
310
369
  #
311
370
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
312
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
371
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
313
372
  namespace = __active_remote_cached_options.delete(:namespace)
314
373
  cache_key = [
315
374
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
316
375
  namespace,
317
376
  name,
318
377
  "#find",
319
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
378
+ #{expanded_cache_key_args}
320
379
  ].compact
321
380
 
322
381
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -328,18 +387,17 @@ module ActiveRemote
328
387
  end
329
388
  end
330
389
  RUBY
390
+
391
+ cached_finder_options[method_name] = cached_finder_options_hash
392
+ cached_methods << method_name
331
393
  end
332
394
 
333
- def _define_cached_search_method(method_name, *method_arguments, cached_finder_options)
395
+ def _define_cached_search_method(method_name, *method_arguments, cached_finder_options_hash)
334
396
  method_arguments.flatten!
335
397
  expanded_method_args = method_arguments.join(',')
336
- sorted_method_args = method_arguments.sort.join(',')
337
- cached_methods << method_name
398
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
338
399
 
339
- expanded_search_args = ''
340
- method_arguments.each do |method_argument|
341
- expanded_search_args << ":#{method_argument} => #{method_argument},"
342
- end
400
+ expanded_search_args = _expanded_search_args(method_arguments)
343
401
 
344
402
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
345
403
  # def self.cached_search_by_user_guid(user_guid, options = {})
@@ -358,14 +416,14 @@ module ActiveRemote
358
416
  # of the result object is maintained for requests/responses
359
417
  #
360
418
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
361
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
419
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
362
420
  namespace = __active_remote_cached_options.delete(:namespace)
363
421
  cache_key = [
364
422
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
365
423
  namespace,
366
424
  name,
367
425
  "#search",
368
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
426
+ #{expanded_cache_key_args}
369
427
  ].compact
370
428
 
371
429
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -377,18 +435,17 @@ module ActiveRemote
377
435
  end
378
436
  end
379
437
  RUBY
438
+
439
+ cached_finder_options[method_name] = cached_finder_options_hash
440
+ cached_methods << method_name
380
441
  end
381
442
 
382
- def _define_cached_search_bang_method(method_name, *method_arguments, cached_finder_options)
443
+ def _define_cached_search_bang_method(method_name, *method_arguments, cached_finder_options_hash)
383
444
  method_arguments.flatten!
384
445
  expanded_method_args = method_arguments.join(',')
385
- sorted_method_args = method_arguments.sort.join(',')
386
- cached_methods << method_name
446
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
387
447
 
388
- expanded_search_args = ''
389
- method_arguments.each do |method_argument|
390
- expanded_search_args << ":#{method_argument} => #{method_argument},"
391
- end
448
+ expanded_search_args = _expanded_search_args(method_arguments)
392
449
 
393
450
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
394
451
  # def self.cached_search_by_user_guid!(user_guid, options = {})
@@ -403,7 +460,7 @@ module ActiveRemote
403
460
  # results = self.search(:user_guid => user_guid)
404
461
  # end
405
462
  #
406
- # raise ::ActiveRemote::RemoteRecordNotFound.new(self.class) if results.size <= 0
463
+ # raise ::ActiveRemote::RemoteRecordNotFound, self if results.nil? || results.first.nil?
407
464
  # results
408
465
  # end
409
466
  # end
@@ -412,14 +469,14 @@ module ActiveRemote
412
469
  # of the result object is maintained for requests/responses
413
470
  #
414
471
  def self.#{method_name}(#{expanded_method_args}, __active_remote_cached_options = {})
415
- __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(#{cached_finder_options}).merge(__active_remote_cached_options)
472
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
416
473
  namespace = __active_remote_cached_options.delete(:namespace)
417
474
  cache_key = [
418
475
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
419
476
  namespace,
420
477
  name,
421
478
  "#search",
422
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
479
+ #{expanded_cache_key_args}
423
480
  ].compact
424
481
 
425
482
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -431,11 +488,14 @@ module ActiveRemote
431
488
  results = self.search(#{expanded_search_args})
432
489
  end
433
490
 
434
- raise ::ActiveRemote::RemoteRecordNotFound.new(self.class) if results.first.nil?
491
+ raise ::ActiveRemote::RemoteRecordNotFound, self if results.nil? || results.first.nil?
435
492
  results
436
493
  end
437
494
  end
438
495
  RUBY
496
+
497
+ cached_finder_options[method_name] = cached_finder_options_hash
498
+ cached_methods << method_name
439
499
  end
440
500
  end
441
501
 
@@ -24,4 +24,128 @@ describe ::ActiveRemote::Cached::ArgumentKeys do
24
24
  options = { :active_remote_cached_replace_characters => true }
25
25
  expect(::ActiveRemote::Cached::ArgumentKeys.new('hello {}', options).cache_key).to eq('helloSPLBRB')
26
26
  end
27
+
28
+ it 'replaces a tab when :active_remote_cached_replace_characters' do
29
+ options = { :active_remote_cached_replace_characters => true }
30
+ expect(::ActiveRemote::Cached::ArgumentKeys.new("a\tb", options).cache_key).to eq('aTBb')
31
+ end
32
+
33
+ it 'replaces a newline when :active_remote_cached_replace_characters' do
34
+ options = { :active_remote_cached_replace_characters => true }
35
+ expect(::ActiveRemote::Cached::ArgumentKeys.new("a\nb", options).cache_key).to eq('aNLb')
36
+ end
37
+
38
+ # REMOVE_CHARACTERS uses [[:space:]], so both options must cover the same
39
+ # whitespace. A raw tab or newline in a key breaks the Memcached protocol.
40
+ it 'leaves no whitespace in the key under either option' do
41
+ argument = "a b\tc\nd\re\ff\vg"
42
+
43
+ removed = ::ActiveRemote::Cached::ArgumentKeys.new(
44
+ argument, :active_remote_cached_remove_characters => true
45
+ ).cache_key
46
+ replaced = ::ActiveRemote::Cached::ArgumentKeys.new(
47
+ argument, :active_remote_cached_replace_characters => true
48
+ ).cache_key
49
+
50
+ expect(removed).not_to match(/[[:space:]]/)
51
+ expect(replaced).not_to match(/[[:space:]]/)
52
+ end
53
+
54
+ it 'joins multiple arguments into one key' do
55
+ expect(::ActiveRemote::Cached::ArgumentKeys.new('hello', 'world', {}).cache_key).to eq('helloworld')
56
+ end
57
+
58
+ it 'removes nil arguments from the key' do
59
+ expect(::ActiveRemote::Cached::ArgumentKeys.new('hello', nil, 'world', {}).cache_key).to eq('helloworld')
60
+ end
61
+
62
+ it 'flattens nested array arguments into the key' do
63
+ expect(::ActiveRemote::Cached::ArgumentKeys.new(['hello', ['world']], {}).cache_key).to eq('helloworld')
64
+ end
65
+
66
+ it 'returns an empty key when no arguments are given' do
67
+ expect(::ActiveRemote::Cached::ArgumentKeys.new({}).cache_key).to eq('')
68
+ end
69
+
70
+ it 'returns the cache key from #to_s' do
71
+ expect(::ActiveRemote::Cached::ArgumentKeys.new('hello', {}).to_s).to eq('hello')
72
+ end
73
+
74
+ it 'removes characters when both the remove and the replace option are given' do
75
+ options = {
76
+ :active_remote_cached_remove_characters => true,
77
+ :active_remote_cached_replace_characters => true
78
+ }
79
+ expect(::ActiveRemote::Cached::ArgumentKeys.new('hello {}', options).cache_key).to eq('hello')
80
+ end
81
+
82
+ describe '.for_fields' do
83
+ it 'names each field in the key' do
84
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], %w[x y], {})
85
+
86
+ expect(argument_keys.cache_key).to eq('alpha.x/beta.y')
87
+ end
88
+
89
+ it 'gives two finders with the same values two different keys' do
90
+ alpha_beta = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], %w[x y], {})
91
+ gamma_delta = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[gamma delta], %w[x y], {})
92
+
93
+ expect(alpha_beta.cache_key).not_to eq(gamma_delta.cache_key)
94
+ end
95
+
96
+ it 'gives a single field a different key than two fields with the same characters' do
97
+ one_field = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[guid], %w[xy], {})
98
+ two_fields = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], %w[x y], {})
99
+
100
+ expect(one_field.cache_key).not_to eq(two_fields.cache_key)
101
+ end
102
+
103
+ it 'joins an array value with a comma' do
104
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[guid], [%w[a b]], {})
105
+
106
+ expect(argument_keys.cache_key).to eq('guid.a.b')
107
+ end
108
+
109
+ it 'removes a nil inside an array value' do
110
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[guid], [['a', nil, 'b']], {})
111
+
112
+ expect(argument_keys.cache_key).to eq('guid.a.b')
113
+ end
114
+
115
+ it 'gives an empty value for a nil argument' do
116
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], ['x', nil], {})
117
+
118
+ expect(argument_keys.cache_key).to eq('alpha.x/beta.')
119
+ end
120
+
121
+ it 'escapes a separator inside a value' do
122
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[guid], ['a/b.c%d'], {})
123
+
124
+ expect(argument_keys.cache_key).to eq('guid.a%2Fb%2Ec%25d')
125
+ end
126
+
127
+ it 'keeps the separators when the remove option is given' do
128
+ options = { :active_remote_cached_remove_characters => true }
129
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], ['hello {}', 'y'], options)
130
+
131
+ expect(argument_keys.cache_key).to eq('alpha.hello/beta.y')
132
+ end
133
+
134
+ it 'keeps the separators when the replace option is given' do
135
+ options = { :active_remote_cached_replace_characters => true }
136
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.for_fields(%i[alpha beta], ['hello {}', 'y'], options)
137
+
138
+ expect(argument_keys.cache_key).to eq('alpha.helloSPLBRB/beta.y')
139
+ end
140
+ end
141
+
142
+ # #cache_key calls gsub! on an instance variable, so a second call must not
143
+ # replace the characters of the first result again.
144
+ it 'returns the same key when :active_remote_cached_replace_characters is called twice' do
145
+ options = { :active_remote_cached_replace_characters => true }
146
+ argument_keys = ::ActiveRemote::Cached::ArgumentKeys.new('hello {}', options)
147
+
148
+ expect(argument_keys.cache_key).to eq('helloSPLBRB')
149
+ expect(argument_keys.cache_key).to eq('helloSPLBRB')
150
+ end
27
151
  end