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,45 @@
1
+ # Custom Compliance Checklist for Familia
2
+ # Documentation: https://qodo-merge-docs.qodo.ai/tools/compliance/
3
+
4
+ pr_compliances:
5
+ - title: "ErrorHandling"
6
+ compliance_label: true
7
+ objective: "All external API calls and database operations must have proper error handling"
8
+ success_criteria: "Try-catch blocks around external calls with appropriate logging or error handling mechanisms"
9
+ failure_criteria: "External API calls, database operations, or network requests without error handling"
10
+
11
+ - title: "TestCoverage"
12
+ compliance_label: true
13
+ objective: "New features must include corresponding tests using the Tryouts framework"
14
+ success_criteria: "Test files present in try/ directory for new functionality following *_try.rb or *.try.rb naming convention"
15
+ failure_criteria: "New code without test coverage or tests not following Tryouts framework conventions"
16
+
17
+ - title: "ChangelogFragment"
18
+ compliance_label: true
19
+ objective: "User-facing changes must include a changelog"
20
+ success_criteria: "New fragment file in changelog.d/ directory following the naming convention and RST format, or updates to CHANGELOG.rst in root directory, or explicit justification for omission"
21
+ failure_criteria: "User-facing changes without changelog fragment, CHANGELOG.rst updates, or documentation updates"
22
+
23
+ - title: "DocumentationUpdates"
24
+ compliance_label: true
25
+ objective: "API changes must be reflected in documentation"
26
+ success_criteria: "YARD documentation comments for new public methods, or updates to docs/ for significant changes"
27
+ failure_criteria: "New public APIs or significant behavior changes without documentation updates"
28
+
29
+ - title: "BackwardCompatibility"
30
+ compliance_label: true
31
+ objective: "Changes must maintain backward compatibility or document breaking changes"
32
+ success_criteria: "No breaking changes to public APIs, or breaking changes clearly documented in migration guides"
33
+ failure_criteria: "Breaking changes without migration documentation or deprecation warnings"
34
+
35
+ - title: "ThreadSafety"
36
+ compliance_label: true
37
+ objective: "Code handling shared state must be thread-safe"
38
+ success_criteria: "Proper synchronization for shared mutable state, or clear documentation of thread-safety assumptions"
39
+ failure_criteria: "Shared mutable state accessed without synchronization in concurrent contexts"
40
+
41
+ - title: "DatabaseKeyNaming"
42
+ compliance_label: true
43
+ objective: "Database key generation must follow Familia conventions"
44
+ success_criteria: "Keys use delim separator, avoid reserved keywords (ttl, db, valkey, redis), and handle empty identifiers"
45
+ failure_criteria: "Keys using reserved keywords, empty identifiers, or non-standard separators"
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test empty identifier edge cases
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../support/helpers/test_helpers'
6
6
 
7
7
 
8
8
  ## empty string identifier handling
@@ -4,7 +4,7 @@
4
4
  # bug in Tryouts 3.1 that prevents the setup instance vars from
5
5
  # being available to the testcases.
6
6
 
7
- require_relative '../helpers/test_helpers'
7
+ require_relative '../support/helpers/test_helpers'
8
8
 
9
9
  Familia.debug = false
10
10
 
@@ -25,22 +25,31 @@ end
25
25
  @test_hash.keys
26
26
  #=> ["name", "age", "nested"]
27
27
 
28
- ## After save and refresh, default behavior uses symbol keys
28
+ ## After save and refresh, default behavior uses string keys
29
29
  @test_obj.refresh!
30
30
  @test_obj.config.keys
31
- #=> [:name, :age, :nested]
31
+ #=> ["name", "age", "nested"]
32
32
 
33
- ## Nested hash also has symbol keys
34
- @test_obj.config[:nested].keys
35
- #=> [:theme]
33
+ ## Nested hash also has string keys
34
+ @test_obj.config["nested"].keys
35
+ #=> ["theme"]
36
36
 
37
37
  ## Get raw JSON from Valkey/Redis
