familia 2.0.0.pre17 → 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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +60 -0
  3. data/CLAUDE.md +9 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +13 -0
  6. data/bin/irb +1 -1
  7. data/docs/guides/core-field-system.md +48 -26
  8. data/docs/migrating/v2.0.0-pre18.md +58 -0
  9. data/docs/qodo-merge-compliance.md +96 -0
  10. data/lib/familia/base.rb +0 -2
  11. data/lib/familia/connection/middleware.rb +58 -4
  12. data/lib/familia/connection.rb +1 -1
  13. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  14. data/lib/familia/data_type/serialization.rb +5 -5
  15. data/lib/familia/data_type.rb +2 -2
  16. data/lib/familia/encryption/encrypted_data.rb +12 -2
  17. data/lib/familia/encryption/manager.rb +11 -4
  18. data/lib/familia/features/autoloader.rb +3 -1
  19. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  20. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  21. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
  22. data/lib/familia/features/safe_dump.rb +2 -3
  23. data/lib/familia/horreum/database_commands.rb +1 -1
  24. data/lib/familia/horreum/definition.rb +6 -37
  25. data/lib/familia/horreum/management.rb +17 -12
  26. data/lib/familia/horreum/persistence.rb +1 -1
  27. data/lib/familia/horreum/serialization.rb +91 -73
  28. data/lib/familia/horreum.rb +10 -6
  29. data/lib/familia/identifier_extractor.rb +60 -0
  30. data/lib/familia/logging.rb +271 -112
  31. data/lib/familia/refinements.rb +0 -1
  32. data/lib/familia/version.rb +1 -1
  33. data/lib/familia.rb +2 -2
  34. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  35. data/pr_agent.toml +31 -0
  36. data/pr_compliance_checklist.yaml +45 -0
  37. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  38. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  39. data/try/edge_cases/json_serialization_try.rb +2 -2
  40. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  41. data/try/edge_cases/race_conditions_try.rb +1 -1
  42. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  43. data/try/edge_cases/string_coercion_try.rb +1 -1
  44. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  45. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  46. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  47. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  48. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  49. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  50. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  51. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  52. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  53. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  54. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  55. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  56. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  57. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  58. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  59. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  60. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  61. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  62. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  63. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  64. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  65. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  66. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  67. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  68. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  69. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  70. data/try/features/expiration/expiration_try.rb +1 -1
  71. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  72. data/try/features/feature_dependencies_try.rb +1 -1
  73. data/try/features/feature_improvements_try.rb +1 -1
  74. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  75. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  76. data/try/features/quantization/quantization_try.rb +1 -1
  77. data/try/features/real_feature_integration_try.rb +17 -14
  78. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  79. data/try/features/relationships/indexing_try.rb +6 -1
  80. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  81. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  82. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  83. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  84. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  85. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  86. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  87. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  88. data/try/features/relationships/relationships_performance_try.rb +1 -1
  89. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  90. data/try/features/relationships/relationships_try.rb +1 -1
  91. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  92. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  93. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  94. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  95. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  96. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  97. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  98. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  99. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  100. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  101. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  102. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  103. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  104. data/try/{core → integration/connection}/pools_try.rb +1 -1
  105. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  106. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  107. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  108. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  109. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  110. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  111. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  112. data/try/{core → integration}/create_method_try.rb +1 -1
  113. data/try/integration/cross_component_try.rb +1 -1
  114. data/try/{core → integration}/database_consistency_try.rb +11 -8
  115. data/try/{core → integration}/familia_extended_try.rb +1 -1
  116. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  117. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  118. data/try/{models → integration/models}/customer_try.rb +1 -1
  119. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  120. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  121. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  122. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  123. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  124. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  125. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  126. data/try/performance/benchmarks_try.rb +2 -2
  127. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  128. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  129. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  130. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  131. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  132. data/try/{core → unit/core}/connection_try.rb +1 -1
  133. data/try/{core → unit/core}/errors_try.rb +1 -1
  134. data/try/{core → unit/core}/extensions_try.rb +1 -1
  135. data/try/unit/core/familia_logger_try.rb +110 -0
  136. data/try/{core → unit/core}/familia_try.rb +1 -1
  137. data/try/{core → unit/core}/middleware_try.rb +41 -1
  138. data/try/{core → unit/core}/settings_try.rb +1 -1
  139. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  140. data/try/{core → unit/core}/tools_try.rb +1 -1
  141. data/try/{core → unit/core}/utils_try.rb +17 -14
  142. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  143. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  144. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  145. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  146. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  147. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  148. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  149. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  150. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  151. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  152. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
  153. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  154. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  155. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  156. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  157. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  158. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  159. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  160. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  161. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  162. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  163. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  164. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  165. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  166. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  167. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  168. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  169. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  170. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  171. metadata +134 -125
  172. data/lib/familia/distinguisher.rb +0 -85
  173. data/lib/familia/refinements/logger_trace.rb +0 -60
  174. data/try/refinements/logger_trace_methods_try.rb +0 -44
  175. /data/try/{debugging → support/debugging}/README.md +0 -0
  176. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  177. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  178. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  179. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  180. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  181. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  182. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  183. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  184. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  185. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  186. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  187. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  188. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  189. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  190. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  191. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  192. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  193. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  194. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  195. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  196. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  197. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  198. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  199. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  200. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  201. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  202. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  203. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  204. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  205. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  206. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  207. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  208. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  209. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  210. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  211. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  212. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  213. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  214. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  215. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  216. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  217. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  218. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  219. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  220. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -7,21 +7,20 @@ module Familia
