Module: Familia::Horreum::Serialization

Included in:
Familia::Horreum
Defined in:
lib/familia/horreum/serialization.rb

Overview

Serialization: Where Objects Go to Become Strings (and Vice Versa)!

This module is chock-full of methods that’ll make your head spin (in a good way)! We’ve got loaders, dumpers, and refreshers galore. It’s like a laundromat for your data, but instead of quarters, it runs on Database commands.

A Note on Our Refreshing Refreshers: In the wild world of Ruby, ‘!’ usually means “Watch out! I’m dangerous!” But here in Familia-land, we march to the beat of a different drummer. Our refresh! method is the real deal, doing all the heavy lifting. The non-bang refresh? Oh, it’s just as rowdy, but it plays nice with method chaining. It’s like the polite twin who still knows how to party.

Remember: In Familia, refreshing isn’t just a chore, it’s a chance to dance with data! Whether you bang(!) or not, you’re still invited to the Database disco.

(P.S. If you’re reading these docs, lol sorry. I asked Claude 3.5 to write in the style of _why the lucky stiff today and got this uncanny valley response. I hope you enjoy reading it as much as I did writing the prompt for it. - @delano).

(Ahem! What I meant to say was that if you’re reading this, congratulations! You’ve stumbled upon the secret garden of documentation. Feel free to smell the Ruby roses, but watch out for the Database thorns!)

Instance Method Summary collapse

Instance Method Details

#apply_fields(**fields) ⇒ self

Apply a smattering of fields to this object like fairy dust.

Examples:

Giving your object a makeover

