active_remote-cached 1.1.1 → 1.2.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,7 +21,12 @@ 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
@@ -36,6 +43,29 @@ module ActiveRemote
36
43
  @cached_methods
37
44
  end
38
45
 
46
+ # The options each cached finder was declared with, keyed by method name.
47
+ # The generated methods read this at call time. Interpolating the options
48
+ # into the generated source instead would require every value to have a
49
+ # literal form, and a value such as 5.minutes does not.
50
+ def cached_finder_options
51
+ @cached_finder_options ||= {}
52
+ end
53
+
54
+ # A subclass inherits the finder methods its parent defined, but not the
55
+ # parent registry, so the lookup walks up the superclass chain.
56
+ def _cached_finder_options_for(method_name)
57
+ klass = self
58
+
59
+ while klass.respond_to?(:cached_finder_options)
60
+ options = klass.cached_finder_options[method_name]
61
+ return options if options
62
+
63
+ klass = klass.superclass
64
+ end
65
+
66
+ {}
67
+ end
68
+
39
69
  def cached_finders_for(*cached_finder_keys)
40
70
  options = cached_finder_keys.extract_options!
41
71
 
@@ -103,37 +133,38 @@ module ActiveRemote
103
133
  "#{::Regexp.last_match(1)}#{params.sort.join('_and_')}#{::Regexp.last_match(3)}".to_sym
104
134
  end
105
135
 
106
- # rubocop:disable Metrics/AbcSize
107
136
  def _args_in_sorted_order(m, args)
108
137
  regex = /cached_(?:delete|exist_search|search|exist_find|find)_by_([0-9a-zA-Z_]*)(!|\?)?/
109
138
 
110
- method_name = _method_missing_name(m)
139
+ called_match = m.match(regex)
140
+ sorted_match = _method_missing_name(m).match(regex)
111
141
 
112
- match_1 = m.match(regex)
113
- match_2 = method_name.match(regex)
142
+ return args unless called_match[1] && sorted_match[1]
114
143
 
115
- args_in_order = []
144
+ called_field_names = called_match[1].split('_and_')
145
+ sorted_field_names = sorted_match[1].split('_and_')
116
146
 
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_')
147
+ _reorder_args(m, args, called_field_names, sorted_field_names)
148
+ end
120
149
 
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
150
+ def _reorder_args(m, args, called_field_names, sorted_field_names)
151
+ if args.size < called_field_names.size
152
+ raise ::ArgumentError,
153
+ "wrong number of arguments to #{m} (given #{args.size}, expected #{called_field_names.size})"
154
+ end
125
155
 
126
- if args.size > args_in_order.size
127
- # Add options if passed
128
- args_in_order << args.last
129
- end
156
+ args_in_order = sorted_field_names.map do |field_name|
157
+ index = called_field_names.index(field_name)
158
+ raise ::ArgumentError, "#{m} has no argument named #{field_name}" if index.nil?
130
159
 
131
- args_in_order
132
- else
133
- args
160
+ args[index]
134
161
  end
162
+
163
+ # Add the options hash if the caller passed one.
164
+ args_in_order << args.last if args.size > called_field_names.size
165
+
166
+ args_in_order
135
167
  end
136
- # rubocop:enable Metrics/AbcSize
137
168
 
138
169
  # rubocop:disable Metrics/AbcSize
139
170
  def _create_cached_finder_for(cached_finder_key, options = {})
@@ -192,25 +223,41 @@ module ActiveRemote
192
223
  "cached_search_by_#{arguments.sort.join('_and_')}"
193
224
  end
194
225
 
195
- def _define_cached_delete_method(method_name, *method_arguments, cached_finder_options)
226
+ def _expanded_search_args(method_arguments)
227
+ method_arguments.map { |method_argument| ":#{method_argument} => #{method_argument}," }.join
228
+ end
229
+
230
+ # Returns the source text that builds the cache key for a generated
231
+ # method. The key names each field, so that two finders that take the
232
+ # same values do not share one cache entry.
233
+ def _expanded_cache_key_args(method_arguments)
234
+ sorted_arguments = method_arguments.sort
235
+ field_names = sorted_arguments.map { |argument| ":#{argument}" }.join(',')
236
+
237
+ '::ActiveRemote::Cached::ArgumentKeys.for_fields(' \
238
+ "[#{field_names}], [#{sorted_arguments.join(',')}], __active_remote_cached_options" \
239
+ ').cache_key'
240
+ end
241
+
242
+ def _define_cached_delete_method(method_name, *method_arguments, cached_finder_options_hash)
196
243
  method_arguments.flatten!
197
244
  expanded_method_args = method_arguments.join(',')
198
- sorted_method_args = method_arguments.sort.join(',')
199
- cached_methods << method_name
245
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
200
246
 
201
247
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
202
248
  # def self.cached_delete_by_user_guid(user_guid, options = {})