7
7
  module Serialization
8
8
  # Converts the object's persistent fields to a hash for external use.
9
9
  #
10
- # Serializes persistent field values for external consumption (APIs, logs),
11
- # excluding non-loggable fields like encrypted fields for security.
12
- # Only non-nil values are included in the resulting hash.
10
+ # Returns actual Ruby values (String, Integer, Hash, etc.) for API consumption,
11
+ # NOT JSON-encoded strings. Excludes non-loggable fields like encrypted fields
12
+ # for security.
13
13
  #
14
- # @return [Hash] Hash with field names as keys and serialized values
15
- # safe for external exposure
14
+ # @return [Hash] Hash with field names as string keys and Ruby values
16
15
  #
17
16
  # @example Converting an object to hash format for API response
18
17
  # user = User.new(name: "John", email: "john@example.com", age: 30)
19
18
  # user.to_h
20
- # # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
21
- # # encrypted fields are excluded for security
19
+ # # => {"name"=>"John", "email"=>"john@example.com", "age"=>30}
20
+ # # Note: Returns actual Ruby types, not JSON strings
22
21
  #
23
- # @note Only loggable fields are included for security
24
- # @note Only fields with non-nil values are included
22
+ # @note Only loggable fields are included. Encrypted fields are excluded.
23
+ # @note Nil values are excluded from the returned hash (storage optimization)
25
24
  #
26
25
  def to_h
27
26
  self.class.persistent_fields.each_with_object({}) do |field, hsh|
@@ -30,40 +29,45 @@ module Familia
30
29
  # Security: Skip non-loggable fields (e.g., encrypted fields)
31
30
  next unless field_type.loggable
32
31
 
33
- method_name = field_type.method_name
34
- val = send(method_name)
35
- prepared = serialize_value(val)
36
- Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
32
+ val = send(field_type.method_name)
33
+ Familia.ld " [to_h] field: #{field} val: #{val.class}"
37
34
 
38
- # Only include non-nil values in the hash for Valkey
39
- # Use string key for database compatibility
40
- hsh[field.to_s] = prepared unless prepared.nil?
35
+ # Use string key for external API compatibility
36
+ # Return Ruby values, not JSON-encoded strings
37
+ hsh[field.to_s] = val
41
38
  end
42
39
  end
43
40
 
44
41
  # Converts the object's persistent fields to a hash for database storage.
45
42
  #
46
- # Serializes ALL persistent field values for database storage, including
47
- # encrypted fields. This is used internally by commit_fields and other
48
- # persistence operations.
43
+ # Returns JSON-encoded strings for ALL persistent field values, ready for
44
+ # Redis storage. Unlike to_h, this includes encrypted fields and serializes
45
+ # values using serialize_value (JSON encoding).
46
+ #
47
+ # @return [Hash] Hash with field names as string keys and JSON-encoded values
49
48
  #
