familia 2.0.0.pre16 → 2.0.0.pre18

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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.rst +82 -0
  7. data/CLAUDE.md +47 -2
  8. data/Gemfile.lock +1 -1
  9. data/README.md +13 -0
  10. data/bin/irb +1 -1
  11. data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/migrating/v2.0.0-pre18.md +58 -0
  14. data/docs/overview.md +2 -2
  15. data/docs/qodo-merge-compliance.md +96 -0
  16. data/docs/reference/api-technical.md +1 -1
  17. data/examples/encrypted_fields.rb +1 -1
  18. data/examples/safe_dump.rb +1 -1
  19. data/lib/familia/base.rb +6 -6
  20. data/lib/familia/connection/middleware.rb +58 -4
  21. data/lib/familia/connection.rb +1 -1
  22. data/lib/familia/data_type/class_methods.rb +63 -0
  23. data/lib/familia/data_type/connection.rb +83 -0
  24. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  25. data/lib/familia/data_type/serialization.rb +5 -5
  26. data/lib/familia/data_type/settings.rb +96 -0
  27. data/lib/familia/data_type/types/hashkey.rb +2 -1
  28. data/lib/familia/data_type/types/sorted_set.rb +113 -10
  29. data/lib/familia/data_type/types/stringkey.rb +0 -4
  30. data/lib/familia/data_type.rb +8 -195
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/features/autoloader.rb +3 -1
  34. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  35. data/lib/familia/features/encrypted_fields.rb +5 -2
  36. data/lib/familia/features/external_identifier.rb +49 -8
  37. data/lib/familia/features/object_identifier.rb +84 -12
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
  40. data/lib/familia/features/relationships/indexing.rb +7 -1
  41. data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/features/transient_fields.rb +7 -2
  44. data/lib/familia/features.rb +6 -1
  45. data/lib/familia/field_type.rb +0 -18
  46. data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
  47. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
  48. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
  49. data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
  50. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
  51. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
  52. data/lib/familia/horreum/serialization.rb +190 -0
  53. data/lib/familia/horreum.rb +39 -14
  54. data/lib/familia/identifier_extractor.rb +60 -0
  55. data/lib/familia/logging.rb +271 -112
  56. data/lib/familia/refinements.rb +0 -1
  57. data/lib/familia/version.rb +1 -1
  58. data/lib/familia.rb +2 -2
  59. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  60. data/pr_agent.toml +31 -0
  61. data/pr_compliance_checklist.yaml +45 -0
  62. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  63. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  64. data/try/edge_cases/json_serialization_try.rb +2 -2
  65. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  66. data/try/edge_cases/race_conditions_try.rb +1 -1
  67. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  68. data/try/edge_cases/string_coercion_try.rb +1 -1
  69. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  70. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  71. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  72. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  77. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  79. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  80. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  81. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  82. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  83. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  84. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  85. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  86. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  87. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  88. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  89. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  94. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  95. data/try/features/expiration/expiration_try.rb +1 -1
  96. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  97. data/try/features/feature_dependencies_try.rb +1 -1
  98. data/try/features/feature_improvements_try.rb +1 -1
  99. data/try/features/field_groups_try.rb +244 -0
  100. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  101. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  102. data/try/features/quantization/quantization_try.rb +1 -1
  103. data/try/features/real_feature_integration_try.rb +17 -14
  104. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  105. data/try/features/relationships/indexing_try.rb +16 -1
  106. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  107. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  108. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  109. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  110. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  111. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  112. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  114. data/try/features/relationships/relationships_performance_try.rb +1 -1
  115. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  116. data/try/features/relationships/relationships_try.rb +1 -1
  117. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  118. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  119. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  121. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  122. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  123. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  124. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  125. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  126. data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
  127. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  128. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  129. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  130. data/try/{core → integration/connection}/pools_try.rb +1 -1
  131. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  135. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  136. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  137. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  138. data/try/{core → integration}/create_method_try.rb +1 -1
  139. data/try/integration/cross_component_try.rb +1 -1
  140. data/try/{core → integration}/database_consistency_try.rb +12 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  144. data/try/{models → integration/models}/customer_try.rb +6 -6
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  147. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +2 -2
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +15 -7
  156. data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
  157. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  158. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  159. data/try/{core → unit/core}/connection_try.rb +5 -5
  160. data/try/{core → unit/core}/errors_try.rb +4 -4
  161. data/try/{core → unit/core}/extensions_try.rb +1 -1
  162. data/try/unit/core/familia_logger_try.rb +110 -0
  163. data/try/{core → unit/core}/familia_try.rb +2 -2
  164. data/try/{core → unit/core}/middleware_try.rb +41 -1
  165. data/try/{core → unit/core}/settings_try.rb +1 -1
  166. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  167. data/try/{core → unit/core}/tools_try.rb +3 -3
  168. data/try/{core → unit/core}/utils_try.rb +17 -14
  169. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  176. data/try/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
  177. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  178. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  179. data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  182. data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
  183. data/try/unit/horreum/defensive_initialization_try.rb +86 -0
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  193. data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
  194. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  195. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  196. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  198. data/try/valkey.conf +26 -0
  199. metadata +149 -132
  200. data/lib/familia/distinguisher.rb +0 -85
  201. data/lib/familia/horreum/core.rb +0 -21
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
  205. /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
  206. /data/try/{debugging → support/debugging}/README.md +0 -0
  207. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  227. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  228. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  229. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  230. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  231. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  233. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  234. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  235. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  250. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Correctness Test: Field deserialization strategies
