familia 2.0.0.pre5 → 2.0.0.pre7

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.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,443 @@
1
+ # lib/familia/validation/test_helpers.rb
2
+
3
+ module Familia
4
+ module Validation
5
+ # Test helper methods for integrating Redis command validation with
6
+ # the tryouts testing framework. Provides easy-to-use assertion methods
7
+ # and automatic setup/cleanup for command validation tests.
8
+ #
9
+ # @example Basic usage in a try file
10
+ # require_relative '../validation/test_helpers'
11
+ # extend Familia::Validation::TestHelpers
12
+ #
13
+ # ## User save should execute expected Redis commands
14
+ # user = TestUser.new(id: "123", name: "John")
15
+ #
16
+ # assert_redis_commands do |expect|
17
+ # expect.hset("testuser:123:object", "name", "John")
18
+ # .hset("testuser:123:object", "id", "123")
19
+ #
20
+ # user.save
21
+ # end
22
+ # #=> true
23
+ #
24
+ # @example Transaction validation
25
+ # assert_atomic_operation do |expect|
26
+ # expect.transaction do |tx|
27
+ # tx.hset("account:123", "balance", "1000")
28
+ # .hset("account:456", "balance", "2000")
29
+ # end
30
+ #
31
+ # transfer_funds(from: "123", to: "456", amount: 500)
32
+ # end
33
+ # #=> true
34
+ #
35
+ module TestHelpers
36
+ # Assert that a block executes the expected Redis commands
37
+ def assert_redis_commands(message = nil, &block)
38
+ validator = Validator.new
39
+ result = validator.validate(&block)
40
+
41
+ unless result.valid?
42
+ error_msg = message || "Redis command validation failed"
43
+ error_msg += "\n" + result.detailed_report
44
+ raise ValidationError, error_msg
45
+ end
46
+
47
+ result.valid?
48
+ end
49
+
50
+ # Assert that a block executes Redis commands atomically
51
+ def assert_atomic_operation(message = nil, &block)
52
+ validator = Validator.new(strict_atomicity: true)
53
+
54
+ if block.arity == 1
55
+ # Block expects expectations parameter
56
+ result = validator.validate(&block)
57
+ else
58
+ # Block is just execution code - validate atomicity only
59
+ result = validator.validate_atomicity(&block)
60
+ end
61
+
62
+ unless result.valid?
63
+ error_msg = message || "Atomic operation validation failed"
64
+ error_msg += "\n" + result.detailed_report
65
+ raise ValidationError, error_msg
66
+ end
67
+
68
+ result.valid?
69
+ end
70
+
71
+ # Assert that specific commands were executed (flexible order)
72
+ def assert_commands_executed(*expected_commands)
73
+ validator = Validator.new
74
+
75
+ CommandRecorder.start_recording
76
+ yield if block_given?
77
+ actual_commands = CommandRecorder.stop_recording
78
+
79
+ result = validator.assert_commands_executed(expected_commands, actual_commands)
80
+
81
+ unless result.valid?
82
+ error_msg = "Expected commands were not executed as specified"
83
+ error_msg += "\n" + result.detailed_report
84
+ raise ValidationError, error_msg
85
+ end
86
+
87
+ result.valid?
88
+ end
89
+
90
+ # Assert that no Redis commands were executed
91
+ def assert_no_redis_commands(&block)
92
+ CommandRecorder.start_recording
93
+ block.call if block_given?
94
+ commands = CommandRecorder.stop_recording
95
+
96
+ unless commands.command_count == 0
97
+ error_msg = "Expected no Redis commands, but #{commands.command_count} were executed:"
98
+ commands.commands.each { |cmd| error_msg += "\n #{cmd}" }
99
+ raise ValidationError, error_msg
100
+ end
101
+
102
+ true
103
+ end
104
+
105
+ # Assert that a specific number of commands were executed
106
+ def assert_command_count(expected_count, &block)
107
+ CommandRecorder.start_recording
108
+ block.call if block_given?
109
+ commands = CommandRecorder.stop_recording
110
+
111
+ actual_count = commands.command_count
112
+ unless actual_count == expected_count
113
+ error_msg = "Expected #{expected_count} Redis commands, but #{actual_count} were executed"
114
+ raise ValidationError, error_msg
115
+ end
116
+
117
+ true
118
+ end
119
+
120
+ # Assert that commands were executed within a transaction
121
+ def assert_transaction_used(&block)
122
+ CommandRecorder.start_recording
123
+ block.call if block_given?
124
+ commands = CommandRecorder.stop_recording
125
+
126
+ unless commands.transaction_count > 0
127
+ error_msg = "Expected operations to use transactions, but none were found"
128
+ raise ValidationError, error_msg
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ # Assert that commands were NOT executed within a transaction
135
+ def assert_no_transaction_used(&block)
136
+ CommandRecorder.start_recording
137
+ block.call if block_given?
138
+ commands = CommandRecorder.stop_recording
139
+
140
+ unless commands.transaction_count == 0
141
+ error_msg = "Expected operations to NOT use transactions, but #{commands.transaction_count} were found"
142
+ raise ValidationError, error_msg
143
+ end
144
+
145
+ true
146
+ end
147
+
148
+ # Capture and return Redis commands without validation
149
+ def capture_redis_commands(&block)
150
+ CommandRecorder.start_recording
151
+ block.call if block_given?
152
+ CommandRecorder.stop_recording
153
+ end
154
+
155
+ # Performance assertion - assert operations complete within time limit
156
+ def assert_performance_within(max_duration_ms, &block)
157
+ start_time = Time.now
158
+ CommandRecorder.start_recording
159
+
160
+ result = block.call if block_given?
161
+
162
+ commands = CommandRecorder.stop_recording
163
+ actual_duration_ms = (Time.now - start_time) * 1000
164
+
165
+ if actual_duration_ms > max_duration_ms
166
+ error_msg = "Operation took #{actual_duration_ms.round(2)}ms, " \
167
+ "expected less than #{max_duration_ms}ms"
168
+ raise ValidationError, error_msg
169
+ end
170
+
171
+ result
172
+ end
173
+
174
+ # Assert efficient command usage (no N+1 patterns)
175
+ def assert_efficient_commands(&block)
176
+ validator = Validator.new(performance_tracking: true)
177
+ commands = capture_redis_commands(&block)
178
+
179
+ analysis = validator.analyze_performance(commands)
180
+
181
+ if analysis[:efficiency_score] < 70 # Threshold for acceptable efficiency
182
+ error_msg = "Inefficient Redis command usage detected (score: #{analysis[:efficiency_score]})"
183
+
184
+ if analysis[:potential_n_plus_one].any?
185
+ error_msg += "\nPotential N+1 patterns:"
186
+ analysis[:potential_n_plus_one].each do |pattern|
187
+ error_msg += "\n #{pattern[:command]}: #{pattern[:count]} calls - #{pattern[:suggestion]}"
188
+ end
189
+ end
190
+
191
+ raise ValidationError, error_msg
192
+ end
193
+
194
+ true
195
+ end
196
+
197
+ # Setup and teardown helpers for validation tests
198
+ def setup_validation_test
199
+ # Ensure middleware is registered
200
+ @original_middleware_state = CommandRecorder.recording?
201
+
202
+ # Clear any existing state
203
+ CommandRecorder.clear if CommandRecorder.recording?
204
+
205
+ # Enable database logging for better debugging
206
+ @original_logging_state = Familia.enable_database_logging
207
+ Familia.enable_database_logging = true
208
+
209
+ # Enable command counting
210
+ @original_counter_state = Familia.enable_database_counter
211
+ Familia.enable_database_counter = true
212
+
213
+ DatabaseCommandCounter.reset
214
+ end
215
+
216
+ def teardown_validation_test
217
+ # Stop recording if active
218
+ CommandRecorder.stop_recording if CommandRecorder.recording?
219
+
220
+ # Restore original states
221
+ Familia.enable_database_logging = @original_logging_state if @original_logging_state
222
+ Familia.enable_database_counter = @original_counter_state if @original_counter_state
223
+
224
+ # Reset counters
225
+ DatabaseCommandCounter.reset
226
+ end
227
+
228
+ # Wrapper for validation tests with automatic setup/teardown
229
+ def with_validation_test(&block)
230
+ setup_validation_test
231
+ begin
232
+ block.call
233
+ ensure
234
+ teardown_validation_test
235
+ end
236
+ end
237
+
238
+ # Helper to create expectation builders for common patterns
239
+ def expect_horreum_save(class_name, identifier, fields = {})
240
+ dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"
241
+
242
+ expectations = CommandExpectations.new
243
+ fields.each do |field, value|
244
+ expectations.hset(dbkey, field.to_s, value.to_s)
245
+ end
246
+
247
+ expectations
248
+ end
249
+
250
+ def expect_horreum_load(class_name, identifier, fields = [])
251
+ dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"
252
+
253
+ expectations = CommandExpectations.new
254
+ if fields.empty?
255
+ expectations.hgetall(dbkey)
256
+ else
257
+ fields.each do |field|
258
+ expectations.hget(dbkey, field.to_s)
259
+ end
260
+ end
261
+
262
+ expectations
263
+ end
264
+
265
+ def expect_data_type_operation(class_name, identifier, type_name, operation, *args)
266
+ dbkey = "#{class_name.to_s.downcase}:#{identifier}:#{type_name}"
267
+
268
+ expectations = CommandExpectations.new
269
+ expectations.command(operation, dbkey, *args)
270
+ end
271
+
272
+ # Debugging helpers
273
+ def debug_print_commands(command_sequence = nil)
274
+ commands = command_sequence || capture_redis_commands { yield if block_given? }
275
+
276
+ puts "Redis Commands Executed (#{commands.command_count} total):"
277
+ puts "=" * 50
278
+
279
+ commands.commands.each_with_index do |cmd, i|
280
+ prefix = cmd.atomic_command? ? "[TX]" : " "
281
+ puts "#{prefix} #{i + 1}. #{cmd} (#{cmd.duration_us}µs)"
282
+ end
283
+
284
+ if commands.transaction_count > 0
285
+ puts "\nTransactions (#{commands.transaction_count} total):"
286
+ commands.transaction_blocks.each_with_index do |tx, i|
287
+ puts " #{i + 1}. #{tx.command_count} commands"
288
+ end
289
+ end
290
+
291
+ puts "=" * 50
292
+ end
293
+
294
+ def debug_print_performance(command_sequence = nil)
295
+ commands = command_sequence || CommandRecorder.current_sequence
296
+ validator = Validator.new(performance_tracking: true)
297
+ analysis = validator.analyze_performance(commands)
298
+
299
+ puts "Performance Analysis:"
300
+ puts "=" * 30
301
+ puts "Total Commands: #{analysis[:total_commands]}"
302
+ puts "Total Duration: #{analysis[:total_duration_ms].round(2)}ms"
303
+ puts "Average Command Time: #{analysis[:average_command_time_us].round(2)}µs"
304
+ puts "Efficiency Score: #{analysis[:efficiency_score].round(1)}/100"
305
+
306
+ if analysis[:slowest_commands].any?
307
+ puts "\nSlowest Commands:"
308
+ analysis[:slowest_commands].each do |cmd|
309
+ puts " #{cmd[:command]} (#{cmd[:duration_us]}µs)"
310
+ end
311
+ end
312
+
313
+ if analysis[:potential_n_plus_one].any?
314
+ puts "\nPotential N+1 Patterns:"
315
+ analysis[:potential_n_plus_one].each do |pattern|
316
+ puts " #{pattern[:command]}: #{pattern[:count]} calls"
317
+ end
318
+ end
319
+
320
+ puts "=" * 30
321
+ end
322
+
323
+ # Matcher helpers for more readable tests
324
+ def match_command(cmd, *args)
325
+ if args.empty?
326
+ ->(recorded) { recorded.command == cmd.to_s.upcase }
327
+ else
328
+ ->(recorded) { recorded.command == cmd.to_s.upcase && recorded.args == args.map(&:to_s) }
329
+ end
330
+ end
331
+
332
+ def match_pattern(pattern)
333
+ case pattern
334
+ when Regexp
335
+ ->(recorded) { pattern.match?(recorded.to_s) }
336
+ when String
337
+ ->(recorded) { recorded.to_s.include?(pattern) }
338
+ else
339
+ pattern
340
+ end
341
+ end
342
+
343
+ def any_string
344
+ ArgumentMatcher.new(:any_string)
345
+ end
346
+
347
+ def any_number
348
+ ArgumentMatcher.new(:any_number)
349
+ end
350
+
351
+ def any_value
352
+ ArgumentMatcher.new(:any_value)
353
+ end
354
+ end
355
+
356
+ # Extended test helpers specifically for Familia data types
357
+ module FamiliaTestHelpers
358
+ include TestHelpers
359
+
360
+ # Assert Familia object operations
361
+ def assert_familia_save(obj, expected_fields = nil, &block)
362
+ class_name = obj.class.name.split('::').last.downcase
363
+ identifier = obj.identifier
364
+
365
+ assert_redis_commands do |expect|
366
+ if expected_fields
367
+ expected_fields.each do |field, value|
368
+ expect.hset("#{class_name}:#{identifier}:object", field.to_s, value.to_s)
369
+ end
370
+ else
371
+ expect.match_pattern(/^HSET #{class_name}:#{identifier}:object/)
372
+ end
373
+
374
+ block.call if block
375
+ obj.save
376
+ end
377
+ end
378
+
379
+ def assert_familia_load(obj_class, identifier, &block)
380
+ class_name = obj_class.name.split('::').last.downcase
381
+
382
+ assert_redis_commands do |expect|
383
+ expect.hgetall("#{class_name}:#{identifier}:object")
384
+
385
+ block.call if block
386
+ obj_class.new(id: identifier).refresh!
387
+ end
388
+ end
389
+
390
+ def assert_familia_destroy(obj, &block)
391
+ class_name = obj.class.name.split('::').last.downcase
392
+ identifier = obj.identifier
393
+
394
+ assert_redis_commands do |expect|
395
+ expect.del("#{class_name}:#{identifier}:object")
396
+
397
+ block.call if block
398
+ obj.destroy!
399
+ end
400
+ end
401
+
402
+ # Assert data type operations
403
+ def assert_list_operation(obj, list_name, operation, *args, &block)
404
+ class_name = obj.class.name.split('::').last.downcase
405
+ identifier = obj.identifier
406
+ dbkey = "#{class_name}:#{identifier}:#{list_name}"
407
+
408
+ assert_redis_commands do |expect|
409
+ expect.command(operation.to_s.upcase, dbkey, *args)
410
+
411
+ block.call if block
412
+ obj.send(list_name).send(operation, *args)
413
+ end
414
+ end
415
+
416
+ def assert_set_operation(obj, set_name, operation, *args, &block)
417
+ class_name = obj.class.name.split('::').last.downcase
418
+ identifier = obj.identifier
419
+ dbkey = "#{class_name}:#{identifier}:#{set_name}"
420
+
421
+ assert_redis_commands do |expect|
422
+ expect.command(operation.to_s.upcase, dbkey, *args)
423
+
424
+ block.call if block
425
+ obj.send(set_name).send(operation, *args)
426
+ end
427
+ end
428
+
429
+ def assert_sorted_set_operation(obj, zset_name, operation, *args, &block)
430
+ class_name = obj.class.name.split('::').last.downcase
431
+ identifier = obj.identifier
432
+ dbkey = "#{class_name}:#{identifier}:#{zset_name}"
433
+
434
+ assert_redis_commands do |expect|
435
+ expect.command(operation.to_s.upcase, dbkey, *args)
436
+
437
+ block.call if block
438
+ obj.send(zset_name).send(operation, *args)
439
+ end
440
+ end
441
+ end
442
+ end
443
+ end