familia 2.0.0.pre3 → 2.0.0.pre5

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -1,242 +1,22 @@
1
- # lib/familia/horreum/class_methods.rb
1
+ # lib/familia/horreum/management_methods.rb
2
2
 
3
3
  require_relative 'related_fields_management'
4
4
 
5
5
  module Familia
6
6
  class Horreum
7
- # Class-level instance variables
8
- # These are set up as nil initially and populated later
9
- @dbclient = nil # TODO
10
- @identifier_field = nil
11
- @default_expiration = nil
12
- @logical_database = nil
13
- @uri = nil
14
- @suffix = nil
15
- @prefix = nil
16
- @fields = nil # []
17
- @class_related_fields = nil # {}
18
- @related_fields = nil # {}
19
- @dump_method = nil
20
- @load_method = nil
21
-
22
- # ClassMethods: Provides class-level functionality for Horreum
7
+ # ManagementMethods: Provides class-level functionality for Horreum
8
+ # records.
23
9
  #
24
10
  # This module is extended into classes that include Familia::Horreum,
25
11
  # providing methods for Database operations and object management.
26
12
  #
27
- # Key features:
13
+ # # Key features:
28
14
  # * Includes RelatedFieldsManagement for DataType field handling
29
- # * Defines methods for managing fields, identifiers, and dbkeys
30
15
  # * Provides utility methods for working with Database objects
31
16
  #
32
- module ClassMethods
33
- include Familia::Settings
17
+ module ManagementMethods
34
18
  include Familia::Horreum::RelatedFieldsManagement
35
19
 