4
+ #
5
+ # Verifies that different deserialization approaches produce identical
6
+ # results for all field types (strings, numbers, JSON, nested structures).
7
+ #
8
+ # Usage:
9
+ # $ try/support/benchmarks/deserialization_correctness_test.rb
10
+ #
11
+
12
+ require_relative '../../../lib/familia'
13
+ require 'json'
14
+
15
+ # Setup Redis connection
16
+ Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
17
+
18
+ # Sample model with various field types
19
+ class CorrectnessTestUser < Familia::Horreum
20
+ identifier_field :user_id
21
+
22
+ field :user_id
23
+ field :name
24
+ field :email
25
+ field :age
26
+ field :active
27
+ field :metadata # Will store JSON hash
28
+ field :tags # Will store JSON array
29
+ field :created_at # Will store timestamp
30
+ field :score # Will store float
31
+ field :simple_string
32
+ field :nested_data # Will store deeply nested JSON
33
+ field :empty_string
34
+ field :nil_value
35
+ end
36
+
37
+ # Create sample data with comprehensive test cases
38
+ sample_data = {
39
+ 'user_id' => 'user_12345',
40
+ 'name' => 'John Doe',
41
+ 'email' => 'john.doe@example.com',
42
+ 'age' => '35',
43
+ 'active' => 'true',
44
+ 'metadata' => '{"role":"admin","department":"engineering","level":5}',
45
+ 'tags' => '["ruby","redis","performance","optimization"]',
46
+ 'created_at' => Time.now.to_i.to_s,
47
+ 'score' => '98.7',
48
+ 'simple_string' => 'Just a plain string value',
49
+ 'nested_data' => '{"user":{"profile":{"settings":{"theme":"dark","notifications":true}}}}',
50
+ 'empty_string' => '',
51
+ 'nil_value' => nil,
52
+ }
53
+
54
+ # Persist sample data to Redis
55
+ user = CorrectnessTestUser.new(**sample_data)
56
+ user.save
57
+
58
+ # Get the raw hash data directly from Redis
59
+ raw_hash = CorrectnessTestUser.dbclient.hgetall(user.dbkey)
60
+
61
+ puts 'Correctness Test: Deserialization Strategies'
62
+ puts '=' * 70
63
+ puts "\nTesting with #{raw_hash.keys.size} fields"
64
+ puts "\n"
65
+
66
+ # Strategy 1: Current field-by-field (reference implementation)
67
+ def strategy_current(fields, klass)
68
+ deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
69
+ klass.new(**deserialized)
70
+ end
71
+
72
+ # Strategy 2: Bulk JSON round-trip
73
+ def strategy_bulk_json(fields, klass)
74
+ parsed = JSON.parse(JSON.dump(fields))
75
+ klass.new(**parsed)
76
+ end
77
+
78
+ # Strategy 3: Selective deserialization (only JSON-looking strings)
79
+ def strategy_selective(fields, klass)
80
+ deserialized = fields.transform_values do |value|
81
+ if value.to_s.start_with?('{', '[')
82
+ begin
83
+ JSON.parse(value, symbolize_names: true)
84
+ rescue JSON::ParserError
85
+ value
86
+ end
87
+ else
88
+ value
89
+ end
90
+ end
91
+ klass.new(**deserialized)
92
+ end
93
+
94
+ # Create objects using each strategy
95
+ current_obj = strategy_current(raw_hash, CorrectnessTestUser)
96
+ bulk_obj = strategy_bulk_json(raw_hash, CorrectnessTestUser)
97
+ selective_obj = strategy_selective(raw_hash, CorrectnessTestUser)
98
+
99
+ # Test helper to compare field values
100
+ def compare_values(field, current_val, test_val, strategy_name)
101
+ current_class = current_val.class
102
+ test_class = test_val.class
103
+
104
+ if current_val == test_val && current_class == test_class
105
+ { status: :pass, field: field, strategy: strategy_name }
106
+ else
107
+ {
108
+ status: :fail,
109
+ field: field,
110
+ strategy: strategy_name,
111
+ current: { value: current_val.inspect, class: current_class },
112
+ test: { value: test_val.inspect, class: test_class },
113
+ }
114
+ end
115
+ end
116
+
117
+ # Run correctness tests
118
+ results = []
119
+ fields_to_test = CorrectnessTestUser.fields
120
+
121
+ puts "Testing #{fields_to_test.size} fields across strategies...\n\n"
122
+
123
+ # Test Bulk JSON strategy
124
+ puts 'Strategy: Bulk JSON round-trip'
125
+ puts '-' * 70
126
+ fields_to_test.each do |field|
127
+ current_val = current_obj.send(field)
128
+ test_val = bulk_obj.send(field)
129
+ result = compare_values(field, current_val, test_val, 'Bulk JSON')
130
+ results << result
131
+
132
+ if result[:status] == :pass
133
+ puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
134
+ else
135
+ puts " ✗ #{field}: MISMATCH"
136
+ puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
137
+ puts " Bulk: #{result[:test][:value]} (#{result[:test][:class]})"
138
+ end
139
+ end
140
+
141
+ puts "\n"
142
+
143
+ # Test Selective strategy
144
+ puts 'Strategy: Selective (only JSON-like strings)'
145
+ puts '-' * 70
146
+ fields_to_test.each do |field|
147
+ current_val = current_obj.send(field)
148
+ test_val = selective_obj.send(field)
149
+ result = compare_values(field, current_val, test_val, 'Selective')
150
+ results << result
151
+
152
+ if result[:status] == :pass
153
+ puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
154
+ else
155
+ puts " ✗ #{field}: MISMATCH"
156
+ puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
157
+ puts " Selective: #{result[:test][:value]} (#{result[:test][:class]})"
158
+ end
159
+ end
160
+
161
+ puts "\n" + ('=' * 70)
162
+ puts 'SUMMARY'
163
+ puts '=' * 70
164
+
165
+ # Group results by strategy
166
+ by_strategy = results.group_by { |r| r[:strategy] }
167
+
168
+ by_strategy.each do |strategy, strategy_results|
169
+ passed = strategy_results.count { |r| r[:status] == :pass }
170
+ failed = strategy_results.count { |r| r[:status] == :fail }
171
+ total = strategy_results.size
172
+
173
+ status_icon = failed == 0 ? '✓' : '✗'
174
+ puts "\n#{status_icon} #{strategy}: #{passed}/#{total} passed"
175
+
176
+ next unless failed > 0
177
+
178
+ puts ' Failed fields:'
179
+ strategy_results.select { |r| r[:status] == :fail }.each do |result|
180
+ puts " - #{result[:field]}"
181
+ end
182
+ end
183
+
184
+ # Overall assessment
185
+ all_passed = results.all? { |r| r[:status] == :pass }
186
+
187
+ puts "\n" + ('=' * 70)
188
+ puts 'VERDICT'
189
+ puts '=' * 70
190
+
191
+ if all_passed
192
+ puts '✓ All strategies produce identical results to current implementation'
193
+ puts ' Safe to use for optimization'
194
+ else
195
+ puts '✗ Some strategies produce different results'
196
+ puts ' Review failures before implementing'
197
+ end
198
+
199
+ puts "\n" + ('=' * 70)
200
+ puts 'RECOMMENDATIONS'
201
+ puts '=' * 70
202
+
203
+ # Analyze which strategies passed
204
+ bulk_passed = by_strategy['Bulk JSON'].all? { |r| r[:status] == :pass }
205
+ selective_passed = by_strategy['Selective'].all? { |r| r[:status] == :pass }
206
+
207
+ if bulk_passed && selective_passed
208
+ puts '✓ Both Bulk JSON and Selective strategies are correct'
209
+ puts ' → Use Selective for best performance (+15-18% overhead)'
210
+ puts ' → Use Bulk JSON for simplicity (+20% overhead)'
211
+ elsif bulk_passed
212
+ puts '✓ Bulk JSON strategy is correct'
213
+ puts ' → Safe to implement (+20% overhead vs baseline)'
214
+ elsif selective_passed
215
+ puts '✓ Selective strategy is correct'
216
+ puts ' → Safe to implement (+15-18% overhead vs baseline)'
217
+ else
218
+ puts '✗ No alternative strategies passed all tests'
219
+ puts ' → Stick with current implementation'
220
+ puts ' → Or fix identified issues in failing strategies'
221
+ end
222
+
223
+ # Cleanup
224
+ user.destroy!
225
+
226
+ __END__
227
+
228
+ # Expected output:
229
+ #
230
+ # All strategies should pass if they correctly handle:
231
+ # - Simple strings (no parsing needed)
232
+ # - JSON objects (hashes)
233
+ # - JSON arrays
234
+ # - Numbers as strings
235
+ # - Empty strings
236
+ # - Nil values
237
+ # - Nested JSON structures
@@ -6,10 +6,20 @@
6
6
 