38
38
  @raw_json = @test_obj.hget('config')
39
39
  @raw_json.class
40
40
  #=> String
41
41
 
42
- ## deserialize_value with default symbolize: true returns symbol keys
43
- @symbol_result = @test_obj.deserialize_value(@raw_json)
42
+ ## deserialize_value with default symbolize: false returns string keys
43
+ @string_result_default = @test_obj.deserialize_value(@raw_json)
44
+ @string_result_default.keys
45
+ #=> ["name", "age", "nested"]
46
+
47
+ ## Nested hash in default result also has string keys
48
+ @string_result_default["nested"].keys
49
+ #=> ["theme"]
50
+
51
+ ## deserialize_value with symbolize: true returns symbol keys
52
+ @symbol_result = @test_obj.deserialize_value(@raw_json, symbolize: true)
44
53
  @symbol_result.keys
45
54
  #=> [:name, :age, :nested]
46
55
 
@@ -48,21 +57,12 @@ end
48
57
  @symbol_result[:nested].keys
49
58
  #=> [:theme]
50
59
 
51
- ## deserialize_value with symbolize: false returns string keys
52
- @string_result = @test_obj.deserialize_value(@raw_json, symbolize: false)
53
- @string_result.keys
54
- #=> ["name", "age", "nested"]
55
-
56
- ## Nested hash in string result also has string keys
57
- @string_result['nested'].keys
58
- #=> ["theme"]
59
-
60
- ## Values are preserved correctly in both cases
60
+ ## Values are preserved correctly with symbol keys
61
61
  @symbol_result[:name]
62
62
  #=> "John"
63
63
 
64
- ## String keys also work correctly
65
- @string_result['name']
64
+ ## Values are preserved correctly with string keys
65
+ @string_result_default['name']
66
66
  #=> "John"
67
67
 
68
68
  ## Arrays are handled correctly too
@@ -71,27 +71,27 @@ end
71
71
  @array_json = @test_obj.hget('config')
72
72
  #=> "[{\"item\":\"value\"},\"string\",123]"
73
73
 
74
+ ## Array with default (symbolize: false) keeps hash keys as strings
75
+ @string_array_default = @test_obj.deserialize_value(@array_json)
76
+ @string_array_default[0].keys
77
+ #=> ["item"]
78
+
74
79
  ## Array with symbolize: true converts hash keys to symbols
75
- @symbol_array = @test_obj.deserialize_value(@array_json)
80
+ @symbol_array = @test_obj.deserialize_value(@array_json, symbolize: true)
76
81
  @symbol_array[0].keys
77
82
  #=> [:item]
78
83
 
79
- ## Array with symbolize: false keeps hash keys as strings
80
- @string_array = @test_obj.deserialize_value(@array_json, symbolize: false)
81
- @string_array[0].keys
82
- #=> ["item"]
83
-
84
- ## Non-hash/array values are returned as-is
84
+ ## JSON-encoded string is parsed correctly
85
85
  @test_obj.deserialize_value('"just a string"')
86
- #=> "\"just a string\""
86
+ #=> "just a string"
87
87
 
88
- ## Non-hash/array values are returned as-is
88
+ ## Non-JSON string returns as-is
89
89
  @test_obj.deserialize_value('just a string')
90
90
  #=> "just a string"
91
91
 
92
- ## A stringified number is still a stringified number
92
+ ## JSON number is parsed to Integer
93
93
  @test_obj.deserialize_value('42')
94
- #=> "42"
94
+ #=> 42
95
95
 
96
96
  ## Invalid JSON returns original string
97
97
  @test_obj.deserialize_value('invalid json')
@@ -1,6 +1,6 @@
1
1
  # try/edge_cases/json_serialization_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
 
@@ -44,7 +44,7 @@ test_obj.simple = 'just a string'
44
44
  test_obj.tags = %w[ruby valkey json familia]
45
45
  test_obj.save
46
46
  test_obj.hgetall
47
- #=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
47
+ #=> {"id"=>"\"json_test_1\"", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"\"just a string\""}
48
48
 