50
- # @return [Hash] Hash with field names as keys and serialized values
51
- # ready for database storage
49
+ # @example Internal storage preparation
50
+ # user = User.new(name: "John", age: 30)
51
+ # user.to_h_for_storage
52
+ # # => {"name"=>"\"John\"", "age"=>"30"}
53
+ # # Note: Strings are JSON-encoded with quotes
52
54
  #
53
- # @note Includes ALL persistent fields, including encrypted fields
54
- # @note Only fields with non-nil values are included for storage efficiency
55
+ # @note This is an internal method used by commit_fields and hmset
56
+ # @note Nil values are excluded to optimize Redis storage
55
57
  #
56
58
  def to_h_for_storage
57
59
  self.class.persistent_fields.each_with_object({}) do |field, hsh|
58
60
  field_type = self.class.field_types[field]
59
- method_name = field_type.method_name
60
- val = send(method_name)
61
+
62
+ val = send(field_type.method_name)
61
63
  prepared = serialize_value(val)
62
- Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
63
64
 
64
- # Only include non-nil values in the hash for Valkey
65
+ if Familia.debug?
66
+ Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
67
+ end
68
+
65
69
  # Use string key for database compatibility
66
- hsh[field.to_s] = prepared unless prepared.nil?
70
+ hsh[field.to_s] = prepared
67
71
  end
68
72
  end
69
73
 
@@ -84,7 +88,7 @@ module Familia
84
88
  # methods to maintain data consistency across operations.
85
89
  #
86
90
  def to_a
87
- self.class.persistent_fields.filter_map do |field|
91
+ self.class.persistent_fields.map do |field|
88
92
  field_type = self.class.field_types[field]
89
93
 
90
94
  # Security: Skip non-loggable fields (e.g., encrypted fields)
@@ -92,80 +96,94 @@ module Familia
92
96
 
93
97
  method_name = field_type.method_name
94
98
  val = send(method_name)
95
- prepared = serialize_value(val)
96
- Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
97
- prepared
99
+ Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class}"
100
+
101
+ # Return actual Ruby values, including nil to maintain array positions
102
+ val
98
103
  end
99
104
  end
100
105
 
101
106
  # Serializes a Ruby object for Valkey storage.
102
107
  #
103
- # Converts Ruby objects into the DB-compatible string representations using
104
- # the Familia distinguisher for type coercion. Falls back to JSON serialization
105
- # for complex types (Hash, Array) when the primary distinguisher returns nil.
108
+ # Converts ALL Ruby values (including strings) to JSON-encoded strings for
109
+ # type-safe storage. This ensures round-trip type preservation: the type you
110
+ # save is the type you get back.
106
111
  #
107
112
  # The serialization process:
108
- # 1. Attempts conversion using Familia.distinguisher with relaxed type checking
109
- # 2. For Hash/Array types that return nil, tries custom dump_method or Familia::JsonSerializer.dump
110
- # 3. Logs warnings when serialization fails completely
113
+ # 1. ConcealedStrings (encrypted values) extract encrypted_value
114
+ # 2. ALL other types JSON serialization (String, Integer, Boolean, Float, nil, Hash, Array)
111
115
  #
112
116
  # @param val [Object] The Ruby object to serialize for Valkey storage
113
117
  #
114
- # @return [String, nil] The serialized value ready for Valkey storage, or nil
115
- # if serialization failed
118
+ # @return [String] JSON-encoded string representation
119
+ #
120
+ # @example Type preservation through JSON encoding
121
+ # serialize_value("007") # => "\"007\"" (JSON string)
122
+ # serialize_value(123) # => "123" (JSON number)
123
+ # serialize_value(true) # => "true" (JSON boolean)
124
+ # serialize_value({a: 1}) # => "{\"a\":1}" (JSON object)
116
125
  #
117
- # @example Serializing different data types
118
- # serialize_value("hello") # => "hello"
119
- # serialize_value(42) # => "42"
120
- # serialize_value({name: "John"}) # => '{"name":"John"}'
121
- # serialize_value([1, 2, 3]) # => "[1,2,3]"
126
+ # @note Strings are JSON-encoded to prevent type coercion bugs where
127
+ # string "123" would be indistinguishable from integer 123 in storage
122
128
  #
