familia 2.0.0.pre4 → 2.0.0.pre6

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,535 @@
1
+ # lib/familia/horreum/serialization.rb
2
+ #
3
+ module Familia
4
+ # Familia::Horreum
5
+ #
6
+ # Core persistence class for object-relational mapping with Valkey/Redis.
7
+ # Provides serialization, field management, and database interaction capabilities.
8
+ #
9
+ class Horreum
10
+ # Valid return values from database commands
11
+ #
12
+ # Defines the set of acceptable response values that indicate successful
13
+ # command execution in Valkey operations. These values are used to validate
14
+ # database responses and determine operation success.
15
+ #
16
+ # @return [Array<String, Boolean, Integer, nil>] Frozen array of valid return values:
17
+ # - "OK" - Standard success response for most commands
18
+ # - true - Boolean success indicator
19
+ # - 1 - Numeric success indicator (operation performed)
20
+ # - 0 - Numeric indicator (operation attempted, no change needed)
21
+ # - nil - Valid response for certain operations
22
+ #
23
+ # @example Validating a command response
24
+ # response = redis.set("key", "value")
25
+ # valid = @valid_command_return_values.include?(response)
26
+ # # => true if response is "OK"
27
+ #
28
+ @valid_command_return_values = ['OK', true, 1, 0, nil].freeze
29
+
30
+ class << self
31
+ attr_reader :valid_command_return_values
32
+ end
33
+
34
+ # Serialization: Object persistence and retrieval from the DB
35
+ # Handles conversion between Ruby objects and Valkey hash storage
36
+ #
37
+ module Serialization
38
+ # Persists the object to Valkey storage with automatic timestamping.
39
+ #
40
+ # Saves the current object state to Valkey storage, automatically setting
41
+ # created and updated timestamps if the object supports them. The method
42
+ # commits all persistent fields and optionally updates the key's expiration.
43
+ #
44
+ # @param update_expiration [Boolean] Whether to update the key's expiration
45
+ # time after saving. Defaults to true.
46
+ #
47
+ # @return [Boolean] true if the save operation was successful, false otherwise.
48
+ #
49
+ # @example Save an object to Valkey
50
+ # user = User.new(name: "John", email: "john@example.com")
51
+ # user.save
52
+ # # => true
53
+ #
54
+ # @example Save without updating expiration
55
+ # user.save(update_expiration: false)
56
+ # # => true
57
+ #
58
+ # @note When Familia.debug? is enabled, this method will trace the save
59
+ # operation for debugging purposes.
60
+ #
61
+ # @see #commit_fields The underlying method that performs the field persistence
62
+ #
63
+ def save(update_expiration: true)
64
+ Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug?
65
+
66
+ # No longer need to sync computed identifier with a cache field
67
+ self.created ||= Familia.now.to_i if respond_to?(:created)
68
+ self.updated = Familia.now.to_i if respond_to?(:updated)
69
+
70
+ # Commit our tale to the Database chronicles
71
+ #
72
+ ret = commit_fields(update_expiration: update_expiration)
73
+
74
+ Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
75
+
76
+ # Did Database accept our offering?
77
+ !ret.nil?
78
+ end
79
+
80
+ # Saves the object to Valkey storage only if it doesn't already exist.
81
+ #
82
+ # Conditionally persists the object to Valkey storage by first checking if the
83
+ # identifier field already exists. If the object already exists in storage,
84
+ # raises an error. Otherwise, proceeds with a normal save operation including
85
+ # automatic timestamping.
86
+ #
87
+ # This method provides atomic conditional creation to prevent duplicate objects
88
+ # from being saved when uniqueness is required based on the identifier field.
89
+ #
90
+ # @param update_expiration [Boolean] Whether to update the key's expiration
91
+ # time after saving. Defaults to true.
92
+ #
93
+ # @return [Boolean] true if the save operation was successful
94
+ #
95
+ # @raise [Familia::RecordExistsError] If an object with the same identifier
96
+ # already exists in Valkey storage
97
+ #
98
+ # @example Save a new user only if it doesn't exist
99
+ # user = User.new(id: 123, name: "John")
100
+ # user.save_if_not_exists
101
+ # # => true (saved successfully)
102
+ #
103
+ # @example Attempting to save an existing object
104
+ # existing_user = User.new(id: 123, name: "Jane")
105
+ # existing_user.save_if_not_exists
106
+ # # => raises Familia::RecordExistsError
107
+ #
108
+ # @example Save without updating expiration
109
+ # user.save_if_not_exists(update_expiration: false)
110
+ # # => true
111
+ #
112
+ # @note This method uses HSETNX to atomically check and set the identifier
113
+ # field, ensuring race-condition-free conditional creation.
114
+ #
115
+ # @see #save The underlying save method called when the object doesn't exist
116
+ #
117
+ # Check if save_if_not_exists is implemented correctly. It should:
118
+ #
119
+ # Check if record exists
120
+ # If exists, raise Familia::RecordExistsError
121
+ # If not exists, save
122
+ def save_if_not_exists(update_expiration: true)
123
+ identifier_field = self.class.identifier_field
124
+
125
+ Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
126
+ Familia.trace :SAVE_IF_NOT_EXISTS, dbclient, uri, caller(1..1) if Familia.debug?
127
+
128
+ dbclient.watch(dbkey) do
129
+ if dbclient.exists(dbkey).positive?
130
+ dbclient.unwatch
131
+ raise Familia::RecordExistsError, dbkey
132
+ end
133
+
134
+ result = dbclient.multi do |multi|
135
+ multi.hmset(dbkey, to_h_for_storage)
136
+ end
137
+
138
+ result.is_a?(Array) # transaction succeeded
139
+ end
140
+ end
141
+
142
+ # Commits object fields to the DB storage.
143
+ #
144
+ # Persists the current state of all object fields to the DB using HMSET.
145
+ # Optionally updates the key's expiration time if the feature is enabled
146
+ # for the object's class.
147
+ #
148
+ # @param update_expiration [Boolean] Whether to update the expiration time
149
+ # of the Valkey key. Defaults to true.
150
+ #
151
+ # @return [Object] The result of the HMSET operation from the DB.
152
+ #
153
+ # @example Basic usage
154
+ # user.name = "John"
155
+ # user.email = "john@example.com"
156
+ # result = user.commit_fields
157
+ #
158
+ # @example Without updating expiration
159
+ # result = user.commit_fields(update_expiration: false)
160
+ #
161
+ # @note The expiration update is only performed for classes that have
162
+ # the expiration feature enabled. For others, it's a no-op.
163
+ #
164
+ # @note This method performs debug logging of the object's class, dbkey,
165
+ # and current state before committing to the DB.
166
+ #
167
+ def commit_fields(update_expiration: true)
168
+ prepared_value = to_h_for_storage
169
+ Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
170
+
171
+ result = hmset(prepared_value)
172
+
173
+ # Only classes that have the expiration ferature enabled will
174
+ # actually set an expiration time on their keys. Otherwise
175
+ # this will be a no-op that simply logs the attempt.
176
+ update_expiration(default_expiration: nil) if update_expiration
177
+
178
+ result
179
+ end
180
+
181
+ # Updates multiple fields atomically in a Database transaction.
182
+ #
183
+ # @param fields [Hash] Field names and values to update. Special key :update_expiration
184
+ # controls whether to update key expiration (default: true)
185
+ # @return [MultiResult] Transaction result
186
+ #
187
+ # @example Update multiple fields without affecting expiration
188
+ # metadata.batch_update(viewed: 1, updated: Time.now.to_i, update_expiration: false)
189
+ #
190
+ # @example Update fields with expiration refresh
191
+ # user.batch_update(name: "John", email: "john@example.com")
192
+ #
193
+ def batch_update(**kwargs)
194
+ update_expiration = kwargs.delete(:update_expiration) { true }
195
+ fields = kwargs
196
+
197
+ Familia.trace :BATCH_UPDATE, dbclient, fields.keys, caller(1..1) if Familia.debug?
198
+
199
+ command_return_values = transaction do |conn|
200
+ fields.each do |field, value|
201
+ prepared_value = serialize_value(value)
202
+ conn.hset dbkey, field, prepared_value
203
+ # Update instance variable to keep object in sync
204
+ send("#{field}=", value) if respond_to?("#{field}=")
205
+ end
206
+ end
207
+
208
+ # Update expiration if requested and supported
209
+ self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
210
+
211
+ # Return same MultiResult format as other methods
212
+ summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) }
213
+ MultiResult.new(summary_boolean, command_return_values)
214
+ end
215
+
216
+ # Updates the object by applying multiple field values.
217
+ #
218
+ # Sets multiple attributes on the object instance using their corresponding
219
+ # setter methods. Only fields that have defined setter methods will be updated.
220
+ #
221
+ # @param fields [Hash] Hash of field names (as keys) and their values to apply
222
+ # to the object instance.
223
+ #
224
+ # @return [self] Returns the updated object instance for method chaining.
225
+ #
226
+ # @example Update multiple fields on an object
227
+ # user.apply_fields(name: "John", email: "john@example.com", age: 30)
228
+ # # => #<User:0x007f8a1c8b0a28 @name="John", @email="john@example.com", @age=30>
229
+ #
230
+ def apply_fields(**fields)
231
+ fields.each do |field, value|
232
+ # Apply the field value if the setter method exists
233
+ send("#{field}=", value) if respond_to?("#{field}=")
234
+ end
235
+ self
236
+ end
237
+
238
+ # Permanently removes this object from the DB storage.
239
+ #
240
+ # Deletes the object's Valkey key and all associated data. This operation
241
+ # is irreversible and will permanently destroy all stored information
242
+ # for this object instance.
243
+ #
244
+ # @return [void]
245
+ #
246
+ # @example Remove a user object from storage
247
+ # user = User.new(id: 123)
248
+ # user.destroy!
249
+ # # Object is now permanently removed from the DB
250
+ #
251
+ # @note This method provides high-level object lifecycle management.
252
+ # It operates at the object level for ORM-style operations, while
253
+ # `delete!` operates directly on database keys. Use `destroy!` when
254
+ # removing complete objects from the system.
255
+ #
256
+ # @note When debugging is enabled, this method will trace the deletion
257
+ # operation for diagnostic purposes.
258
+ #
259
+ # @see #delete! The underlying method that performs the key deletion
260
+ #
261
+ def destroy!
262
+ Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug?
263
+ delete!
264
+ end
265
+
266
+ # Clears all fields by setting them to nil.
267
+ #
268
+ # Resets all object fields to nil values, effectively clearing the object's
269
+ # state. This operation affects all fields defined on the object's class,
270
+ # setting each one to nil through their corresponding setter methods.
271
+ #
272
+ # @return [void]
273
+ #
274
+ # @example Clear all fields on an object
275
+ # user.name = "John"
276
+ # user.email = "john@example.com"
277
+ # user.clear_fields!
278
+ # # => user.name and user.email are now nil
279
+ #
280
+ # @note This operation does not persist the changes to the DB. Call save
281
+ # after clear_fields! if you want to persist the cleared state.
282
+ #
283
+ def clear_fields!
284
+ self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
285
+ end
286
+
287
+ # Refreshes the object state from the DB storage.
288
+ #
289
+ # Reloads all persistent field values from the DB, overwriting any unsaved
290
+ # changes in the current object instance. This operation synchronizes the
291
+ # object with its stored state in the database.
292
+ #
293
+ # @return [void]
294
+ #
295
+ # @raise [Familia::KeyNotFoundError] If the Valkey key does not exist
296
+ #
297
+ # @example Refresh object from the DB
298
+ # user.name = "Changed Name" # unsaved change
299
+ # user.refresh!
300
+ # # => user.name is now the value from the DB storage
301
+ #
302
+ # @note This method discards any unsaved changes to the object. Use with
303
+ # caution when the object has been modified but not yet persisted.
304
+ #
305
+ # @note Transient fields are reset to nil during refresh since they have
306
+ # no authoritative source in Valkey storage.
307
+ #
308
+ def refresh!
309
+ Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
310
+ raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
311
+
312
+ fields = hgetall
313
+ Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}"
314
+
315
+ # Reset transient fields to nil for semantic clarity and ORM consistency
316
+ # Transient fields have no authoritative source, so they should return to
317
+ # their uninitialized state during refresh operations
318
+ reset_transient_fields!
319
+
320
+ optimistic_refresh(**fields)
321
+ end
322
+
323
+ # Refreshes object state from the DB and returns self for method chaining.
324
+ #
325
+ # Loads the current state of the object from the DB storage, updating all
326
+ # field values to match their persisted state. This method provides a
327
+ # chainable interface to the refresh! operation.
328
+ #
329
+ # @return [self] The refreshed object instance, enabling method chaining
330
+ #
331
+ # @raise [Familia::KeyNotFoundError] If the Valkey key does not exist
332
+ #
333
+ # @example Refresh and chain operations
334
+ # user.refresh.save
335
+ # user.refresh.apply_fields(status: 'active')
336
+ #
337
+ # @see #refresh! The underlying refresh operation
338
+ #
339
+ def refresh
340
+ refresh!
341
+ self
342
+ end
343
+
344
+ # Converts the object's persistent fields to a hash for external use.
345
+ #
346
+ # Serializes persistent field values for external consumption (APIs, logs),
347
+ # excluding non-loggable fields like encrypted fields for security.
348
+ # Only non-nil values are included in the resulting hash.
349
+ #
350
+ # @return [Hash] Hash with field names as keys and serialized values
351
+ # safe for external exposure
352
+ #
353
+ # @example Converting an object to hash format for API response
354
+ # user = User.new(name: "John", email: "john@example.com", age: 30)
355
+ # user.to_h
356
+ # # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
357
+ # # encrypted fields are excluded for security
358
+ #
359
+ # @note Only loggable fields are included for security
360
+ # @note Only fields with non-nil values are included
361
+ #
362
+ def to_h
363
+ self.class.persistent_fields.each_with_object({}) do |field, hsh|
364
+ field_type = self.class.field_types[field]
365
+
366
+ # Security: Skip non-loggable fields (e.g., encrypted fields)
367
+ next unless field_type.loggable
368
+
369
+ method_name = field_type.method_name
370
+ val = send(method_name)
371
+ prepared = serialize_value(val)
372
+ Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
373
+
374
+ # Only include non-nil values in the hash for Valkey
375
+ # Use string key for database compatibility
376
+ hsh[field.to_s] = prepared unless prepared.nil?
377
+ end
378
+ end
379
+
380
+ # Converts the object's persistent fields to a hash for database storage.
381
+ #
382
+ # Serializes ALL persistent field values for database storage, including
383
+ # encrypted fields. This is used internally by commit_fields and other
384
+ # persistence operations.
385
+ #
386
+ # @return [Hash] Hash with field names as keys and serialized values
387
+ # ready for database storage
388
+ #
389
+ # @note Includes ALL persistent fields, including encrypted fields
390
+ # @note Only fields with non-nil values are included for storage efficiency
391
+ #
392
+ def to_h_for_storage
393
+ self.class.persistent_fields.each_with_object({}) do |field, hsh|
394
+ field_type = self.class.field_types[field]
395
+ method_name = field_type.method_name
396
+ val = send(method_name)
397
+ prepared = serialize_value(val)
398
+ Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
399
+
400
+ # Only include non-nil values in the hash for Valkey
401
+ # Use string key for database compatibility
402
+ hsh[field.to_s] = prepared unless prepared.nil?
403
+ end
404
+ end
405
+
406
+ # Converts the object's persistent fields to an array.
407
+ #
408
+ # Serializes all persistent field values in field definition order,
409
+ # preparing them for Valkey storage. Each value is processed through
410
+ # the serialization pipeline to ensure Valkey compatibility.
411
+ #
412
+ # @return [Array] Array of serialized field values in field order
413
+ #
414
+ # @example Converting an object to array format
415
+ # user = User.new(name: "John", email: "john@example.com", age: 30)
416
+ # user.to_a
417
+ # # => ["John", "john@example.com", "30"]
418
+ #
419
+ # @note Values are serialized using the same process as other persistence
420
+ # methods to maintain data consistency across operations.
421
+ #
422
+ def to_a
423
+ self.class.persistent_fields.filter_map do |field|
424
+ field_type = self.class.field_types[field]
425
+
426
+ # Security: Skip non-loggable fields (e.g., encrypted fields)
427
+ next unless field_type.loggable
428
+
429
+ method_name = field_type.method_name
430
+ val = send(method_name)
431
+ prepared = serialize_value(val)
432
+ Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
433
+ prepared
434
+ end
435
+ end
436
+
437
+ # Serializes a Ruby object for Valkey storage.
438
+ #
439
+ # Converts Ruby objects into the DB-compatible string representations using
440
+ # the Familia distinguisher for type coercion. Falls back to JSON serialization
441
+ # for complex types (Hash, Array) when the primary distinguisher returns nil.
442
+ #
443
+ # The serialization process:
444
+ # 1. Attempts conversion using Familia.distinguisher with relaxed type checking
445
+ # 2. For Hash/Array types that return nil, tries custom dump_method or JSON.dump
446
+ # 3. Logs warnings when serialization fails completely
447
+ #
448
+ # @param val [Object] The Ruby object to serialize for Valkey storage
449
+ #
450
+ # @return [String, nil] The serialized value ready for Valkey storage, or nil
451
+ # if serialization failed
452
+ #
453
+ # @example Serializing different data types
454
+ # serialize_value("hello") # => "hello"
455
+ # serialize_value(42) # => "42"
456
+ # serialize_value({name: "John"}) # => '{"name":"John"}'
457
+ # serialize_value([1, 2, 3]) # => "[1,2,3]"
458
+ #
459
+ # @note This method integrates with Familia's type system and supports
460
+ # custom serialization methods when available on the object
461
+ #
462
+ # @see Familia.distinguisher The primary serialization mechanism
463
+ #
464
+ def serialize_value(val)
465
+ # Security: Handle ConcealedString safely - extract encrypted data for storage
466
+ if val.respond_to?(:encrypted_value)
467
+ return val.encrypted_value
468
+ end
469
+
470
+ prepared = Familia.distinguisher(val, strict_values: false)
471
+
472
+ # If the distinguisher returns nil, try using the dump_method but only
473
+ # use JSON serialization for complex types that need it.
474
+ if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
475
+ prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val)
476
+ end
477
+
478
+ # If both the distinguisher and dump_method return nil, log an error
479
+ Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
480
+
481
+ prepared
482
+ end
483
+
484
+ # Converts a Database string value back to its original Ruby type
485
+ #
486
+ # This method attempts to deserialize JSON strings back to their original
487
+ # Hash or Array types. Simple string values are returned as-is.
488
+ #
489
+ # @param val [String] The string value from Database to deserialize
490
+ # @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
491
+ # @return [Object] The deserialized value (Hash, Array, or original string)
492
+ #
493
+ def deserialize_value(val, symbolize: true)
494
+ return val if val.nil? || val == ''
495
+
496
+ # Try to parse as JSON first for complex types
497
+ begin
498
+ parsed = JSON.parse(val, symbolize_names: symbolize)
499
+ # Only return parsed value if it's a complex type (Hash/Array)
500
+ # Simple values should remain as strings
501
+ return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
502
+ rescue JSON::ParserError
503
+ # Not valid JSON, return as-is
504
+ end
505
+
506
+ val
507
+ end
508
+
509
+ private
510
+
511
+ # Reset all transient fields to nil
512
+ #
513
+ # This method ensures that transient fields return to their uninitialized
514
+ # state during refresh operations. This provides semantic clarity (refresh
515
+ # means "reload from authoritative source"), ORM consistency with other
516
+ # frameworks, and prevents stale transient data accumulation.
517
+ #
518
+ # @return [void]
519
+ #
520
+ def reset_transient_fields!
521
+ return unless self.class.respond_to?(:transient_fields)
522
+
523
+ self.class.transient_fields.each do |field_name|
524
+ field_type = self.class.field_types[field_name]
525
+ next unless field_type&.method_name
526
+
527
+ # Set the transient field back to nil
528
+ send("#{field_type.method_name}=", nil)
529
+ Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
530
+ end
531
+ end
532
+ end
533
+
534
+ end
535
+ end
@@ -7,17 +7,16 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
-
11
10
  # Utils - Module containing utility methods for Familia::Horreum (InstanceMethods)