7
7
  require 'digest'
8
8
 
9
- require_relative '../../lib/familia'
9
+ require_relative '../../../lib/familia'
10
10
 
11
11
  Familia.enable_database_logging = true
12
12
  Familia.enable_database_counter = true
13
+ Familia.uri = 'redis://127.0.0.1:2525'
14
+
15
+ def generate_random_email
16
+ # Generate a random username
17
+ username = (0...8).map { ('a'..'z').to_a[rand(26)] }.join
18
+ # Define a domain
19
+ domain = "example.com"
20
+ # Combine to form an email address
21
+ "#{username}@#{domain}"
22
+ end
13
23
 
14
24
  class Bone < Familia::Horreum
15
25
  using Familia::Refinements::TimeLiterals
@@ -44,12 +54,10 @@ class Customer < Familia::Horreum
44
54
 
45
55
  using Familia::Refinements::TimeLiterals
46
56
 
47
- logical_database 15 # Use something other than the default DB
57
+ logical_database 3 # Use something other than the default DB
48
58
  default_expiration 5.years
49
59
 
50
60
  feature :safe_dump
51
- # feature :expiration
52
- # feature :api_version
53
61
 
54
62
  # Use new SafeDump DSL instead of @safe_dump_fields
55
63
  safe_dump_field :custid
