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,255 +1,21 @@
1
- # lib/familia/horreum/class_methods.rb
1
+ # lib/familia/horreum/subclass/management.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
34
- include Familia::Horreum::RelatedFieldsManagement
35
-
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
- # Converts the class name into a string that can be used to look up
241
- # configuration values. This is particularly useful when mapping
242
- # familia models with specific database numbers in the configuration.
243
- #
244
- # @example V2::Session.config_name => 'session'
245
- #
246
- # @return [String] The underscored class name as a string
247
- def config_name
248
- name.split('::').last
249
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
250
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
251
- .downcase
252
- end
17
+ module ManagementMethods
18
+ include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
253
19
 
254
20
  # Creates and persists a new instance of the class.
255
21
  #
@@ -289,9 +55,7 @@ module Familia
289
55
  #
290
56
  def create(*, **)
291
57
  fobj = new(*, **)
292
- raise Familia::Problem, "#{self} already exists: #{fobj.dbkey}" if fobj.exists?
293
-
294
- fobj.save
58
+ fobj.save_if_not_exists
295
59
  fobj
296
60
  end
297
61
 
@@ -406,8 +170,8 @@ module Familia
406
170
  # User.exists?(123) # Returns true if user:123:object exists in Redis
407
171
  #
408
172
  def exists?(identifier, suffix = nil)
173
+ raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
409
174
  suffix ||= self.suffix
410
- return false if identifier.to_s.empty?
411
175
 
412
176
  objkey = dbkey identifier, suffix
413
177
 
@@ -468,21 +232,34 @@ module Familia
468
232
  # distinction b/c passing in an explicitly nil is how DataType objects
469
233
  # at the class level are created without the global default 'object'
470
234
  # suffix. See DataType#dbkey "parent_class?" for more details.
235
+ #
471
236
  def dbkey(identifier, suffix = self.suffix)
472
- # Familia.ld "[.dbkey] #{identifier} for #{self} (suffix:#{suffix})"
473
- raise NoIdentifier, self if identifier.to_s.empty?
237
+ if identifier.to_s.empty?
238
+ raise NoIdentifier, "#{self} requires non-empty identifier, got: #{identifier.inspect}"
239
+ end
474
240
 
475
241
  identifier &&= identifier.to_s
476
242
  Familia.dbkey(prefix, identifier, suffix)
477
243
  end
478
244
 
479
- def dump_method
480
- @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) }
249
+ end
250
+
251
+ def any?(filter = '*')
252
+ matching_keys_count(filter) > 0
481
253
  end
482
254
 
483
- def load_method
484
- @load_method || :from_json # Familia.load_method
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
+ #
259
+ def matching_keys_count(filter = '*')
260
+ dbclient.keys(dbkey(filter)).compact.size
485
261
  end
262
+ alias size matching_keys_count # For backwards compatibility
486
263
  end
487
264
  end
488
265
  end
@@ -15,7 +15,7 @@ module Familia
15
15
  #
16
16
  # Usage:
17
17
  # Include this module in classes that need DataType management
18
- # Call setup_relations_accessors to initialize the feature
18
+ # Call setup_related_fields_accessors to initialize the feature
19
19
  #
20
20
  module RelatedFieldsManagement
21
21
  # A practical flag to indicate that a Horreum member has relations,
@@ -23,17 +23,22 @@ module Familia
23
23
  @has_relations = nil
24
24
 
25
25
  def self.included(base)
26
- base.extend(ClassMethods)
27
- base.setup_relations_accessors
26
+ base.extend(RelatedFieldsAccessors)
27
+ base.setup_related_fields_accessors
28
28
  end
29
29
 
30
- module ClassMethods
30
+ module RelatedFieldsAccessors
31
31
  # Sets up all DataType related methods
32
- # This method is the core of the metaprogramming logic
32
+ # This method generates the following for each registered DataType:
33
33
  #
34
- def setup_relations_accessors
34
+ # Instance methods: set(), list(), hashkey(), sorted_set(), etc.
35
+ # Query methods: set?(), list?(), hashkey?(), sorted_set?(), etc.
36
+ # Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
37
+ # Class methods: class_set(), class_list(), etc.
38
+ #
39
+ def setup_related_fields_accessors
35
40
  Familia::DataType.registered_types.each_pair do |kind, klass|
36
- Familia.ld "[registered_types] #{kind} => #{klass}"
41
+ Familia.trace :registered_types, kind, klass, caller(1..1) if Familia.debug?
37
42
 
38
43
  # Dynamically define instance-level relation methods
39
44
  #
@@ -86,11 +91,11 @@ module Familia
86
91
  end
87
92
  end
88
93
  end
89
- # End of ClassMethods module
94
+ # End of RelatedFieldsAccessors module
90
95
 
91
96
  # Creates an instance-level relation
92
97
  def attach_instance_related_field(name, klass, opts)