36
- # Sets or retrieves the unique identifier field for the class.
37
- #
38
- # This method defines or returns the field or method that contains the unique
39
- # identifier used to generate the dbkey for the object. If a value is provided,
40
- # it sets the identifier field; otherwise, it returns the current identifier field.
41
- #
42
- # @param [Object] val the field name or method to set as the identifier field (optional).
43
- # @return [Object] the current identifier field.
44
- #
45
- def identifier_field(val = nil)
46
- if val
47
- # Validate identifier field definition at class definition time
48
- case val
49
- when Symbol, String, Proc
50
- @identifier_field = val
51
- else
52
- raise Problem, <<~ERROR
53
- Invalid identifier field definition: #{val.inspect}.
54
- Use a field name (Symbol/String) or Proc.
55
- ERROR
56
- end
57
- end
58
- @identifier_field
59
- end
60
-
61
- # Defines a field for the class and creates accessor methods.
62
- #
63
- # This method defines a new field for the class, creating getter and setter
64
- # instance methods similar to `attr_accessor`. It also generates a fast
65
- # writer method for immediate persistence to Redis.
66
- #
67
- # @param [Symbol, String] name the name of the field to define.
68
- #
69
- def field(name)
70
- fields << name
71
- attr_accessor name
72
-
73
- # Every field gets a fast attribute method for immediately persisting
74
- fast_attribute! name
75
- end
76
-
77
- # Defines a fast attribute method with a bang (!) suffix for a given
78
- # attribute name. Fast attribute methods are used to immediately read or
79
- # write attribute values from/to Redis. Calling a fast attribute method
80
- # has no effect on any of the object's other attributes and does not
81
- # trigger a call to update the object's expiration time.
82
- #
83
- # The dynamically defined method performs the following:
84
- # - Acts as both a reader and a writer method.
85
- # - When called without arguments, retrieves the current value from Redis.
86
- # - When called with an argument, persists the value to Database immediately.
87
- # - Checks if the correct number of arguments is provided (zero or one).
88
- # - Converts the provided value to a format suitable for Database storage.
89
- # - Uses the existing accessor method to set the attribute value when
90
- # writing.
91
- # - Persists the value to Database immediately using the hset command when
92
- # writing.
93
- # - Includes custom error handling to raise an ArgumentError if the wrong
94
- # number of arguments is given.
95
- # - Raises a custom error message if an exception occurs during the
96
- # execution of the method.
97
- #
98
- # @param [Symbol, String] name the name of the attribute for which the
99
- # fast method is defined.
100
- # @return [Object] the current value of the attribute when called without
101
- # arguments.
102
- # @raise [ArgumentError] if more than one argument is provided.
103
- # @raise [RuntimeError] if an exception occurs during the execution of the
104
- # method.
105
- #
106
- def fast_attribute!(name = nil)
107
- # Fast attribute accessor method for the '#{name}' attribute.
108
- # This method provides immediate read and write access to the attribute
109
- # in Redis.
110
- #
111
- # When called without arguments, it retrieves the current value of the
112
- # attribute from Redis.
113
- # When called with an argument, it immediately persists the new value to
114
- # Redis.
115
- #
116
- # @overload #{name}!
117
- # Retrieves the current value of the attribute from Redis.
118
- # @return [Object] the current value of the attribute.
119
- #
120
- # @overload #{name}!(value)
121
- # Sets and immediately persists the new value of the attribute to
122
- # Redis.
123
- # @param value [Object] the new value to set for the attribute.
124
- # @return [Object] the newly set value.
125
- #
126
- # @raise [ArgumentError] if more than one argument is provided.
127
- # @raise [RuntimeError] if an exception occurs during the execution of
128
- # the method.
129
- #
130
- # @note This method bypasses any object-level caching and interacts
131
- # directly with Redis. It does not trigger updates to other attributes
132
- # or the object's expiration time.
133
- #
134
- # @example
135
- #
136
- # def #{name}!(*args)
137
- # # Method implementation
138
- # end
139
- #
140
- define_method :"#{name}!" do |*args|
141
- # Check if the correct number of arguments is provided (exactly one).
142
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
143
-
144
- val = args.first
145
-
146
- # If no value is provided to this fast attribute method, make a call
147
- # to the db to return the current stored value of the hash field.
148
- return hget name if val.nil?
149
-
150
- begin
151
- # Trace the operation if debugging is enabled.
152
- Familia.trace :FAST_WRITER, dbclient, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
153
-
154
- # Convert the provided value to a format suitable for Database storage.
155
- prepared = serialize_value(val)
156
- Familia.ld "[.fast_attribute!] #{name} val: #{val.class} prepared: #{prepared.class}"
157
-
158
- # Use the existing accessor method to set the attribute value.
159
- send :"#{name}=", val
160
-
161
- # Persist the value to Database immediately using the hset command.
162
- hset name, prepared
163
- rescue Familia::Problem => e
164
- # Raise a custom error message if an exception occurs during the execution of the method.
165
- raise "#{name}! method failed: #{e.message}", e.backtrace
166
- end
167
- end
168
- end
169
-
170
- # Returns the list of field names defined for the class in the order
171
- # that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
172
- def fields
173
- @fields ||= []
174
- @fields
175
- end
176
-
177
- def class_related_fields
178
- @class_related_fields ||= {}
179
- @class_related_fields
180
- end
181
-
182
- def related_fields
183
- @related_fields ||= {}
184
- @related_fields
185
- end
186
-
187
- def has_relations?
188
- @has_relations ||= false
189
- end
190
-
191
- def logical_database(v = nil)
192
- Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v}", caller(1..1) if Familia.debug?
193
- @logical_database = v unless v.nil?
194
- @logical_database || parent&.logical_database
195
- end
196
-
197
- def all(suffix = nil)
198
- suffix ||= self.suffix
199
- # objects that could not be parsed will be nil
200
- keys(suffix).filter_map { |k| find_by_key(k) }
201
- end
202
-
203
- def any?(filter = '*')
204
- matching_keys_count(filter) > 0
205
- end
206
-
207
- # Returns the number of dbkeys matching the given filter pattern
208
- # @param filter [String] dbkey pattern to match (default: '*')
209
- # @return [Integer] Number of matching keys
210
- def matching_keys_count(filter = '*')
211
- dbclient.keys(dbkey(filter)).compact.size
212
- end
213
- alias size matching_keys_count # For backwards compatibility
214
-
215
- def suffix(a = nil, &blk)
216
- @suffix = a || blk if a || !blk.nil?
217
- @suffix || Familia.default_suffix
218
- end
219
-
220
- # Sets or retrieves the prefix for generating Redis keys.
221
- #
222
- # @param a [String, Symbol, nil] the prefix to set (optional).
223
- # @return [String, Symbol] the current prefix.
224
- #
225
- # The exception is only raised when both @prefix is nil/falsy AND name is nil,
226
- # which typically occurs with anonymous classes that haven't had their prefix
227
- # explicitly set.
228
- #
229
- def prefix(a = nil)
230
- @prefix = a if a
231
- @prefix || begin
232
- if name.nil?
233
- raise Problem, 'Cannot generate prefix for anonymous class. ' \
234
- 'Use `prefix` method to set explicitly.'
235
- end
236
- name.downcase.gsub('::', Familia.delim).to_sym
237
- end
238
- end
239
-
240
20
  # Creates and persists a new instance of the class.