12
11
  #
13
12
  module Utils
14
-
15
- def uri(suffix = nil)
16
- u = Familia.uri(self.class.uri) # returns URI::Redis
17
- u.logical_database = logical_database if logical_database # override the logical_database if we have one
18
- u.key = dbkey(suffix)
19
- u
20
- end
13
+ # def uri
14
+ # base_uri = self.class.uri || Familia.uri
15
+ # u = base_uri.dup # make a copy to modify safely
16
+ # u.logical_database = logical_database if logical_database
17
+ # u.key = dbkey
18
+ # u
19
+ # end
21
20
 
22
21
  # +suffix+ is the value to be used at the end of the db key
23
22
  # (e.g. `customer:customer_id:scores` would have `scores` as the suffix
@@ -27,8 +26,9 @@ module Familia
27
26
  # Whether this is a Horreum or DataType object, the value is taken
28
27
  # from the `identifier` method).
29
28
  #
30
- def dbkey(suffix = nil, ignored = nil)
29
+ def dbkey(suffix = nil, _ignored = nil)
31
30
  raise Familia::NoIdentifier, "No identifier for #{self.class}" if identifier.to_s.empty?
31
+
32
32
  suffix ||= self.suffix # use the instance method to get the default suffix
33
33
  self.class.dbkey identifier, suffix