@@ -108,7 +116,7 @@ end
108
116
  class Session < Familia::Horreum
109
117
  using Familia::Refinements::TimeLiterals
110
118
 
111
- logical_database 14 # don't use Onetime's default DB
119
+ logical_database 2 # a non-default database
112
120
  default_expiration 180.minutes
113
121
 
114
122
  identifier_field :sessid
@@ -195,7 +203,7 @@ end
195
203
  #
196
204
  # NOTE: This will do nothing unless RedactedString is already requried
197
205
  unless defined?(RedactedString)
198
- require_relative '../../lib/familia/features/transient_fields/redacted_string'
206
+ require_relative '../../../lib/familia/features/transient_fields/redacted_string'
199
207
  end
200
208
  module RedactedStringTestHelper
201
209
  refine RedactedString do
@@ -207,7 +215,7 @@ module RedactedStringTestHelper
207
215
  end
208
216
 
209
217
  unless defined?(SingleUseRedactedString)
210
- require_relative '../../lib/familia/features/transient_fields/single_use_redacted_string'
218
+ require_relative '../../../lib/familia/features/transient_fields/single_use_redacted_string'
211
219
  end
212
220
  module SingleUseRedactedStringTestHelper
213
221
  refine SingleUseRedactedString do
@@ -59,7 +59,7 @@ docker exec $CONTAINER_ID bash -c '
59
59
  # $