241
21
  #
242
22
  # @param *args [Array] Variable number of positional arguments to be passed
@@ -462,13 +242,23 @@ module Familia
462
242
  Familia.dbkey(prefix, identifier, suffix)
463
243
  end
464
244
 
465
- def dump_method
466
- @dump_method || :to_json # Familia.dump_method
245
+ def all(suffix = nil)
246
+ suffix ||= self.suffix
247
+ # objects that could not be parsed will be nil
248
+ keys(suffix).filter_map { |k| find_by_key(k) }
467
249
  end
468
250
 
469
- def load_method
470
- @load_method || :from_json # Familia.load_method
251
+ def any?(filter = '*')
252
+ matching_keys_count(filter) > 0
471
253
  end
254
+
255
+ # Returns the number of dbkeys matching the given filter pattern
256
+ # @param filter [String] dbkey pattern to match (default: '*')
257
+ # @return [Integer] Number of matching keys
258
+ def matching_keys_count(filter = '*')
259
+ dbclient.keys(dbkey(filter)).compact.size
260
+ end
261
+ alias size matching_keys_count # For backwards compatibility
472
262
  end
473
263
  end
474
264
  end
@@ -1,8 +1,6 @@
1
1
  # lib/familia/horreum/serialization.rb
2
2
  #
3
3
  module Familia
4
-
5
-
6
4
  # Familia::Horreum
7
5
  #
8
6
  class Horreum
@@ -26,7 +24,7 @@ module Familia
26
24
  #
27
25
  # May your Database returns be ever valid, and your data ever flowing!
28
26
  #
29
- @valid_command_return_values = ["OK", true, 1, 0, nil].freeze
27
+ @valid_command_return_values = ['OK', true, 1, 0, nil].freeze
30
28
 
31
29
  class << self
32
30
  attr_reader :valid_command_return_values
@@ -59,7 +57,6 @@ module Familia
59
57
  # the Ruby roses, but watch out for the Database thorns!)
60
58
  #
61
59
  module Serialization
62
-
63
60
  # Save our precious data to Redis, with a sprinkle of timestamp magic!
64
61
  #
65
62
  # This method is like a conscientious historian, not only recording your
@@ -76,7 +73,7 @@ module Familia
76
73
  # @note This method will leave breadcrumbs (traces) if you're in debug mode.
77
74
  # It's like Hansel and Gretel, but for data operations!
78
75
  #
79
- def save update_expiration: true
76
+ def save(update_expiration: true)
80
77
  Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug?
81
78
 
82
79
  # No longer need to sync computed identifier with a cache field
@@ -198,11 +195,11 @@ module Familia
198
195
  # @note The expiration update is only performed for classes that have
199
196
  # the expiration feature enabled. For others, it's a no-op.
200
197
  #
201
- def commit_fields update_expiration: true
198
+ def commit_fields(update_expiration: true)
202
199
  prepared_value = to_h
203
200
  Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
204
201
 
205
- result = self.hmset(prepared_value)
202
+ result = hmset(prepared_value)
206
203
 
207
204
  # Only classes that have the expiration ferature enabled will
208
205
  # actually set an expiration time on their keys. Otherwise
@@ -258,7 +255,7 @@ module Familia
258
255
  # # => All fields are now nil, like a spell gone slightly too well.
259
256
  #
260
257
  def clear_fields!
261
- self.class.fields.each { |field| send("#{field}=", nil) }
258
+ self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
262
259
  end
263
260
 
264
261
  # The Great Database Refresh-o-matic 3000
@@ -284,8 +281,15 @@ module Familia
284
281
  def refresh!
285
282
  Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
286
283
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
284
+
287
285
  fields = hgetall
288
286
  Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}"