93
- Familia.ld "[#{self}##{name}] Attaching instance-level #{klass} #{opts}"
98
+ Familia.trace :attach_instance, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
94
99
  raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
95
100
 
96
101
  name = name.to_s.to_sym
@@ -115,7 +120,7 @@ module Familia
115
120
 
116
121
  # Creates a class-level relation
117
122
  def attach_class_related_field(name, klass, opts)
118
- Familia.ld "[#{self}.#{name}] Attaching class-level #{klass} #{opts}"
123
+ Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
119
124
  raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
120
125
 
121
126
  name = name.to_s.to_sym
@@ -1,5 +1,10 @@
1
1
  # lib/familia/horreum.rb
2
2
 
3
+ require_relative 'horreum/subclass/definition'
4
+ require_relative 'horreum/subclass/management'
5
+ require_relative 'horreum/shared/settings'
6
+ require_relative 'horreum/core'
7
+
3
8
  module Familia
4
9
  #
5
10
  # Horreum: A module for managing Redis-based object storage and relationships
@@ -23,6 +28,8 @@ module Familia
23
28
  #
24
29
  class Horreum
25
30
  include Familia::Base
31
+ include Familia::Horreum::Core
32
+ include Familia::Horreum::Settings
26
33
 
27
34
  # Singleton Class Context
28
35
  #
@@ -62,12 +69,14 @@ module Familia
62
69
  # Extends ClassMethods to subclasses and tracks Familia members
63
70
  def inherited(member)
64
71
  Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
65
- member.extend(ClassMethods)
66
- member.extend(Connection)
67
- member.extend(Features)
68
72
 
69
- # Tracks all the classes/modules that include Familia. It's
70
- # 10pm, do you know where you Familia members are?
73
+ # Class-level functionality extensions:
74
+ member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
75
+ member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
76
+ member.extend(Familia::Horreum::Connection) # dbclient, connection management
77
+ member.extend(Familia::Features) # feature() method for optional modules
78
+
79
+ # Track all classes that inherit from Horreum
71
80
  Familia.members << member
72
81
  super
73
82
  end
@@ -83,7 +92,7 @@ module Familia
83
92
  # Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
84
93
  #
85
94
  def initialize(*args, **kwargs)
86
- Familia.ld "[Horreum] Initializing #{self.class}"
95
+ Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
87
96
  initialize_relatives
88
97
 
89
98
  # No longer auto-create a key field - the identifier method will
@@ -122,7 +131,7 @@ module Familia
122
131
  elsif args.any?
123
132
  initialize_with_positional_args(*args)
124
133
  else
125
- Familia.ld "[Horreum] #{self.class} initialized with no arguments"
134
+ Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
126
135
  # Default values are intentionally NOT set here to:
127
136
  # - Maintain Database memory efficiency (only store non-nil values)
128
137
  # - Avoid conflicts with nil-skipping serialization logic
@@ -133,7 +142,11 @@ module Familia
133
142
  # Implementing classes can define an init method to do any
134
143
  # additional initialization. Notice that this is called
135
144
  # after the fields are set.
136
- init if respond_to?(:init)
145
+ init
146
+ end
147
+
148
+ def init(*args, **kwargs)
149
+ # Default no-op
137
150
  end
138
151
 
139
152
  # Sets up related Database objects for the instance
@@ -150,10 +163,10 @@ module Familia
150
163
  # familia_object.dbkey == v1:bone:INDEXVALUE:object
151
164
  # familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
152
165
  #
153
- self.class.related_fields.each_pair do |name, datatype_definition|
154
- klass = datatype_definition.klass
155
- opts = datatype_definition.opts
156
- Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
166
+ self.class.related_fields.each_pair do |name, data_type_definition|
167
+ klass = data_type_definition.klass
168
+ opts = data_type_definition.opts
169
+ Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
157
170
 
158
171
  # As a subclass of Familia::Horreum, we add ourselves as the parent
159
172
  # automatically. This is what determines the dbkey for DataType
@@ -215,7 +228,9 @@ module Familia
215
228
  # we use symbols. So we check for both.
216
229
  value = fields[field.to_sym] || fields[field.to_s]
217
230
  if value
218
- send(:"#{field}=", value)
231
+ # Use the mapped method name, not the field name
232
+ method_name = self.class.field_method_map[field] || field
233
+ send(:"#{method_name}=", value)
219
234
  field.to_sym
220
235
  end
221
236
  end
@@ -283,9 +298,8 @@ module Familia
283
298
  # # => #<Redis client v5.4.1 for redis://localhost:6379/0>
284
299
  #
285
300
  def dbclient
286
- conn = Fiber[:familia_transaction] || @dbclient || self.class.dbclient
301
+ Fiber[:familia_transaction] || @dbclient || self.class.dbclient
287
302
  # conn.select(self.class.logical_database)
288
- conn
289
303
  end
290
304
 
291
305
  def generate_id