60
60
  # $ docker run --rm -d -p 3000:3000 \
61
61
  # -e SECRET=$SECRET \
62
- # -e REDIS_URL=redis://host.docker.internal:6379/0 \
62
+ # -e REDIS_URL=redis://host.docker.internal:2525/0 \
63
63
  # ghcr.io/onetimesecret/devtimesecret-lite:latest
64
64
  #
65
65
  # abcd1234
@@ -19,7 +19,7 @@
19
19
  # the directory patterns that autoloader.included generates and also the
20
20
  # exclusion logic works correctly.
21
21
 
22
- require_relative '../../lib/familia'
22
+ require_relative '../../../lib/familia'
23
23
  require 'fileutils'
24
24
  require 'tmpdir'
25
25
 
@@ -1,6 +1,6 @@
1
1
  # try/core/base_enhancements_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -97,14 +97,6 @@ EmptyBaseTest.respond_to?(:feature)
97
97
  Familia::Base.features_available
98
98
  #=:> Hash
99
99
 
100
- ## Dump and load methods are set
101
- Familia::Base.dump_method
102
- #=> :to_json
103
-
104
- ## Load method is set correctly
105
- Familia::Base.load_method
106
- #=> :from_json
107
-
108
100
  ## Base module provides inspect with class name
109
101
  @base_uuid.inspect.include?('BaseUuidTest')
110
102
  #=> true
@@ -1,6 +1,6 @@
1
1
  # try/core/connection_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -12,10 +12,10 @@ Familia.uri
12
12
 
13
13
  ## Default URI points to localhost database server
14
14
  Familia.uri.to_s
15
- #=> "redis://127.0.0.1"
15
+ #=> "redis://127.0.0.1:2525"
16
16
 
17
17
  ## Can parse URI from string
18
- uri = URI.parse('redis://localhost:6379/1')
18
+ uri = URI.parse('redis://localhost:2525/1')
19
19
  uri.host
20
20
  #=> "localhost"
21
21
 
@@ -29,7 +29,7 @@ Familia.connect
29
29
 
30
30
  ## Can create connection to different URI
31
31
  ## Doesn't confirm the logical DB number, dbclient.options raises an error?
32
- test_uri = 'redis://localhost:6379/2'
32
+ test_uri = 'redis://localhost:2525/2'
33
33
  Familia.create_dbclient(test_uri)
34
34
  #=:> Redis
35
35
 