287
+
288
+ # Reset transient fields to nil for semantic clarity and ORM consistency
289
+ # Transient fields have no authoritative source, so they should return to
290
+ # their uninitialized state during refresh operations
291
+ reset_transient_fields!
292
+
289
293
  optimistic_refresh(**fields)
290
294
  end
291
295
 
@@ -326,14 +330,15 @@ module Familia
326
330
  # @note Watch in awe as each field is lovingly prepared for its Database adventure!
327
331
  #
328
332
  def to_h
329
- self.class.fields.inject({}) do |hsh, field|
330
- val = send(field)
333
+ self.class.persistent_fields.each_with_object({}) do |field, hsh|
334
+ field_type = self.class.field_types[field]
335
+ method_name = field_type.method_name
336
+ val = send(method_name)
331
337
  prepared = serialize_value(val)
332
338
  Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
333
339
 
334
340
  # Only include non-nil values in the hash for Redis
335
341
  hsh[field] = prepared unless prepared.nil?
336
- hsh
337
342
  end
338
343
  end
339
344
 
@@ -353,10 +358,12 @@ module Familia
353
358
  # before joining the parade.
354
359
  #
355
360
  def to_a
356
- self.class.fields.map do |field|
357
- val = send(field)
361
+ self.class.persistent_fields.collect do |field|
362
+ field_type = self.class.field_types[field]
363
+ method_name = field_type.method_name
364
+ val = send(method_name)
358
365
  prepared = serialize_value(val)
359
- Familia.ld " [to_a] field: #{field} val: #{val.class} prepared: #{prepared.class}"
366
+ Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
360
367
  prepared
361
368
  end
362
369
  end
@@ -406,9 +413,7 @@ module Familia
406
413
  end
407
414
 
408
415
  # 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
416
+ Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
412
417
 
413
418
  prepared
414
419
  end
@@ -423,7 +428,7 @@ module Familia
423
428
  # @return [Object] The deserialized value (Hash, Array, or original string)
424
429
  #
425
430
  def deserialize_value(val, symbolize: true)
426
- return val if val.nil? || val == ""
431
+ return val if val.nil? || val == ''
427
432
 
428
433
  # Try to parse as JSON first for complex types
429
434
  begin
@@ -438,6 +443,29 @@ module Familia
438
443
  val
439
444
  end
440
445
 
446
+ private
447
+
448
+ # Reset all transient fields to nil
449
+ #
450
+ # This method ensures that transient fields return to their uninitialized
451
+ # state during refresh operations. This provides semantic clarity (refresh
452
+ # means "reload from authoritative source"), ORM consistency with other
453
+ # frameworks, and prevents stale transient data accumulation.
454
+ #
455
+ # @return [void]
456
+ #
457
+ def reset_transient_fields!
458
+ return unless self.class.respond_to?(:transient_fields)
459
+
460
+ self.class.transient_fields.each do |field_name|
461
+ field_type = self.class.field_types[field_name]
462
+ next unless field_type&.method_name
463
+
464
+ # Set the transient field back to nil
465
+ send("#{field_type.method_name}=", nil)
466
+ Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
467
+ end
468
+ end
441
469
  end
442
470
 
443
471
  include Serialization # these become Horreum instance methods
@@ -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
@@ -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,7 +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
40
 
42
41
  include Utils # these become Horreum instance methods
@@ -62,7 +62,8 @@ module Familia
62
62
  # Extends ClassMethods to subclasses and tracks Familia members
63
63
  def inherited(member)
64
64
  Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
65
- member.extend(ClassMethods)
65
+ member.extend(DefinitionMethods)
66
+ member.extend(ManagementMethods)
66
67
  member.extend(Connection)
67
68
  member.extend(Features)
68
69
 
@@ -133,7 +134,11 @@ module Familia
133
134
  # Implementing classes can define an init method to do any
134
135
  # additional initialization. Notice that this is called
135
136
  # after the fields are set.
136
- init if respond_to?(:init)
137
+ init
138
+ end
139
+
140
+ def init(*args, **kwargs)
141
+ # Default no-op
137
142
  end
138
143
 
139
144
  # Sets up related Database objects for the instance
@@ -150,9 +155,9 @@ module Familia
150
155
  # familia_object.dbkey == v1:bone:INDEXVALUE:object