123
129
  # @note This method integrates with Familia's type system and supports
124
130
  # custom serialization methods when available on the object
125
131
  #
126
- # @see Familia.distinguisher The primary serialization mechanism
132
+ # @see Familia.identifier_extractor For extracting identifiers from Familia objects
127
133
  #
128
134
  def serialize_value(val)
129
135
  # Security: Handle ConcealedString safely - extract encrypted data for storage
130
136
  return val.encrypted_value if val.respond_to?(:encrypted_value)
131
137
 
132
- prepared = Familia.distinguisher(val, strict_values: false)
133
-
134
- # If the distinguisher returns nil, try using the dump_method but only
135
- # use JSON serialization for complex types that need it.
136
- if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
137
- prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
138
- end
139
-
140
- # If both the distinguisher and dump_method return nil, log an error
141
- Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
142
-
143
- prepared
138
+ # ALWAYS write valid JSON for type preservation
139
+ # This includes strings, which get JSON-encoded with wrapping quotes
140
+ Familia::JsonSerializer.dump(val)
144
141
  end
145
142
 
146
- # Converts a Database string value back to its original Ruby type
143
+ # Converts a Redis string value back to its original Ruby type
144
+ #
145
+ # This method deserializes JSON strings back to their original Ruby types
146
+ # (Integer, Boolean, Float, nil, Hash, Array). Plain strings that cannot
147
+ # be parsed as JSON are returned as-is.
147
148
  #
148
- # This method attempts to deserialize JSON strings back to their original
149
- # Hash or Array types. Simple string values are returned as-is.
149
+ # This pairs with serialize_value which JSON-encodes all non-string values.
150
+ # The contract ensures type preservation across Redis storage:
151
+ # - Strings stored as-is → returned as-is
152
+ # - All other types JSON-encoded → JSON-decoded back to original type
150
153
  #
151
- # @param val [String] The string value from Database to deserialize
152
- # @param symbolize [Boolean] Whether to symbolize hash keys (default: true for compatibility)
153
- # @return [Object] The deserialized value (Hash, Array, or original string)
154
+ # @param val [String] The string value from Redis to deserialize
155
+ # @param symbolize [Boolean] Whether to symbolize hash keys (default: false)
156
+ # @param field_name [Symbol, nil] Optional field name for better error context
157
+ # @return [Object] The deserialized value with original Ruby type, or the original string if not JSON
154
158
  #
155
- def deserialize_value(val, symbolize: true)
156
- return val if val.nil? || val == ''
159
+ def deserialize_value(val, symbolize: false, field_name: nil)
160
+ return nil if val.nil? || val == ''
157
161
 
158
- # Try to parse as JSON first for complex types
159
162
  begin
160
- parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
161
- # Only return parsed value if it's a complex type (Hash/Array)
162
- # Simple values should remain as strings
163
- return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
163
+ Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
164
164
  rescue Familia::SerializerError
165
- # Not valid JSON, return as-is
165
+ log_deserialization_issue(val, field_name)
166
+ val
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def log_deserialization_issue(val, field_name)
173
+ context = field_name ? "#{self.class}##{field_name}" : self.class.to_s
174
+ dbkey_info = respond_to?(:dbkey) ? dbkey : 'no dbkey'
175
+
176
+ msg = if looks_like_json?(val)
177
+ "Corrupted JSON in #{context}: #{val.inspect} (#{dbkey_info})"
178
+ else
179
+ "Legacy plain string in #{context}: #{val.inspect} (#{dbkey_info})"
166
180
  end
167
181
 
168
- val
182
+ Familia.le(msg)
183
+ end
184
+
185
+ def looks_like_json?(val)
186
+ val.start_with?('{', '[', '"') || %w[true false null].include?(val)
169
187
  end
170
188
  end
171
189
  end
@@ -299,7 +299,9 @@ module Familia
299
299
 
300
300
  def initialize_with_keyword_args_deserialize_value(**fields)
301
301
  # Deserialize Database string values back to their original types