@@ -48,7 +48,7 @@ Familia.enable_database_counter
48
48
  #=> true
49
49
 
50
50
  ## Middleware gets registered when enabled
51
- dbclient = Familia.create_dbclient('redis://localhost:6379/3')
51
+ dbclient = Familia.create_dbclient('redis://localhost:2525/2')
52
52
  dbclient.ping
53
53
  #=> "PONG"
54
54
 
@@ -1,6 +1,6 @@
1
1
  # try/core/errors_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -40,16 +40,16 @@ raise Familia::NotDistinguishableError, 'A customized message'
40
40
  #=~> /A customized message/
41
41
 
42
42
  ## NotConnected error stores URI
43
- test_uri = URI.parse('redis://localhost:6379')
43
+ test_uri = URI.parse('redis://localhost:2525')
44
44
  begin
45
45
  raise Familia::NotConnected.new(test_uri)
46
46
  rescue Familia::NotConnected => e
47
47
  e.uri.to_s
48
48
  end
49
- #=> "redis://localhost"
49
+ #=> "redis://localhost:2525"
50
50
 
51
51
  ## NotConnected error has custom message
52
- test_uri = URI.parse('redis://localhost:6379')
52
+ test_uri = URI.parse('redis://localhost:2525')
53
53
  begin
54
54
  raise Familia::NotConnected.new(test_uri)
55
55
  rescue Familia::NotConnected => e
@@ -1,4 +1,4 @@
1
- require_relative '../helpers/test_helpers'
1
+ require_relative '../../support/helpers/test_helpers'
2
2
 
3
3
  module RefinedContext
4
4
  using Familia::Refinements::TimeLiterals