151
156
  # familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
152
157
  #
153
- self.class.related_fields.each_pair do |name, datatype_definition|
154
- klass = datatype_definition.klass
155
- opts = datatype_definition.opts
158
+ self.class.related_fields.each_pair do |name, data_type_definition|
159
+ klass = data_type_definition.klass
160
+ opts = data_type_definition.opts
156
161
  Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
157
162
 
158
163
  # As a subclass of Familia::Horreum, we add ourselves as the parent
@@ -215,7 +220,9 @@ module Familia
215
220
  # we use symbols. So we check for both.
216
221
  value = fields[field.to_sym] || fields[field.to_s]
217
222
  if value
218
- send(:"#{field}=", value)
223
+ # Use the mapped method name, not the field name
224
+ method_name = self.class.field_method_map[field] || field
225
+ send(:"#{method_name}=", value)
219
226
  field.to_sym
220
227
  end
221
228
  end
@@ -283,9 +290,8 @@ module Familia
283
290
  # # => #<Redis client v5.4.1 for redis://localhost:6379/0>
284
291
  #
285
292
  def dbclient
286
- conn = Fiber[:familia_transaction] || @dbclient || self.class.dbclient
293
+ Fiber[:familia_transaction] || @dbclient || self.class.dbclient
287
294
  # conn.select(self.class.logical_database)
288
- conn
289
295
  end
290
296
 
291
297
  def generate_id
@@ -299,13 +305,15 @@ module Familia
299
305
  # This allows passing Familia objects directly where strings are expected
300
306
  # without requiring explicit .identifier calls
301
307
  return super if identifier.to_s.empty?
308
+
302
309
  identifier.to_s
303
310
  end
304
311
  end
305
312
  end
306
313
 
307
- require_relative 'horreum/class_methods'
308
- require_relative 'horreum/commands'
314
+ require_relative 'horreum/definition_methods'
315
+ require_relative 'horreum/management_methods'
316
+ require_relative 'horreum/database_commands'
309
317
  require_relative 'horreum/connection'
310
318
  require_relative 'horreum/serialization'
311
319
  require_relative 'horreum/settings'
@@ -6,14 +6,14 @@ require 'logger'
6
6
  module Familia
7
7
  @logger = Logger.new($stdout)
8
8
  @logger.progname = name
9
- @logger.formatter = proc do |severity, datetime, progname, msg|
10
- severity_letter = severity[0] # Get the first letter of the severity
9
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
10
+ severity_letter = severity[0] # Get the first letter of the severity
11
11
  pid = Process.pid
12
12
  thread_id = Thread.current.object_id
13
- full_path, line = caller[4].split(":")[0..1]
13
+ full_path, line = caller(5..5).first.split(':')[0..1]
14
14
  parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
15
15
  relative_path = full_path.sub(parent_path.to_s, 'familia')
16
- utc_datetime = datetime.utc.strftime("%m-%d %H:%M:%S.%6N")
16
+ utc_datetime = datetime.utc.strftime('%m-%d %H:%M:%S.%6N')
17
17
 
18
18
  # Get the severity letter from the thread local variable or use
19
19
  # the default. The thread local variable is set in the trace
@@ -115,6 +115,7 @@ module Familia
115
115
 
116
116
  def ld(*msg)
117
117
  return unless Familia.debug?
118
+
118
119
  @logger.debug(*msg)
119
120
  end
120
121
 
@@ -152,15 +153,15 @@ module Familia
152
153
  # and multi blocks. In some contexts it's nil where the
153
154
  # database connection isn't relevant.
154
155
  instance_id = if dbclient
155
- case dbclient
156
- when Redis
157
- dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
158
- when Redis::Future
159
- "Redis::Future"
160
- else
161
- dbclient.class.name
162
- end
163
- end
156
+ case dbclient
157
+ when Redis
158
+ dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
159
+ when Redis::Future
160
+ 'Redis::Future'
161
+ else
162
+ dbclient.class.name
163
+ end
164
+ end
164
165
 
165
166
  codeline = if context
166
167
  context = [context].flatten
@@ -170,7 +171,6 @@ module Familia
170
171
 
171
172
  @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
172
173
  end
173
-
174
174
  end
175
175
  end
176
176