302
- deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
302
+ deserialized_fields = fields.each_with_object({}) do |(field_name, value), hsh|
303
+ hsh[field_name] = deserialize_value(value, field_name: field_name)
304
+ end
303
305
  initialize_with_keyword_args(**deserialized_fields)
304
306
  end
305
307
 
@@ -308,7 +310,7 @@ module Familia
308
310
  #
309
311
  # This method is part of horreum.rb rather than serialization.rb because it
310
312
  # operates solely on the provided values and doesn't query Database or other
311
- # external sources. That's why it's called "optimistic" refresh: it assumes
313
+ # external sources. That's why it's called "naive" refresh: it assumes
312
314
  # the provided values are correct and updates the object accordingly.
313
315
  #
314
316
  # @see #refresh!
@@ -316,8 +318,8 @@ module Familia
316
318
  # @param fields [Hash] A hash of field names and their new values to update
317
319
  # the object with.
318
320
  # @return [Array] The list of field names that were updated.
319
- def optimistic_refresh(**fields)
320
- Familia.ld "[optimistic_refresh] #{self.class} #{dbkey} #{fields.keys}"
321
+ def naive_refresh(**fields)
322
+ Familia.ld "[naive_refresh] #{self.class} #{dbkey} #{fields.keys}"
321
323
  initialize_with_keyword_args_deserialize_value(**fields)
322
324
  end
323
325
 
@@ -401,8 +403,10 @@ module Familia
401
403
  self.class.fields.filter_map do |field|
402
404
  # Database will give us field names as strings back, but internally
403
405
  # we use symbols. So we check for both.
404
- value = fields[field.to_sym] || fields[field.to_s]
405
- if value
406
+ # Use fetch with default to avoid || operator which skips false values
407
+ value = fields.fetch(field.to_sym) { fields[field.to_s] }
408
+ # Check for nil explicitly to allow false and 0 values
409
+ unless value.nil?
406
410
  # Use the mapped method name, not the field name
407
411
  method_name = self.class.field_method_map[field] || field
408
412
  send(:"#{method_name}=", value)
@@ -0,0 +1,60 @@
1
+ # lib/familia/identifier_extractor.rb
2
+
3
+ module Familia
4
+ # IdentifierExtractor - Extracts identifiers from Familia objects for storage
5
+ #
6
+ # This module provides a focused mechanism for converting object references
7
+ # into Redis-storable strings. It handles two primary cases:
8
+ #
9
+ # 1. Class references: Customer → "Customer"
10
+ # 2. Familia::Base instances: customer_obj → customer_obj.identifier
11
+ #
12
+ # This is primarily used by DataType serialization when storing object
13
+ # references in Redis data structures (lists, sets, zsets). It extracts
14
+ # the identifier rather than serializing the entire object.
15
+ #
16
+ # @example With class_zset
17
+ # class Customer < Familia::Horreum
18
+ # class_zset :instances, class: self
19
+ # end
20
+ # # When adding: Customer.instances.add(customer_obj)
21
+ # # Stores: customer_obj.identifier (e.g., "customer_123")
22
+ #
23
+ module IdentifierExtractor
24
+ # Extracts a Redis-storable identifier from a Familia object or class.
25
+ #
26
+ # @param value [Object] The value to extract an identifier from
27
+ # @return [String] The extracted identifier or class name
28
+ # @raise [Familia::NotDistinguishableError] If value is not a Class or Familia::Base
29
+ #
30
+ def identifier_extractor(value, strict_values: true)
31
+ case value
32
+ when ::Symbol, ::String, ::Integer, ::Float
33
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'simple_value' if Familia.debug?
34
+ # DataTypes (lists, sets, zsets) can store simple values directly
35
+ # Convert to string for Redis storage
36
+ value.to_s
37
+
38
+ when Class
39
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'class' if Familia.debug?
40
+ value.name
41
+
42
+ when Familia::Base
43
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_instance' if Familia.debug?
44
+ value.identifier
45
+
46
+ else
47
+ # Check if value's class inherits from Familia::Base
48
+ if value.class.ancestors.member?(Familia::Base)
49
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_ancestor' if Familia.debug?
50
+ value.identifier
51
+ else
52
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'error' if Familia.debug?
53
+ raise Familia::NotDistinguishableError, value
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ extend IdentifierExtractor
60
+ end