@@ -0,0 +1,110 @@
1
+ # try/unit/core/familia_logger_try.rb
2
+
3
+ require_relative '../../support/helpers/test_helpers'
4
+ require 'logger'
5
+ require 'stringio'
6
+
7
+ ## FamiliaLogger has trace method
8
+ logger = Familia::FamiliaLogger.new(StringIO.new)
9
+ logger.respond_to?(:trace)
10
+ #=> true
11
+
12
+ ## trace method logs with TRACE level
13
+ output = StringIO.new
14
+ logger = Familia::FamiliaLogger.new(output)
15
+ logger.level = Familia::FamiliaLogger::TRACE
16
+ logger.trace('Test message')
17
+ output.string
18
+ #=~> /Test message/
19
+
20
+ ## FamiliaLogger has TRACE constant
21
+ Familia::FamiliaLogger::TRACE
22
+ #=> 0
23
+
24
+ ## trace method accepts progname parameter
25
+ output = StringIO.new
26
+ logger = Familia::FamiliaLogger.new(output)
27
+ logger.level = Familia::FamiliaLogger::TRACE
28
+ logger.trace('MyApp') { 'Test message' }
29
+ output.string
30
+ #=~> /MyApp/
31
+ #=~> /Test message/
32
+
33
+ ## trace method accepts block for message
34
+ output = StringIO.new
35
+ logger = Familia::FamiliaLogger.new(output)
36
+ logger.level = Familia::FamiliaLogger::TRACE
37
+ logger.trace { 'Block message' }
38
+ output.string
39
+ #=~> /Block message/
40
+
41
+ ## LogFormatter properly formats TRACE messages
42
+ output = StringIO.new
43
+ logger = Familia::FamiliaLogger.new(output)
44
+ logger.level = Familia::FamiliaLogger::TRACE
45
+ logger.formatter = Familia::LogFormatter.new
46
+ logger.trace('Trace test')
47
+ output.string
48
+ #=~> /^T,/
49
+
50
+ ## Nested trace calls preserve TRACE context
51
+ output = StringIO.new
52
+ logger = Familia::FamiliaLogger.new(output)
53
+ logger.level = Familia::FamiliaLogger::TRACE
54
+ logger.formatter = Familia::LogFormatter.new
55
+ logger.trace do
56
+ logger.trace('Inner trace message')
57
+ 'Outer trace message'
58
+ end
59
+ output.string.lines.size
60
+ #=> 2
61
+
62
+ ## Nested trace inner message formatted as TRACE
63
+ output = StringIO.new
64
+ logger = Familia::FamiliaLogger.new(output)
65
+ logger.level = Familia::FamiliaLogger::TRACE
66
+ logger.formatter = Familia::LogFormatter.new
67
+ logger.trace do
68
+ logger.trace('Inner trace message')
69
+ 'Outer trace message'
70
+ end
71
+ output.string.lines[0]
72
+ #=~> /^T,.*Inner trace message/
73
+
74
+ ## Nested trace outer message formatted as TRACE
75
+ output = StringIO.new
76
+ logger = Familia::FamiliaLogger.new(output)
77
+ logger.level = Familia::FamiliaLogger::TRACE
78
+ logger.formatter = Familia::LogFormatter.new
79
+ logger.trace do
80
+ logger.trace('Inner trace message')
81
+ 'Outer trace message'
82
+ end
83
+ output.string.lines[1]
84
+ #=~> /^T,.*Outer trace message/
85
+
86
+ ## Nested trace calls clean up context for subsequent debug calls
87
+ output = StringIO.new
88
+ logger = Familia::FamiliaLogger.new(output)
89
+ logger.level = Familia::FamiliaLogger::TRACE
90
+ logger.formatter = Familia::LogFormatter.new
91
+ logger.trace do
92
+ logger.trace('Inner trace')
93
+ 'Outer trace'
94
+ end
95
+ logger.debug('After nested traces')
96
+ output.string.lines.size
97
+ #=> 3
98
+
99
+ ## Debug call after nested traces formatted as DEBUG not TRACE
100
+ output = StringIO.new
101
+ logger = Familia::FamiliaLogger.new(output)
102
+ logger.level = Familia::FamiliaLogger::TRACE
103
+ logger.formatter = Familia::LogFormatter.new
104
+ logger.trace do
105
+ logger.trace('Inner trace')
106
+ 'Outer trace'
107
+ end
108
+ logger.debug('After nested traces')
109
+ output.string.lines[2]
110
+ #=~> /^D,.*After nested traces/
@@ -1,6 +1,6 @@
1
1
  # try/core/familia_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  ## Check for help class
6
6
  Bone.related_fields.keys # consistent b/c hashes are ordered
@@ -12,7 +12,7 @@ Familia.uri
12
12
 
13
13
  ## Familia has a uri as a string
14
14
  Familia.uri.to_s
15
- #=> 'redis://127.0.0.1'
15
+ #=> 'redis://127.0.0.1:2525'
16
16
 
17
17
  ## Familia has a url, an alias to uri
18
18
  Familia.url.eql?(Familia.uri)
@@ -3,7 +3,7 @@
3
3
  # Test Valkey/Redis middleware components
4
4
  # Mock Valkey/Redis client with middleware for testing
5
5
 
6
- require_relative '../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  class MockDatabase
9
9
  attr_reader :logged_commands
@@ -27,6 +27,46 @@ class MockDatabase
27
27
  end
28
28
  end
29
29
 