dragon.apply_fields(name: "Puff", breathes: "fire", loves: "Toys
named Jackie")
# => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
@loves="Toys named Jackie">

Parameters:

  • fields (Hash)

    A magical bag of named attributes to sprinkle onto this instance. Each key-value pair is like a tiny spell, ready to enchant our object’s properties.

Returns:

  • (self)

    Returns the newly bejeweled instance, now sparkling with fresh attributes.



143
144
145
146
147
148
149
# File 'lib/familia/horreum/serialization.rb', line 143

def apply_fields(**fields)
  fields.each do |field, value|
    # Whisper the new value into the object's ear (if it's listening)
    send("#{field}=", value) if respond_to?("#{field}=")
  end
  self
end

#batch_update(**kwargs) ⇒ MultiResult

Updates multiple fields atomically in a Database transaction.

Examples:

Update multiple fields without affecting expiration

.batch_update(viewed: 1, updated: Time.now.to_i, update_expiration: false)

Update fields with expiration refresh

user.batch_update(name: "John", email: "john@example.com")

Parameters:

  • fields (Hash)

    Field names and values to update. Special key :update_expiration controls whether to update key expiration (default: true)

Returns:



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/familia/horreum/serialization.rb', line 105

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 method returns an undefined value.

The Great Nilpocalypse: clear_fields!

Imagine your object as a grand old mansion, every room stuffed with trinkets, secrets, and the odd rubber duck. This method? It flings open every window and lets a wild wind of nothingness sweep through, leaving each field as empty as a poet’s wallet.

All your precious attributes—gone! Swept into the void! It’s a spring cleaning for the soul, a reset button for your existential dread.

Examples:

The Vanishing Act

wizard.clear_fields!
# => All fields are now nil, like a spell gone slightly too well.


257
258
259
# File 'lib/familia/horreum/serialization.rb', line 257

def clear_fields!
  self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
end

#commit_fields(update_expiration: true) ⇒ MultiResult

Note:

Be warned, young programmer! This method dabbles in the arcane art of transactions. Side effects may include data persistence and a slight tingling sensation. The method does not raise exceptions for unexpected Database responses, but logs warnings and returns a failure status.

Note:

This method performs logging at various levels: - Debug: Logs the object’s class, dbkey, and current state before committing - Warn: Logs any unexpected return values from Database commands - Debug: Logs the final result, including success status and all return values

Note:

The expiration update is only performed for classes that have the expiration feature enabled. For others, it’s a no-op.

Commit our precious fields to Redis.

This method performs a sacred ritual, sending our cherished attributes on a journey through the ethernet to find their resting place in Redis. It executes a transaction that includes setting field values and, if applicable, updating the expiration time.

The MultiResult object responds to: - successful?: Returns the boolean success value - results: Returns the array of command return values

Examples:

Offering your changes to the Database deities

unicorn.name = "Charlie"
unicorn.horn_length = "magnificent"
result = unicorn.commit_fields
if result.successful?
  puts "The Database gods are pleased with your offering"
  p result.results  # => ["OK", "OK"]
else
  puts "The Database gods frown upon your offering"
  p result.results  # Examine the unexpected values
end

Parameters:

  • update_expiration (Boolean) (defaults to: true)

    Whether to update the expiration time of the dbkey. This is true by default, but can be disabled if you don’t want to mess with the cosmic balance of your key’s lifespan.

Returns:

  • (MultiResult)

    A mystical object containing: - success: A boolean indicating if all Database commands succeeded - results: An array of strings, cryptic messages from the Database gods

See Also:



198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/familia/horreum/serialization.rb', line 198

def commit_fields(update_expiration: true)
  prepared_value = to_h
  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.
  self.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.

Parameters:

  • val (String)

    The string value from Database to deserialize

  • symbolize_keys (Boolean)

    Whether to symbolize hash keys (default: true for compatibility)

Returns:

  • (Object)

    The deserialized value (Hash, Array, or original string)



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/familia/horreum/serialization.rb', line 430

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

Note:

If debugging is enabled, this method will leave a trace of its destructive path, like breadcrumbs for future data archaeologists.

This method returns an undefined value.

Dramatically vanquish this object from the face of Redis! (ed: delete it)

This method is the doomsday device of our little data world. It will mercilessly eradicate all traces of our object from Redis, leaving naught but digital dust in its wake. Use with caution, lest you accidentally destroy the wrong data-verse!

This method is part of Familia’s high-level object lifecycle management. While delete! operates directly on dbkeys, destroy! operates at the object level and is used for ORM-style operations. Use destroy! when removing complete objects from the system, and delete! when working directly with dbkeys.

Examples:

Bidding a fond farewell to your pet rock

rocky = PetRock.new(name: "Dwayne")
rocky.destroy!
# => *poof* Rocky is no more. A moment of silence, please.

See Also:



236
237
238
239
# File 'lib/familia/horreum/serialization.rb', line 236

def destroy!
  Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug?
  delete!
end

#refreshself

Note:

Caution, young Rubyist! While this method loves to play chain-tag with other methods, it’s still got that refresh! kick. It’ll update your object faster than you can say “matz!”

Ah, the magical refresh dance! It’s like giving your object a sip from the fountain of youth.

This method twirls your object around, dips it into the Database pool, and brings it back sparkling clean and up-to-date. It’s using the refresh! spell behind the scenes, so expect some Database whispering.

Returns:

  • (self)

    Your object, freshly bathed in Database waters, ready to dance with more methods in a conga line of Ruby joy!

Raises:



312
313
314
315
# File 'lib/familia/horreum/serialization.rb', line 312

def refresh
  refresh!
  self
end

#refresh!void

This method returns an undefined value.

The Great Database Refresh-o-matic 3000

Imagine your object as a forgetful time traveler. This method is like zapping it with a memory ray from Redis-topia. ZAP! New memories!

WARNING: This is not a gentle mind-meld. It’s more like a full brain transplant. Any half-baked ideas floating in your object’s head? POOF! Gone quicker than cake at a hobbit’s birthday party. Unsaved spells will definitely be forgotten.

list of all the brain bits that got a makeover!

Remember: In the game of Redis-Refresh, you win or you… well, you always win, but sometimes you forget why you played in the first place.

Examples:

object.refresh!

Raises:



281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/familia/horreum/serialization.rb', line 281

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

Note:

This method will leave breadcrumbs (traces) if you’re in debug mode. It’s like Hansel and Gretel, but for data operations!

Save our precious data to Redis, with a sprinkle of timestamp magic!

This method is like a conscientious historian, not only recording your object’s current state but also meticulously timestamping when it was created and last updated. It’s the record keeper of your data’s life story!

Examples:

Preserving your pet rock for posterity

rocky = PetRock.new(name: "Dwayne")
rocky.save
# => true (Dwayne is now immortalized in Redis)

Returns:

  • (Boolean)

    true if the save was successful, false if Database was grumpy.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/familia/horreum/serialization.rb', line 76

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

#serialize_value(val) ⇒ String

Behold, the grand tale of two serialization sorcerers: Familia::DataType and Familia::Horreum!

These twin wizards, though cut from the same magical cloth, have their own unique spells for turning Ruby objects into Redis-friendly potions. Let’s peek into their spell books:

Shared Incantations: - Both transform various data creatures for Database safekeeping - They tame wild Strings, Symbols, and those slippery Numerics - Secret rituals (aka custom serialization) are welcome

Mystical Differences: - DataType reads the future in opts[:class] tea leaves - Horreum prefers to interrogate types more thoroughly - DataType leaves a trail of debug breadcrumbs

But wait! Enter the wise Familia.distinguisher, a grand unifier of serialization magic!

This clever mediator: 1. Juggles a circus of data types from both realms 2. Offers a ‘strict_values’ toggle for the type-obsessed 3. Welcomes custom spells via dump_method 4. Sprinkles debug fairy dust à la DataType

By channeling the Familia.distinguisher, we’ve created a harmonious serialization symphony, flexible enough to dance with any data type that shimmies our way. And should we need to teach it new tricks, we know just where to wave our wands!

Parameters:

  • value (Object)

    The mystical object to be transformed

Returns:

  • (String)

    The transformed, Redis-ready value.



406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/familia/horreum/serialization.rb', line 406

def serialize_value(val)
  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_aArray

Note:

Each value is carefully disguised in its Database costume

Line up all our attributes in a neat little array parade!

This method marshals all our object’s attributes into an orderly procession, ready to march into Database in perfect formation. It’s like a little data army, but friendlier and less prone to conquering neighboring databases.

before joining the parade.

Examples:

Arranging your unicorn’s attributes in a line

unicorn.to_a
# => ["Charlie", "magnificent", 5]

Returns:

  • (Array)

    A splendid array of Redis-ready values, in the order of our fields.



360
361
362
363
364
365
366
367
368
369
# File 'lib/familia/horreum/serialization.rb', line 360

def to_a
  self.class.persistent_fields.collect do |field|
    field_type = self.class.field_types[field]
    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_hHash

Note:

Watch in awe as each field is lovingly prepared for its Database adventure!

Transform this object into a magical hash of wonders!

This method performs an alchemical transmutation, turning our noble object into a more plebeian hash. But fear not, for in this form, it can slip through the cracks of the universe (or at least, into Redis) with ease.

Examples:

Turning your dragon into a hash

dragon.to_h
# => {"name"=>"Puff", "breathes"=>"fire", "age"=>1000}

Returns:

  • (Hash)

    A glittering hash, each key a field name, each value a Redis-ready treasure.



332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/familia/horreum/serialization.rb', line 332

def to_h
  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] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"

    # Only include non-nil values in the hash for Redis
    hsh[field] = prepared unless prepared.nil?
  end
end