49
49
  ## Test 4: Hash should be deserialized back to Hash
50
50
  test_obj = JsonTest.new 'any_id_will_do'
@@ -0,0 +1,170 @@
1
+ # Edge case tests for deserialize_value with legacy data detection
2
+ #
3
+ # Tests the nuanced deserialization that distinguishes between:
4
+ # - Corrupted JSON (data that looks like JSON but fails to parse)
5
+ # - Legacy plain strings (data that was never JSON)
6
+ # - Valid JSON data
7
+
8
+ require_relative '../../../lib/familia'
9
+ require 'logger'
10
+ require 'stringio'
11
+
12
+ # Capture log output for verification
13
+ @log_output = StringIO.new
14
+ @original_logger = Familia.instance_variable_get(:@logger)
15
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
16
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
17
+
18
+ class TestModel < Familia::Horreum
19
+ identifier_field :test_id
20
+ field :test_id
21
+ field :data
22
+ end
23
+
24
+ @model = TestModel.new(test_id: "test1")
25
+
26
+ ## Valid JSON number deserializes correctly
27
+ @result = @model.deserialize_value("123", field_name: :data)
28
+ @result
29
+ #=> 123
30
+
31
+ ## Valid JSON boolean deserializes correctly
32
+ @result = @model.deserialize_value("true", field_name: :data)
33
+ @result
34
+ #=> true
35
+
36
+ ## Valid JSON string deserializes correctly
37
+ @result = @model.deserialize_value('"hello"', field_name: :data)
38
+ @result
39
+ #=> "hello"
40
+
41
+ ## Valid JSON array deserializes correctly
42
+ @result = @model.deserialize_value('[1,2,3]', field_name: :data)
43
+ @result
44
+ #=> [1, 2, 3]
45
+
46
+ ## Valid JSON object deserializes correctly
47
+ @result = @model.deserialize_value('{"key":"value"}', field_name: :data)
48
+ @result
49
+ #=> {"key"=>"value"}
50
+
51
+ ## Plain string (legacy data) returns as-is
52
+ @log_output = StringIO.new
53
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
54
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
55
+ @result = @model.deserialize_value("plain text", field_name: :data)
56
+ @result
57
+ #=> "plain text"
58
+
59
+ ## Legacy data logs at debug level
60
+ @log_output.rewind
61
+ @log_content = @log_output.read
62
+ puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
63
+ @log_content
64
+ #=~> /Legacy plain string/
65
+
66
+ ## Corrupted JSON starting with { logs error
67
+ @log_output = StringIO.new
68
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
69
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
70
+ @result = @model.deserialize_value("{broken", field_name: :data)
71
+ @result
72
+ #=> "{broken"
73
+
74
+ ## Corrupted JSON logs at error level
75
+ @log_output.rewind
76
+ @log_content = @log_output.read
77
+ puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
78
+ @log_content.match?(/Corrupted JSON/)
79
+ #=> true
80
+
81
+ ## Corrupted JSON starting with [ logs error
82
+ @log_output = StringIO.new
83
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
84
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
85
+ @result = @model.deserialize_value("[1,2,", field_name: :data)
86
+ @result
87
+ #=> "[1,2,"
88
+
89
+ ## Corrupted array logs at error level
90
+ @log_output.rewind
91
+ @log_content = @log_output.read
92
+ @log_content.match?(/Corrupted JSON/)
93
+ #=> true
94
+
95
+ ## Corrupted JSON starting with quote logs error
96
+ @log_output = StringIO.new
97
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
98
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
99
+ @result = @model.deserialize_value('"unterminated', field_name: :data)
100
+ @result
101
+ #=> '"unterminated'
102
+
103
+ ## Unterminated string logs at error level
104
+ @log_output.rewind
105
+ @log_content = @log_output.read
106
+ @log_content.match?(/Corrupted JSON/)
107
+ #=> true
108
+
109
+ ## Corrupted boolean-like value logs error
110
+ @log_output = StringIO.new
111
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
112
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
113
+ @result = @model.deserialize_value("true123", field_name: :data)
114
+ @result
115
+ #=> "true123"
116
+
117
+ ## Plain text starting with 'true' is legacy data
118
+ @log_output.rewind
119
+ @log_content = @log_output.read
120
+ @log_content
121
+ #=~> /Legacy plain string/
122
+
123
+ ## Field name context appears in error messages
124
+ @log_output = StringIO.new
125
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
126
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
127
+ @result = @model.deserialize_value("{broken", field_name: :important_field)
128
+ @log_output.rewind
129
+ @log_content = @log_output.read
130
+ @log_content.match?(/TestModel#important_field/)
131
+ #=> true
132
+
133
+ ## dbkey context appears in error messages when available
134
+ @model.save
135
+ @log_output = StringIO.new
136
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
137
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
138
+ @result = @model.deserialize_value("{broken", field_name: :data)
139
+ @log_output.rewind
140
+ @log_content = @log_output.read
141
+ @log_content.match?(/#{Regexp.escape(@model.dbkey)}/)
142
+ #=> true
143
+
144
+ ## Empty string returns nil
145
+ @result = @model.deserialize_value("", field_name: :data)
146
+ @result
147
+ #=> nil
148
+
149
+ ## nil returns nil
150
+ @result = @model.deserialize_value(nil, field_name: :data)
151
+ @result
152
+ #=> nil
153
+
154
+ ## JSON null deserializes to nil
155
+ @result = @model.deserialize_value("null", field_name: :data)
156
+ @result
157
+ #=> nil
158
+
159
+ ## Symbolize option works with hash keys
160
+ @result = @model.deserialize_value('{"name":"test"}', symbolize: true, field_name: :data)
161
+ @result.keys.first.class
162
+ #=> Symbol
163
+
164
+ ## Default keeps string keys
165
+ @result = @model.deserialize_value('{"name":"test"}', field_name: :data)
166
+ @result.keys.first.class
167
+ #=> String
168
+
169
+ # Teardown
170
+ Familia.instance_variable_set(:@logger, @original_logger)
@@ -1,6 +1,6 @@
1
1
  # Test connection race conditions
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## concurrent connection access test
6
6
  user_class = Class.new(Familia::Horreum) do
@@ -1,6 +1,6 @@
1
1
  # Test reserved keyword handling
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## attempting to use ttl as field name causes error
6
6
  TestClass = Class.new(Familia::Horreum) do
@@ -1,6 +1,6 @@
1
1
  # try/edge_cases/string_coercion_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,6 +1,6 @@
1
1
  # Test TTL side effects
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## field update behavior with TTL
6
6
  begin
@@ -3,7 +3,7 @@
3
3
  require 'concurrent'
4
4
  require 'base64'
5
5
 
6
- require_relative '../../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  test_keys = {
9
9
  v1: Base64.strict_encode64('a' * 32),
@@ -1,6 +1,6 @@
1
1
  # try/features/encryption_fields/concealed_string_core_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  Familia.debug = false
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  # Setup encryption keys for testing
8
8
  test_keys = {
@@ -1,6 +1,6 @@
1
1
  # try/features/encrypted_fields_core_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test constants will be redefined in each test since variables don't persist
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
  require 'base64'
7
7
 
8
8
 
@@ -3,7 +3,7 @@
3
3
  # Security tests for the no-cache encryption strategy
4
4
  # These tests verify that we maintain security properties by NOT caching derived keys
5
5
 
6
- require_relative '../../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  test_keys = {
9
9
  v1: Base64.strict_encode64('a' * 32),
@@ -1,6 +1,6 @@
1
1
  # try/features/encrypted_fields_security_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  # Define all test classes up front to avoid tryouts retry conflicts
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  # Setup encryption keys for error testing
8
8
  @test_keys = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  test_keys = {
8
8
  v1: Base64.strict_encode64('a' * 32),
@@ -1,4 +1,4 @@
1
- require_relative '../../helpers/test_helpers'
1
+ require_relative '../../support/helpers/test_helpers'
2
2
  require 'base64'
3
3
 
4
4
  # Setup encryption configuration
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  # Setup multiple key versions for rotation testing
8
8
  @test_keys = {
@@ -1,7 +1,7 @@
1
1
  # try/features/encryption_fields/memory_security_try.rb
2
2
 
3
3
  require 'base64'
4
- require_relative '../../helpers/test_helpers'
4
+ require_relative '../../support/helpers/test_helpers'
5
5
 
6
6
  test_keys = {
7
7
  v1: Base64.strict_encode64('a' * 32),
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  # This tryouts file is based on the premise that there is no current key
8
8
  # version set. This is a global setting so if other tryouts rely on
@@ -3,7 +3,7 @@
3
3
  require 'base64'
4
4
  require 'set'
5
5
 
6
- require_relative '../../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  test_keys = {
9
9
  v1: Base64.strict_encode64('a' * 32),
@@ -1,6 +1,6 @@
1
1
  # try/features/encryption_fields/secure_by_default_behavior_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  Familia.debug = false
@@ -3,7 +3,7 @@
3
3
  require 'concurrent'
4
4
  require 'base64'
5
5
 
6
- require_relative '../../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  # Setup encryption keys for testing
9
9
  test_keys = {
@@ -1,6 +1,6 @@
1
1
  # try/features/encryption_fields/universal_serialization_safety_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  Familia.debug = false
@@ -5,7 +5,7 @@
5
5
 
6
6
  require 'base64'
7
7
 
8
- require_relative '../helpers/test_helpers'
8
+ require_relative '../../support/helpers/test_helpers'
9
9
  require 'familia/encryption/providers/xchacha20_poly1305_provider'
10
10
 
11
11
  # SETUP
@@ -1,7 +1,7 @@
1
1
  # try/encryption/encryption_core_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
4
- require_relative '../../lib/familia/encryption'
3
+ require_relative '../../support/helpers/test_helpers'
4
+ require_relative '../../../lib/familia/encryption'
5
5
  require 'base64'
6
6
 
7
7
  # Test constants will be redefined in each test since variables don't persist
@@ -6,7 +6,7 @@
6
6
 
7
7
  require 'base64'
8
8
 
9
- require_relative '../helpers/test_helpers'
9
+ require_relative '../../support/helpers/test_helpers'
10
10
 
11
11
  @test_keys = {
12
12
  v1: Base64.strict_encode64('a' * 32)
@@ -5,7 +5,7 @@
5
5
 
6
6
  require 'base64'
7
7
 
8
- require_relative '../helpers/test_helpers'
8
+ require_relative '../../support/helpers/test_helpers'
9
9
 
10
10
  # Test basic functionality
11
11
 
@@ -1,6 +1,6 @@
1
1
  # try/encryption/providers/aes_gcm_provider_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  ## AES-GCM provider availability check (always available with OpenSSL)
@@ -1,6 +1,6 @@
1
1
  # try/encryption/providers/xchacha20_poly1305_provider_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  ## XChaCha20Poly1305 provider availability check
@@ -5,7 +5,7 @@
5
5
 
6
6
  require 'base64'
7
7
 
8
- require_relative '../helpers/test_helpers'
8
+ require_relative '../../support/helpers/test_helpers'
9
9
 
10
10
  ## Test successful encryption
11
11
  test_keys = { v1: Base64.strict_encode64('a' * 32) }
@@ -1,7 +1,7 @@
1
1
  # try/encryption/secure_memory_handling_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
4
- require_relative '../../lib/familia/encryption/providers/secure_xchacha20_poly1305_provider'
3
+ require_relative '../../support/helpers/test_helpers'
4
+ require_relative '../../../lib/familia/encryption/providers/secure_xchacha20_poly1305_provider'
5
5
  require 'base64'
6
6
 
7
7
  # SETUP
@@ -1,6 +1,6 @@
1
1
  # try/features/expiration_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,6 +1,6 @@
1
1
  # try/features/external_identifier/external_identifier_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