30
+ ## increment_middleware_version! increases version counter
31
+ initial_version = Familia.middleware_version
32
+ Familia.increment_middleware_version!
33
+ Familia.middleware_version > initial_version
34
+ #=> true
35
+
36
+ ## increment_middleware_version! increments by exactly 1
37
+ initial_version = Familia.middleware_version
38
+ Familia.increment_middleware_version!
39
+ Familia.middleware_version - initial_version
40
+ #=> 1
41
+
42
+ ## fiber_connection= stores connection with current version
43
+ mock_connection = "test_connection"
44
+ Familia.fiber_connection=(mock_connection)
45
+ stored = Fiber[:familia_connection]
46
+ [stored[0], stored[1] == Familia.middleware_version]
47
+ #=> ["test_connection", true]
48
+
49
+ ## fiber_connection= updates version when middleware version changes
50
+ mock_connection = "test_connection"
51
+ Familia.fiber_connection=(mock_connection)
52
+ old_version = Fiber[:familia_connection][1]
53
+ Familia.increment_middleware_version!
54
+ Familia.fiber_connection=(mock_connection)
55
+ new_version = Fiber[:familia_connection][1]
56
+ new_version > old_version
57
+ #=> true
58
+
59
+ ## clear_fiber_connection! removes fiber-local connection
60
+ Familia.fiber_connection=("test_connection")
61
+ Familia.clear_fiber_connection!
62
+ Fiber[:familia_connection]
63
+ #=> nil
64
+
65
+ ## clear_fiber_connection! is safe when no connection exists
66
+ Familia.clear_fiber_connection!
67
+ Fiber[:familia_connection]
68
+ #=> nil
69
+
30
70
  ## MockDatabase can log commands with timing
31
71
  dbclient = MockDatabase.new
32
72
  result = dbclient.get("test_key")
@@ -1,6 +1,6 @@
1
1
  # try/core/settings_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -1,4 +1,4 @@
1
- require_relative '../helpers/test_helpers'
1
+ require_relative '../../support/helpers/test_helpers'
2
2
 
3
3
  module RefinedContext
4
4
  using Familia::Refinements::TimeLiterals
@@ -2,12 +2,12 @@
2
2
 
3
3
  # Test Familia::Tools - key migration and utility functions
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  ## move_keys across Valkey/Redis instances (if available)
8
8
  begin
9
- source_redis = Redis.new(db: 10)
10
- dest_redis = Redis.new(db: 11)
9
+ source_redis = Redis.new(db: 1, port: 2525)
10
+ dest_redis = Redis.new(db: 2, port: 2525)
11
11
  source_redis.set('test:key1', 'value1')
12
12
  source_redis.set('test:key2', 'value2')
13
13
 
@@ -1,6 +1,6 @@
1
1
  # try/core/utils_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -62,26 +62,29 @@ custom_stamp = Familia.qstamp(3600, time: test_time)
62
62
  Time.at(custom_stamp).utc.hour
63
63
  #=> 14
64
64
 
65
- ## distinguisher handles basic types
66
- str_result = Familia.distinguisher('test')
67
- int_result = Familia.distinguisher(123)
68
- sym_result = Familia.distinguisher(:symbol)
69
- [str_result, int_result, sym_result]
70
- #=> ["test", "123", "symbol"]
65
+ ## identifier_extractor extracts class names
66
+ test_class = Class.new(Familia::Horreum)
67
+ test_class.define_singleton_method(:name) { 'TestClass' }
68
+ Familia.identifier_extractor(test_class)
69
+ #=> "TestClass"
71
70
 
72
- ## distinguisher raises error for high-risk types with strict mode
71
+ ## identifier_extractor extracts identifiers from Familia objects
72
+ customer_class = Class.new(Familia::Horreum) do
73
+ identifier_field :custid
74
+ field :custid
75
+ end
76
+ customer = customer_class.new(custid: 'customer_123')
77
+ Familia.identifier_extractor(customer)
78
+ #=> "customer_123"
79
+
80
+ ## identifier_extractor raises error for non-Familia objects
73
81
  begin
74
- Familia.distinguisher(true, strict_values: true)
82
+ Familia.identifier_extractor({ key: 'value' })
75
83
  rescue Familia::NotDistinguishableError => e
76
84
  e.class
77
85
  end
78
86
  #=> Familia::NotDistinguishableError
79
87
 
80
- ## distinguisher allows high-risk types with non-strict mode
81
- result = Familia.distinguisher(false, strict_values: false)
82
- result
83
- #=> "false"
84
-
85
88
  # Cleanup - restore defaults, leave nothing but footprints
86
89
  Familia.delim(':')
87
90
  Familia.suffix(:object)