@@ -299,14 +313,8 @@ module Familia
299
313
  # This allows passing Familia objects directly where strings are expected
300
314
  # without requiring explicit .identifier calls
301
315
  return super if identifier.to_s.empty?
316
+
302
317
  identifier.to_s
303
318
  end
304
319
  end
305
320
  end
306
-
307
- require_relative 'horreum/class_methods'
308
- require_relative 'horreum/commands'
309
- require_relative 'horreum/connection'
310
- require_relative 'horreum/serialization'
311
- require_relative 'horreum/settings'
312
- require_relative 'horreum/utils'
@@ -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
 
@@ -1,16 +1,18 @@
1
1
  # lib/familia/settings.rb
2
2
 
3
3
  module Familia
4
-
5
4
  @delim = ':'
6
5
  @prefix = nil
7
6
  @suffix = :object
8
7
  @default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
9
8
  @logical_database = nil
9
+ @encryption_keys = nil
10
+ @current_key_version = nil
11
+ @encryption_personalization = 'FamilialMatters'
10
12
 
11
13
  module Settings
12
-
13
- attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix
14
+ attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix, :encryption_keys,
15
+ :current_key_version, :encryption_personalization
14
16
 
15
17
  def delim(val = nil)
16
18
  @delim = val if val
@@ -44,5 +46,39 @@ module Familia
44
46
  suffix
45
47
  end
46
48
 
49
+ def encryption_keys(val = nil)
50
+ @encryption_keys = val if val
51
+ @encryption_keys
52
+ end
53
+
54
+ def current_key_version(val = nil)
55
+ @current_key_version = val if val
56
+ @current_key_version
57
+ end
58
+
59
+ # Personalization string for BLAKE2b key derivation in XChaCha20Poly1305.
60
+ # This provides cryptographic domain separation, ensuring derived keys are
61
+ # unique per application even with identical master keys and contexts.
62
+ # Must be 16 bytes or less (automatically padded with null bytes).
63
+ #
64
+ # @example
65
+ # Familia.configure do |config|
66
+ # config.encryption_personalization = 'MyApp1.0'
67
+ # end
68
+ #
69
+ # @param val [String, nil] The personalization string, or nil to get current value
70
+ # @return [String] Current personalization string
71
+ def encryption_personalization(val = nil)
72
+ if val
73
+ raise ArgumentError, 'Personalization string cannot exceed 16 bytes' if val.bytesize > 16
74
+
75
+ @encryption_personalization = val
76
+ end
77
+ @encryption_personalization
78
+ end
79
+
80
+ def config
81
+ self
82
+ end
47
83
  end
48
84
  end
data/lib/familia/utils.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # lib/familia/utils.rb
2
2
 
3
3
  module Familia
4
+
5
+ # Family-related utility methods
6
+ #
4
7
  module Utils
5
8
 
6
9
  # Joins array elements with Familia delimiter
@@ -145,5 +148,47 @@ module Familia
145
148
  end
146
149
  end
147
150
 
151
+ # Converts an absolute file path to a path relative to the current working
152
+ # directory. This simplifies logging and error reporting by showing
153
+ # only the relevant parts of file paths instead of lengthy absolute paths.
154
+ #
155
+ # @param filepath [String, Pathname] The file path to convert
156
+ # @return [Pathname, String, nil] A relative path from current directory,
157
+ # basename if path goes outside current directory, or nil if filepath is nil
158
+ #
159
+ # @example Using current directory as base
160
+ # Utils.pretty_path("/home/dev/project/lib/config.rb") # => "lib/config.rb"
161
+ #
162
+ # @example Path outside current directory
163
+ # Utils.pretty_path("/etc/hosts") # => "hosts"
164
+ #
165
+ # @example Nil input
166
+ # Utils.pretty_path(nil) # => nil
167
+ #
168
+ # @see Pathname#relative_path_from Ruby standard library documentation
169
+ def pretty_path(filepath)
170
+ return nil if filepath.nil?
171
+
172
+ basepath = Dir.pwd
173
+ relative_path = Pathname.new(filepath).relative_path_from(basepath)
174
+ if relative_path.to_s.start_with?('..')
175
+ File.basename(filepath)
176
+ else
177
+ relative_path
178
+ end
179
+ end
180
+
181
+ # Formats a stack trace with pretty file paths for improved readability
182
+ #
183
+ # @param limit [Integer] Maximum number of stack frames to include (default: 3)
184
+ # @return [String] Formatted stack trace with relative paths joined by newlines
185
+ #
186
+ # @example
187
+ # Utils.pretty_stack(limit: 10)
188
+ # # => "lib/models/user.rb:25:in `save'\n lib/controllers/app.rb:45:in `create'"
189
+ def pretty_stack(skip: 1, limit: 5)
190
+ caller(skip..(skip + limit + 1)).first(limit).map { |frame| pretty_path(frame) }.join("\n")
191
+ end
192
+
148
193
  end
149
194
  end