203
249
  # ::ActiveRemote::Cached.cache.delete([name, user_guid])
204
250
  # end
205
251
  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)
252
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
207
253
  namespace = __active_remote_cached_options.delete(:namespace)
254
+ argument_cache_key = #{expanded_cache_key_args}
208
255
  find_cache_key = [
209
256
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
210
257
  namespace,
211
258
  name,
212
259
  "#find",
213
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
260
+ argument_cache_key
214
261
  ].compact
215
262
 
216
263
  search_cache_key = [
@@ -218,35 +265,36 @@ module ActiveRemote
218
265
  namespace,
219
266
  name,
220
267
  "#search",
221
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
268
+ argument_cache_key
222
269
  ].compact
223
270
 
224
271
  ::ActiveRemote::Cached.cache.delete(find_cache_key)
225
272
  ::ActiveRemote::Cached.cache.delete(search_cache_key)
226
273
  end
227
274
  RUBY
275
+
276
+ cached_finder_options[method_name] = cached_finder_options_hash
277
+ cached_methods << method_name
228
278
  end
229
279
 
230
- def _define_cached_exist_find_method(method_name, *method_arguments, cached_finder_options)
280
+ def _define_cached_exist_find_method(method_name, *method_arguments, cached_finder_options_hash)
231
281
  method_arguments.flatten!
232
282
  expanded_method_args = method_arguments.join(',')
233
- sorted_method_args = method_arguments.sort.join(',')
234
- cached_methods << method_name
235
- cached_methods << "#{method_name}?"
283
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
236
284
 
237
285
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
238
286
  # def self.cached_exist_find_by_user_guid(user_guid, options = {})
239
287
  # ::ActiveRemote::Cached.cache.exist?([name, user_guid])
240
288
  # end
241
289
  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)
290
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
243
291
  namespace = __active_remote_cached_options.delete(:namespace)
244
292
  cache_key = [
245
293
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
246
294
  namespace,
247
295
  name,
248
296
  "#find",
249
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
297
+ #{expanded_cache_key_args}
250
298
  ].compact
251
299
 
252
300
  ::ActiveRemote::Cached.cache.exist?(cache_key)
@@ -254,28 +302,30 @@ module ActiveRemote
254
302
  RUBY
255
303
 
256
304
  singleton_class.send(:alias_method, "#{method_name}?", method_name)
305
+
306
+ cached_finder_options[method_name] = cached_finder_options_hash
307
+ cached_methods << method_name
308
+ cached_methods << "#{method_name}?"
257
309
  end
258
310
 
259
- def _define_cached_exist_search_method(method_name, *method_arguments, cached_finder_options)
311
+ def _define_cached_exist_search_method(method_name, *method_arguments, cached_finder_options_hash)
260
312
  method_arguments.flatten!
261
313
  expanded_method_args = method_arguments.join(',')
262
- sorted_method_args = method_arguments.sort.join(',')
263
- cached_methods << method_name
264
- cached_methods << "#{method_name}?"
314
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
265
315
 
266
316
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
267
317
  # def self.cached_exist_search_by_user_guid(user_guid, options = {})
268
318
  # ::ActiveRemote::Cached.cache.exist?([namespace, name, "#search", user_guid])
269
319
  # end
270
320
  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)
321
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
272
322
  namespace = __active_remote_cached_options.delete(:namespace)
273
323
  cache_key = [
274
324
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
275
325
  namespace,
276
326
  name,
277
327
  "#search",
278
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
328
+ #{expanded_cache_key_args}
279
329
  ].compact
280
330
 
281
331
  ::ActiveRemote::Cached.cache.exist?(cache_key)
@@ -283,18 +333,18 @@ module ActiveRemote
283
333
  RUBY
284
334
 
285
335
  singleton_class.send(:alias_method, "#{method_name}?", method_name)
336
+
337
+ cached_finder_options[method_name] = cached_finder_options_hash
338
+ cached_methods << method_name
339
+ cached_methods << "#{method_name}?"
286
340
  end
287
341
 
288
- def _define_cached_find_method(method_name, *method_arguments, cached_finder_options)
342
+ def _define_cached_find_method(method_name, *method_arguments, cached_finder_options_hash)
289
343
  method_arguments.flatten!
290
344
  expanded_method_args = method_arguments.join(',')
291
- sorted_method_args = method_arguments.sort.join(',')
292
- cached_methods << method_name
345
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
293
346
 
294
- expanded_search_args = ''
295
- method_arguments.each do |method_argument|
296
- expanded_search_args << ":#{method_argument} => #{method_argument},"
297
- end
347
+ expanded_search_args = _expanded_search_args(method_arguments)
298
348
 
299
349
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
300
350
  # def self.cached_find_by_user_guid(user_guid, options = {})
@@ -309,14 +359,14 @@ module ActiveRemote
309
359
  # of the result object is maintained for requests/responses
310
360
  #