34
34
  end
@@ -36,9 +36,6 @@ module Familia
36
36
  def join(*args)
37
37
  Familia.join(args.map { |field| send(field) })
38
38
  end
39
-
40
39
  end
41
-
42
- include Utils # these become Horreum instance methods
43
40
  end
44
41
  end
@@ -0,0 +1,21 @@
1
+ # lib/familia/horreum/core.rb
2
+
3
+ require_relative 'core/database_commands'
4
+ require_relative 'core/serialization'
5
+ require_relative 'core/connection'
6
+ require_relative 'core/utils'
7
+
8
+ module Familia
9
+ class Horreum
10
+ module Core
11
+ include Familia::Horreum::DatabaseCommands
12
+ include Familia::Horreum::Serialization
13
+ # include for instance methods after it's loaded. Note that Horreum::Utils
14
+ # are also included and at one time also has a uri method. This connection
15
+ # module is also extended for the class level methods. It will require some
16
+ # disambiguation at some point.
17
+ include Familia::Horreum::Connection
18
+ include Familia::Horreum::Utils
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # lib/familia/horreum/settings.rb
2
- #
2
+
3
3
  module Familia
4
4
  # InstanceMethods - Module containing instance-level methods for Familia
5
5
  #
@@ -7,7 +7,6 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
-
11
10
  # Settings - Module containing settings for Familia::Horreum (InstanceMethods)
12
11
  #
13
12
  module Settings
@@ -26,6 +25,15 @@ module Familia
26
25
  @logical_database || self.class.logical_database
27
26
  end
28
27
 
28
+ # Retrieves the prefix for the current instance by delegating to its class.
29
+ #
30
+ # @return [String] The prefix associated with the class of the current instance.
31
+ # @example
32
+ # instance.prefix
33
+ def prefix
34
+ self.class.prefix
35
+ end
36
+
29
37
  def suffix
30
38
  @suffix || self.class.suffix
31
39
  end
@@ -38,7 +46,5 @@ module Familia
38
46
  @load_method || self.class.load_method
39
47
  end
40
48
  end
41
-
42
- include Settings # these become Horreum instance methods
43
49
  end
44
50
  end