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
@@ -1,445 +0,0 @@
1
- # lib/familia/horreum/serialization.rb
2
- #
3
- module Familia
4
-
5
-
6
- # Familia::Horreum
7
- #
8
- class Horreum
9
- # The Sacred Scrolls of Database Responses
10
- #
11
- # Behold! The mystical runes that Database whispers back to us:
12
- #
13
- # "OK" - The sweet sound of success, like a tiny "ding!" from the depths of data.
14
- # true - The boolean Buddha nods in agreement.
15
- # 1 - A lonely digit, standing tall and proud. "I did something!" it proclaims.
16
- # 0 - The silent hero. It tried its best, bless its heart.
17
- # nil - The zen master of responses. It's not nothing, it's... enlightenment!
18
- #
19
- # These sacred signs are our guide through the Database wilderness. When we cast
20
- # our spells (er, commands), we seek these friendly faces in the returned
21
- # smoke signals.
22
- #
23
- # Should our Database rituals summon anything else, we pause. We ponder. We
24
- # possibly panic. For the unexpected in Redis-land is like finding a penguin
25
- # in your pasta - delightfully confusing, but probably not what you ordered.
26
- #
27
- # May your Database returns be ever valid, and your data ever flowing!
28
- #
29
- @valid_command_return_values = ["OK", true, 1, 0, nil].freeze
30
-
31
- class << self
32
- attr_reader :valid_command_return_values
33
- end
34
-
35
- # Serialization: Where Objects Go to Become Strings (and Vice Versa)!
36
- #
37
- # This module is chock-full of methods that'll make your head spin (in a
38
- # good way)! We've got loaders, dumpers, and refreshers galore. It's like
39
- # a laundromat for your data, but instead of quarters, it runs on Database commands.
40
- #
41
- # A Note on Our Refreshing Refreshers:
42
- # In the wild world of Ruby, '!' usually means "Watch out! I'm dangerous!"
43
- # But here in Familia-land, we march to the beat of a different drummer.
44
- # Our refresh! method is the real deal, doing all the heavy lifting.
45
- # The non-bang refresh? Oh, it's just as rowdy, but it plays nice with
46
- # method chaining. It's like the polite twin who still knows how to party.
47
- #
48
- # Remember: In Familia, refreshing isn't just a chore, it's a chance to
49
- # dance with data! Whether you bang(!) or not, you're still invited to
50
- # the Database disco.
51
- #
52
- # (P.S. If you're reading these docs, lol sorry. I asked Claude 3.5 to
53
- # write in the style of _why the lucky stiff today and got this uncanny
54
- # valley response. I hope you enjoy reading it as much as I did writing
55
- # the prompt for it. - @delano).
56
- #
57
- # (Ahem! What I meant to say was that if you're reading this, congratulations!
58
- # You've stumbled upon the secret garden of documentation. Feel free to smell
59
- # the Ruby roses, but watch out for the Database thorns!)
60
- #
61
- module Serialization
62
-
63
- # Save our precious data to Redis, with a sprinkle of timestamp magic!
64
- #
65
- # This method is like a conscientious historian, not only recording your
66
- # object's current state but also meticulously timestamping when it was
67
- # created and last updated. It's the record keeper of your data's life story!
68
- #
69
- # @return [Boolean] true if the save was successful, false if Database was grumpy.
70
- #
71
- # @example Preserving your pet rock for posterity
72
- # rocky = PetRock.new(name: "Dwayne")
73
- # rocky.save
74
- # # => true (Dwayne is now immortalized in Redis)
75
- #
76
- # @note This method will leave breadcrumbs (traces) if you're in debug mode.
77
- # It's like Hansel and Gretel, but for data operations!
78
- #
79
- def save update_expiration: true
80
- Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug?
81
-
82
- # No longer need to sync computed identifier with a cache field
83
- self.created ||= Familia.now.to_i if respond_to?(:created)
84
- self.updated = Familia.now.to_i if respond_to?(:updated)
85
-
86
- # Commit our tale to the Database chronicles
87
- #
88
- ret = commit_fields(update_expiration: update_expiration)
89
-
90
- Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
91
-
92
- # Did Database accept our offering?
93
- !ret.nil?
94
- end
95
-
96
- # Updates multiple fields atomically in a Database transaction.
97
- #
98
- # @param fields [Hash] Field names and values to update. Special key :update_expiration
99
- # controls whether to update key expiration (default: true)
100
- # @return [MultiResult] Transaction result
101
- #
102
- # @example Update multiple fields without affecting expiration
103
- # metadata.batch_update(viewed: 1, updated: Time.now.to_i, update_expiration: false)
104
- #
105
- # @example Update fields with expiration refresh
106
- # user.batch_update(name: "John", email: "john@example.com")
107
- #
108
- def batch_update(**kwargs)
109
- update_expiration = kwargs.delete(:update_expiration) { true }
110
- fields = kwargs
111
-
112
- Familia.trace :BATCH_UPDATE, dbclient, fields.keys, caller(1..1) if Familia.debug?
113
-
114
- command_return_values = transaction do |conn|
115
- fields.each do |field, value|
116
- prepared_value = serialize_value(value)
117
- conn.hset dbkey, field, prepared_value
118
- # Update instance variable to keep object in sync
119
- send("#{field}=", value) if respond_to?("#{field}=")
120
- end
121
- end
122
-
123
- # Update expiration if requested and supported
124
- self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
125
-
126
- # Return same MultiResult format as other methods
127
- summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) }
128
- MultiResult.new(summary_boolean, command_return_values)
129
- end
130
-
131
- # Apply a smattering of fields to this object like fairy dust.
132
- #
133
- # @param fields [Hash] A magical bag of named attributes to sprinkle onto
134
- # this instance. Each key-value pair is like a tiny spell, ready to
135
- # enchant our object's properties.
136
- #
137
- # @return [self] Returns the newly bejeweled instance, now sparkling with
138
- # fresh attributes.
139
- #
140
- # @example Giving your object a makeover
141
- # dragon.apply_fields(name: "Puff", breathes: "fire", loves: "Toys
142
- # named Jackie")
143
- # # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
144
- # @loves="Toys named Jackie">
145
- #
146
- def apply_fields(**fields)
147
- fields.each do |field, value|
148
- # Whisper the new value into the object's ear (if it's listening)
149
- send("#{field}=", value) if respond_to?("#{field}=")
150
- end
151
- self
152
- end
153
-
154
- # Commit our precious fields to Redis.
155
- #
156
- # This method performs a sacred ritual, sending our cherished attributes
157
- # on a journey through the ethernet to find their resting place in Redis.
158
- # It executes a transaction that includes setting field values and,
159
- # if applicable, updating the expiration time.
160
- #
161
- # @param update_expiration [Boolean] Whether to update the expiration time
162
- # of the dbkey. This is true by default, but can be disabled if you
163
- # don't want to mess with the cosmic balance of your key's lifespan.
164
- #
165
- # @return [MultiResult] A mystical object containing:
166
- # - success: A boolean indicating if all Database commands succeeded
167
- # - results: An array of strings, cryptic messages from the Database gods
168
- #
169
- # The MultiResult object responds to:
170
- # - successful?: Returns the boolean success value
171
- # - results: Returns the array of command return values
172
- #
173
- # @note Be warned, young programmer! This method dabbles in the arcane
174
- # art of transactions. Side effects may include data persistence and a
175
- # slight tingling sensation. The method does not raise exceptions for
176
- # unexpected Database responses, but logs warnings and returns a failure status.
177
- #
178
- # @example Offering your changes to the Database deities
179
- # unicorn.name = "Charlie"
180
- # unicorn.horn_length = "magnificent"
181
- # result = unicorn.commit_fields
182
- # if result.successful?
183
- # puts "The Database gods are pleased with your offering"
184
- # p result.results # => ["OK", "OK"]
185
- # else
186
- # puts "The Database gods frown upon your offering"
187
- # p result.results # Examine the unexpected values
188
- # end
189
- #
190
- # @see Familia::Horreum.valid_command_return_values for the list of
191
- # acceptable Database command return values.
192
- #
193
- # @note This method performs logging at various levels:
194
- # - Debug: Logs the object's class, dbkey, and current state before committing
195
- # - Warn: Logs any unexpected return values from Database commands
196
- # - Debug: Logs the final result, including success status and all return values
197
- #
198
- # @note The expiration update is only performed for classes that have
199
- # the expiration feature enabled. For others, it's a no-op.
200
- #
201
- def commit_fields update_expiration: true
202
- prepared_value = to_h
203
- Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
204
-
205
- result = self.hmset(prepared_value)
206
-
207
- # Only classes that have the expiration ferature enabled will
208
- # actually set an expiration time on their keys. Otherwise
209
- # this will be a no-op that simply logs the attempt.
210
- self.update_expiration(default_expiration: nil) if update_expiration
211
-
212
- result
213
- end
214
-
215
- # Dramatically vanquish this object from the face of Redis! (ed: delete it)
216
- #
217
- # This method is the doomsday device of our little data world. It will
218
- # mercilessly eradicate all traces of our object from Redis, leaving naught
219
- # but digital dust in its wake. Use with caution, lest you accidentally
220
- # destroy the wrong data-verse!
221
- #
222
- # @return [void] Returns nothing, for nothing remains after destruction.
223
- #
224
- # @example Bidding a fond farewell to your pet rock
225
- # rocky = PetRock.new(name: "Dwayne")
226
- # rocky.destroy!
227
- # # => *poof* Rocky is no more. A moment of silence, please.
228
- #
229
- # This method is part of Familia's high-level object lifecycle management. While `delete!`
230
- # operates directly on dbkeys, `destroy!` operates at the object level and is used for
231
- # ORM-style operations. Use `destroy!` when removing complete objects from the system, and
232
- # `delete!` when working directly with dbkeys.
233
- #
234
- # @note If debugging is enabled, this method will leave a trace of its
235
- # destructive path, like breadcrumbs for future data archaeologists.
236
- #
237
- # @see #delete! The actual hitman carrying out the deed.
238
- #
239
- def destroy!
240
- Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug?
241
- delete!
242
- end
243
-
244
- # The Great Nilpocalypse: clear_fields!
245
- #
246
- # Imagine your object as a grand old mansion, every room stuffed with
247
- # trinkets, secrets, and the odd rubber duck. This method? It flings open
248
- # every window and lets a wild wind of nothingness sweep through, leaving
249
- # each field as empty as a poet’s wallet.
250
- #
251
- # All your precious attributes—gone! Swept into the void! It’s a spring
252
- # cleaning for the soul, a reset button for your existential dread.
253
- #
254
- # @return [void] Nothing left but echoes and nils.
255
- #
256
- # @example The Vanishing Act
257
- # wizard.clear_fields!
258
- # # => All fields are now nil, like a spell gone slightly too well.
259
- #
260
- def clear_fields!
261
- self.class.fields.each { |field| send("#{field}=", nil) }
262
- end
263
-
264
- # The Great Database Refresh-o-matic 3000
265
- #
266
- # Imagine your object as a forgetful time traveler. This method is like
267
- # zapping it with a memory ray from Redis-topia. ZAP! New memories!
268
- #
269
- # WARNING: This is not a gentle mind-meld. It's more like a full brain
270
- # transplant. Any half-baked ideas floating in your object's head? POOF!
271
- # Gone quicker than cake at a hobbit's birthday party. Unsaved spells
272
- # will definitely be forgotten.
273
- #
274
- # @return [void] What do you get for this daring act of digital amnesia? A shiny
275
- # list of all the brain bits that got a makeover!
276
- #
277
- # Remember: In the game of Redis-Refresh, you win or you... well, you
278
- # always win, but sometimes you forget why you played in the first place.
279
- #
280
- # @raise [Familia::KeyNotFoundError] If the dbkey does not exist.
281
- #
282
- # @example
283
- # object.refresh!
284
- def refresh!
285
- Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
286
- raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
287
- fields = hgetall
288
- Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}"
289
- optimistic_refresh(**fields)
290
- end
291
-
292
- # Ah, the magical refresh dance! It's like giving your object a
293
- # sip from the fountain of youth.
294
- #
295
- # This method twirls your object around, dips it into the Database pool,
296
- # and brings it back sparkling clean and up-to-date. It's using the
297
- # refresh! spell behind the scenes, so expect some Database whispering.
298
- #
299
- # @note Caution, young Rubyist! While this method loves to play
300
- # chain-tag with other methods, it's still got that refresh! kick.
301
- # It'll update your object faster than you can say "matz!"
302
- #
303
- # @return [self] Your object, freshly bathed in Database waters, ready
304
- # to dance with more methods in a conga line of Ruby joy!
305
- #
306
- # @raise [Familia::KeyNotFoundError] If the dbkey does not exist.
307
- #
308
- def refresh
309
- refresh!
310
- self
311
- end
312
-
313
- # Transform this object into a magical hash of wonders!
314
- #
315
- # This method performs an alchemical transmutation, turning our noble object
316
- # into a more plebeian hash. But fear not, for in this form, it can slip through
317
- # the cracks of the universe (or at least, into Redis) with ease.
318
- #
319
- # @return [Hash] A glittering hash, each key a field name, each value a
320
- # Redis-ready treasure.
321
- #
322
- # @example Turning your dragon into a hash
323
- # dragon.to_h
324
- # # => {"name"=>"Puff", "breathes"=>"fire", "age"=>1000}
325
- #
326
- # @note Watch in awe as each field is lovingly prepared for its Database adventure!
327
- #
328
- def to_h
329
- self.class.fields.inject({}) do |hsh, field|
330
- val = send(field)
331
- prepared = serialize_value(val)
332
- Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
333
-
334
- # Only include non-nil values in the hash for Redis
335
- hsh[field] = prepared unless prepared.nil?
336
- hsh
337
- end
338
- end
339
-
340
- # Line up all our attributes in a neat little array parade!
341
- #
342
- # This method marshals all our object's attributes into an orderly procession,
343
- # ready to march into Database in perfect formation. It's like a little data army,
344
- # but friendlier and less prone to conquering neighboring databases.
345
- #
346
- # @return [Array] A splendid array of Redis-ready values, in the order of our fields.
347
- #
348
- # @example Arranging your unicorn's attributes in a line
349
- # unicorn.to_a
350
- # # => ["Charlie", "magnificent", 5]
351
- #
352
- # @note Each value is carefully disguised in its Database costume
353
- # before joining the parade.
354
- #
355
- def to_a
356
- self.class.fields.map do |field|
357
- val = send(field)
358
- prepared = serialize_value(val)
359
- Familia.ld " [to_a] field: #{field} val: #{val.class} prepared: #{prepared.class}"
360
- prepared
361
- end
362
- end
363
-
364
- # Behold, the grand tale of two serialization sorcerers:
365
- # Familia::DataType and Familia::Horreum!
366
- #
367
- # These twin wizards, though cut from the same magical cloth,
368
- # have their own unique spells for turning Ruby objects into
369
- # Redis-friendly potions. Let's peek into their spell books:
370
- #
371
- # Shared Incantations:
372
- # - Both transform various data creatures for Database safekeeping
373
- # - They tame wild Strings, Symbols, and those slippery Numerics
374
- # - Secret rituals (aka custom serialization) are welcome
375
- #
376
- # Mystical Differences:
377
- # - DataType reads the future in opts[:class] tea leaves
378
- # - Horreum prefers to interrogate types more thoroughly
379
- # - DataType leaves a trail of debug breadcrumbs
380
- #
381
- # But wait! Enter the wise Familia.distinguisher,
382
- # a grand unifier of serialization magic!
383
- #
384
- # This clever mediator:
385
- # 1. Juggles a circus of data types from both realms
386
- # 2. Offers a 'strict_values' toggle for the type-obsessed
387
- # 3. Welcomes custom spells via dump_method
388
- # 4. Sprinkles debug fairy dust à la DataType
389
- #
390
- # By channeling the Familia.distinguisher, we've created a
391
- # harmonious serialization symphony, flexible enough to dance
392
- # with any data type that shimmies our way. And should we need
393
- # to teach it new tricks, we know just where to wave our wands!
394
- #
395
- # @param value [Object] The mystical object to be transformed
396
- #
397
- # @return [String] The transformed, Redis-ready value.
398
- #
399
- def serialize_value(val)
400
- prepared = Familia.distinguisher(val, strict_values: false)
401
-
402
- # If the distinguisher returns nil, try using the dump_method but only
403
- # use JSON serialization for complex types that need it.
404
- if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
405
- prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val)
406
- end
407
-
408
- # If both the distinguisher and dump_method return nil, log an error
409
- if prepared.nil?
410
- Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}"
411
- end
412
-
413
- prepared
414
- end
415
-
416
- # Converts a Database string value back to its original Ruby type
417
- #
418
- # This method attempts to deserialize JSON strings back to their original
419
- # Hash or Array types. Simple string values are returned as-is.
420
- #
421
- # @param val [String] The string value from Database to deserialize
422
- # @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
423
- # @return [Object] The deserialized value (Hash, Array, or original string)
424
- #
425
- def deserialize_value(val, symbolize: true)
426
- return val if val.nil? || val == ""
427
-
428
- # Try to parse as JSON first for complex types
429
- begin
430
- parsed = JSON.parse(val, symbolize_names: symbolize)
431
- # Only return parsed value if it's a complex type (Hash/Array)
432
- # Simple values should remain as strings
433
- return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
434
- rescue JSON::ParserError
435
- # Not valid JSON, return as-is
436
- end
437
-
438
- val
439
- end
440
-
441
- end
442
-
443
- include Serialization # these become Horreum instance methods
444
- end
445
- end