311
361
  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)
362
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
313
363
  namespace = __active_remote_cached_options.delete(:namespace)
314
364
  cache_key = [
315
365
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
316
366
  namespace,
317
367
  name,
318
368
  "#find",
319
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
369
+ #{expanded_cache_key_args}
320
370
  ].compact
321
371
 
322
372
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -328,18 +378,17 @@ module ActiveRemote
328
378
  end
329
379
  end
330
380
  RUBY
381
+
382
+ cached_finder_options[method_name] = cached_finder_options_hash
383
+ cached_methods << method_name
331
384
  end
332
385
 
333
- def _define_cached_search_method(method_name, *method_arguments, cached_finder_options)
386
+ def _define_cached_search_method(method_name, *method_arguments, cached_finder_options_hash)
334
387
  method_arguments.flatten!
335
388
  expanded_method_args = method_arguments.join(',')
336
- sorted_method_args = method_arguments.sort.join(',')
337
- cached_methods << method_name
389
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
338
390
 
339
- expanded_search_args = ''
340
- method_arguments.each do |method_argument|
341
- expanded_search_args << ":#{method_argument} => #{method_argument},"
342
- end
391
+ expanded_search_args = _expanded_search_args(method_arguments)
343
392
 
344
393
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
345
394
  # def self.cached_search_by_user_guid(user_guid, options = {})
@@ -358,14 +407,14 @@ module ActiveRemote
358
407
  # of the result object is maintained for requests/responses
359
408
  #
360
409
  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)
410
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
362
411
  namespace = __active_remote_cached_options.delete(:namespace)
363
412
  cache_key = [
364
413
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
365
414
  namespace,
366
415
  name,
367
416
  "#search",
368
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
417
+ #{expanded_cache_key_args}
369
418
  ].compact
370
419
 
371
420
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -377,18 +426,17 @@ module ActiveRemote
377
426
  end
378
427
  end
379
428
  RUBY
429
+
430
+ cached_finder_options[method_name] = cached_finder_options_hash
431
+ cached_methods << method_name
380
432
  end
381
433
 
382
- def _define_cached_search_bang_method(method_name, *method_arguments, cached_finder_options)
434
+ def _define_cached_search_bang_method(method_name, *method_arguments, cached_finder_options_hash)
383
435
  method_arguments.flatten!
384
436
  expanded_method_args = method_arguments.join(',')
385
- sorted_method_args = method_arguments.sort.join(',')
386
- cached_methods << method_name
437
+ expanded_cache_key_args = _expanded_cache_key_args(method_arguments)
387
438
 
388
- expanded_search_args = ''
389
- method_arguments.each do |method_argument|
390
- expanded_search_args << ":#{method_argument} => #{method_argument},"
391
- end
439
+ expanded_search_args = _expanded_search_args(method_arguments)
392
440
 
393
441
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
394
442
  # def self.cached_search_by_user_guid!(user_guid, options = {})
@@ -403,7 +451,7 @@ module ActiveRemote
403
451
  # results = self.search(:user_guid => user_guid)
404
452
  # end
405
453
  #
406
- # raise ::ActiveRemote::RemoteRecordNotFound.new(self.class) if results.size <= 0
454
+ # raise ::ActiveRemote::RemoteRecordNotFound, self if results.nil? || results.first.nil?
407
455
  # results
408
456
  # end
409
457
  # end
@@ -412,14 +460,14 @@ module ActiveRemote
412
460
  # of the result object is maintained for requests/responses
413
461
  #
414
462
  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)
463
+ __active_remote_cached_options = ::ActiveRemote::Cached.default_options.merge(_cached_finder_options_for('#{method_name}')).merge(__active_remote_cached_options)
416
464
  namespace = __active_remote_cached_options.delete(:namespace)
417
465
  cache_key = [
418
466
  RUBY_AND_ACTIVE_SUPPORT_VERSION,
419
467
  namespace,
420
468
  name,
421
469
  "#search",
422
- ::ActiveRemote::Cached::ArgumentKeys.new(#{sorted_method_args}, __active_remote_cached_options).cache_key
470
+ #{expanded_cache_key_args}
423
471
  ].compact
424
472
 
425
473
  ::ActiveRemote::Cached.cache.fetch(cache_key, __active_remote_cached_options) do
@@ -431,11 +479,14 @@ module ActiveRemote
431
479
  results = self.search(#{expanded_search_args})
432
480
  end
433
481
 
434
- raise ::ActiveRemote::RemoteRecordNotFound.new(self.class) if results.first.nil?
482
+ raise ::ActiveRemote::RemoteRecordNotFound, self if results.nil? || results.first.nil?
435
483
  results
436
484
  end
437
485
  end
438
486
  RUBY
487
+
488
+ cached_finder_options[method_name] = cached_finder_options_hash
489
+ cached_methods << method_name
439
490
  end
440
491
  end
441
492
 
@@ -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