Module: Familia::Horreum::Serialization
- Included in:
- Core
- Defined in:
- lib/familia/horreum/core/serialization.rb
Overview
Serialization: Object persistence and retrieval from the DB Handles conversion between Ruby objects and Valkey hash storage
Instance Method Summary collapse
-
#apply_fields(**fields) ⇒ self
Updates the object by applying multiple field values.
-
#batch_update(**kwargs) ⇒ MultiResult
Updates multiple fields atomically in a Database transaction.
-
#clear_fields! ⇒ void
Clears all fields by setting them to nil.
-
#commit_fields(update_expiration: true) ⇒ Object
Commits object fields to the DB storage.
-
#deserialize_value(val, symbolize: true) ⇒ Object
Converts a Database string value back to its original Ruby type.
-
#destroy! ⇒ void
Permanently removes this object from the DB storage.
-
#refresh ⇒ self
Refreshes object state from the DB and returns self for method chaining.
-
#refresh! ⇒ void
Refreshes the object state from the DB storage.
-
#save(update_expiration: true) ⇒ Boolean
Persists the object to Valkey storage with automatic timestamping.
-
#save_if_not_exists(update_expiration: true) ⇒ Boolean
Saves the object to Valkey storage only if it doesn’t already exist.
-
#serialize_value(val) ⇒ String?
Serializes a Ruby object for Valkey storage.
-
#to_a ⇒ Array
Converts the object’s persistent fields to an array.
-
#to_h ⇒ Hash
Converts the object’s persistent fields to a hash for external use.
-
#to_h_for_storage ⇒ Hash
Converts the object’s persistent fields to a hash for database storage.
Instance Method Details
#apply_fields(**fields) ⇒ self
Updates the object by applying multiple field values.
Sets multiple attributes on the object instance using their corresponding setter methods. Only fields that have defined setter methods will be updated.
230 231 232 233 234 235 236 |
# File 'lib/familia/horreum/core/serialization.rb', line 230 def apply_fields(**fields) fields.each do |field, value| # Apply the field value if the setter method exists send("#{field}=", value) if respond_to?("#{field}=") end self end |
#batch_update(**kwargs) ⇒ MultiResult
Updates multiple fields atomically in a Database transaction.
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/familia/horreum/core/serialization.rb', line 193 def batch_update(**kwargs) update_expiration = kwargs.delete(:update_expiration) { true } fields = kwargs Familia.trace :BATCH_UPDATE, dbclient, fields.keys, caller(1..1) if Familia.debug? command_return_values = transaction do |conn| fields.each do |field, value| prepared_value = serialize_value(value) conn.hset dbkey, field, prepared_value # Update instance variable to keep object in sync send("#{field}=", value) if respond_to?("#{field}=") end end # Update expiration if requested and supported self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration) # Return same MultiResult format as other methods summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) } MultiResult.new(summary_boolean, command_return_values) end |
#clear_fields! ⇒ void
This operation does not persist the changes to the DB. Call save after clear_fields! if you want to persist the cleared state.
This method returns an undefined value.
Clears all fields by setting them to nil.
Resets all object fields to nil values, effectively clearing the object’s state. This operation affects all fields defined on the object’s class, setting each one to nil through their corresponding setter methods.
283 284 285 |
# File 'lib/familia/horreum/core/serialization.rb', line 283 def clear_fields! self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) } end |
#commit_fields(update_expiration: true) ⇒ Object
The expiration update is only performed for classes that have the expiration feature enabled. For others, it’s a no-op.
This method performs debug logging of the object’s class, dbkey, and current state before committing to the DB.
Commits object fields to the DB storage.
Persists the current state of all object fields to the DB using HMSET. Optionally updates the key’s expiration time if the feature is enabled for the object’s class.
167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/familia/horreum/core/serialization.rb', line 167 def commit_fields(update_expiration: true) prepared_value = to_h_for_storage Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})" result = hmset(prepared_value) # Only classes that have the expiration ferature enabled will # actually set an expiration time on their keys. Otherwise # this will be a no-op that simply logs the attempt. update_expiration(default_expiration: nil) if update_expiration result end |
#deserialize_value(val, symbolize: true) ⇒ Object
Converts a Database string value back to its original Ruby type
This method attempts to deserialize JSON strings back to their original Hash or Array types. Simple string values are returned as-is.
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 |
# File 'lib/familia/horreum/core/serialization.rb', line 493 def deserialize_value(val, symbolize: true) return val if val.nil? || val == '' # Try to parse as JSON first for complex types begin parsed = JSON.parse(val, symbolize_names: symbolize) # Only return parsed value if it's a complex type (Hash/Array) # Simple values should remain as strings return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array) rescue JSON::ParserError # Not valid JSON, return as-is end val end |
#destroy! ⇒ void
This method provides high-level object lifecycle management.
It operates at the object level for ORM-style operations, while
delete!
operates directly on database keys. Use destroy!
when
removing complete objects from the system.
When debugging is enabled, this method will trace the deletion operation for diagnostic purposes.
This method returns an undefined value.
Permanently removes this object from the DB storage.
Deletes the object’s Valkey key and all associated data. This operation is irreversible and will permanently destroy all stored information for this object instance.
261 262 263 264 |
# File 'lib/familia/horreum/core/serialization.rb', line 261 def destroy! Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug? delete! end |
#refresh ⇒ self
Refreshes object state from the DB and returns self for method chaining.
Loads the current state of the object from the DB storage, updating all field values to match their persisted state. This method provides a chainable interface to the refresh! operation.
339 340 341 342 |
# File 'lib/familia/horreum/core/serialization.rb', line 339 def refresh refresh! self end |
#refresh! ⇒ void
This method discards any unsaved changes to the object. Use with caution when the object has been modified but not yet persisted.
Transient fields are reset to nil during refresh since they have no authoritative source in Valkey storage.
This method returns an undefined value.
Refreshes the object state from the DB storage.
Reloads all persistent field values from the DB, overwriting any unsaved changes in the current object instance. This operation synchronizes the object with its stored state in the database.
308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/familia/horreum/core/serialization.rb', line 308 def refresh! Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug? raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey) fields = hgetall Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}" # Reset transient fields to nil for semantic clarity and ORM consistency # Transient fields have no authoritative source, so they should return to # their uninitialized state during refresh operations reset_transient_fields! optimistic_refresh(**fields) end |
#save(update_expiration: true) ⇒ Boolean
When Familia.debug? is enabled, this method will trace the save operation for debugging purposes.
Persists the object to Valkey storage with automatic timestamping.
Saves the current object state to Valkey storage, automatically setting created and updated timestamps if the object supports them. The method commits all persistent fields and optionally updates the key’s expiration.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/familia/horreum/core/serialization.rb', line 63 def save(update_expiration: true) Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug? # No longer need to sync computed identifier with a cache field self.created ||= Familia.now.to_i if respond_to?(:created) self.updated = Familia.now.to_i if respond_to?(:updated) # Commit our tale to the Database chronicles # ret = commit_fields(update_expiration: update_expiration) Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})" # Did Database accept our offering? !ret.nil? end |
#save_if_not_exists(update_expiration: true) ⇒ Boolean
This method uses HSETNX to atomically check and set the identifier field, ensuring race-condition-free conditional creation.
Saves the object to Valkey storage only if it doesn’t already exist.
Conditionally persists the object to Valkey storage by first checking if the identifier field already exists. If the object already exists in storage, raises an error. Otherwise, proceeds with a normal save operation including automatic timestamping.
This method provides atomic conditional creation to prevent duplicate objects from being saved when uniqueness is required based on the identifier field.
Check if save_if_not_exists is implemented correctly. It should:
Check if record exists If exists, raise Familia::RecordExistsError If not exists, save
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/familia/horreum/core/serialization.rb', line 122 def save_if_not_exists(update_expiration: true) identifier_field = self.class.identifier_field Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}" Familia.trace :SAVE_IF_NOT_EXISTS, dbclient, uri, caller(1..1) if Familia.debug? dbclient.watch(dbkey) do if dbclient.exists(dbkey).positive? dbclient.unwatch raise Familia::RecordExistsError, dbkey end result = dbclient.multi do |multi| multi.hmset(dbkey, to_h_for_storage) end result.is_a?(Array) # transaction succeeded end end |
#serialize_value(val) ⇒ String?
This method integrates with Familia’s type system and supports custom serialization methods when available on the object
Serializes a Ruby object for Valkey storage.
Converts Ruby objects into the DB-compatible string representations using the Familia distinguisher for type coercion. Falls back to JSON serialization for complex types (Hash, Array) when the primary distinguisher returns nil.
The serialization process: 1. Attempts conversion using Familia.distinguisher with relaxed type checking 2. For Hash/Array types that return nil, tries custom dump_method or JSON.dump 3. Logs warnings when serialization fails completely
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 |
# File 'lib/familia/horreum/core/serialization.rb', line 464 def serialize_value(val) # Security: Handle ConcealedString safely - extract encrypted data for storage if val.respond_to?(:encrypted_value) return val.encrypted_value end prepared = Familia.distinguisher(val, strict_values: false) # If the distinguisher returns nil, try using the dump_method but only # use JSON serialization for complex types that need it. if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array)) prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val) end # If both the distinguisher and dump_method return nil, log an error Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil? prepared end |
#to_a ⇒ Array
Values are serialized using the same process as other persistence methods to maintain data consistency across operations.
Converts the object’s persistent fields to an array.
Serializes all persistent field values in field definition order, preparing them for Valkey storage. Each value is processed through the serialization pipeline to ensure Valkey compatibility.
422 423 424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'lib/familia/horreum/core/serialization.rb', line 422 def to_a self.class.persistent_fields.filter_map do |field| field_type = self.class.field_types[field] # Security: Skip non-loggable fields (e.g., encrypted fields) next unless field_type.loggable method_name = field_type.method_name val = send(method_name) prepared = serialize_value(val) Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}" prepared end end |
#to_h ⇒ Hash
Only loggable fields are included for security
Only fields with non-nil values are included
Converts the object’s persistent fields to a hash for external use.
Serializes persistent field values for external consumption (APIs, logs), excluding non-loggable fields like encrypted fields for security. Only non-nil values are included in the resulting hash.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/familia/horreum/core/serialization.rb', line 362 def to_h self.class.persistent_fields.each_with_object({}) do |field, hsh| field_type = self.class.field_types[field] # Security: Skip non-loggable fields (e.g., encrypted fields) next unless field_type.loggable method_name = field_type.method_name val = send(method_name) prepared = serialize_value(val) Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}" # Only include non-nil values in the hash for Valkey # Use string key for database compatibility hsh[field.to_s] = prepared unless prepared.nil? end end |
#to_h_for_storage ⇒ Hash
Includes ALL persistent fields, including encrypted fields
Only fields with non-nil values are included for storage efficiency
Converts the object’s persistent fields to a hash for database storage.
Serializes ALL persistent field values for database storage, including encrypted fields. This is used internally by commit_fields and other persistence operations.
392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/familia/horreum/core/serialization.rb', line 392 def to_h_for_storage self.class.persistent_fields.each_with_object({}) do |field, hsh| field_type = self.class.field_types[field] method_name = field_type.method_name val = send(method_name) prepared = serialize_value(val) Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}" # Only include non-nil values in the hash for Valkey # Use string key for database compatibility hsh[field.to_s] = prepared unless prepared.nil? end end |