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
@@ -0,0 +1,469 @@
1
+ # lib/familia/horreum/subclass/definition.rb
2
+
3
+ require_relative 'related_fields_management'
4
+ require_relative '../shared/settings'
5
+
6
+ module Familia
7
+ VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
8
+
9
+ # Familia::Horreum
10
+ #
11
+ class Horreum
12
+ # Class-level instance variables
13
+ # These are set up as nil initially and populated later
14
+ #
15
+ # Connection and database settings
16
+ @dbclient = nil
17
+ @logical_database = nil
18
+ @uri = nil
19
+
20
+ # Database Key generation settings
21
+ @prefix = nil
22
+ @identifier_field = nil
23
+ @suffix = nil
24
+
25
+ # Fields and relationships
26
+ @fields = nil
27
+ @class_related_fields = nil
28
+ @related_fields = nil
29
+ @default_expiration = nil
30
+
31
+ # Serialization settings
32
+ @dump_method = nil
33
+ @load_method = nil
34
+
35
+ # DefinitionMethods: Provides class-level functionality for Horreum subclasses
36
+ #
37
+ # This module is extended into classes that include Familia::Horreum,
38
+ # providing methods for Database operations and object management.
39
+ #
40
+ # Key features:
41
+ # * Includes RelatedFieldsManagement for DataType field handling
42
+ # * Defines methods for managing fields, identifiers, and dbkeys
43
+ # * Provides utility methods for working with Database objects
44
+ #
45
+ module DefinitionMethods
46
+ include Familia::Settings
47
+ include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
48
+
49
+ # Sets or retrieves the unique identifier field for the class.
50
+ #
51
+ # This method defines or returns the field or method that contains the unique
52
+ # identifier used to generate the dbkey for the object. If a value is provided,
53
+ # it sets the identifier field; otherwise, it returns the current identifier field.
54
+ #
55
+ # @param [Object] val the field name or method to set as the identifier field (optional).
56
+ # @return [Object] the current identifier field.
57
+ #
58
+ def identifier_field(val = nil)
59
+ if val
60
+ # Validate identifier field definition at class definition time
61
+ case val
62
+ when Symbol, String, Proc
63
+ @identifier_field = val
64
+ else
65
+ raise Problem, <<~ERROR
66
+ Invalid identifier field definition: #{val.inspect}.
67
+ Use a field name (Symbol/String) or Proc.
68
+ ERROR
69
+ end
70
+ end
71
+ @identifier_field
72
+ end
73
+
74
+ # Defines a field for the class and creates accessor methods.
75
+ #
76
+ # This method defines a new field for the class, creating getter and setter
77
+ # instance methods similar to `attr_accessor`. It also generates a fast
78
+ # writer method for immediate persistence to Redis.
79
+ #
80
+ # @param name [Symbol, String] the name of the field to define. If a method
81
+ # with the same name already exists, an error is raised.
82
+ # @param as [Symbol, String, false, nil] as the name to use for the accessor method (defaults to name).
83
+ # If false or nil, no accessor methods are created.
84
+ # @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :"#{name}!").
85
+ # If false or nil, no fast writer method is created.
86
+ # @param on_conflict [Symbol] conflict resolution strategy when method already exists:
87
+ # - :raise - raise error if method exists (default)
88
+ # - :skip - skip definition if method exists
89
+ # - :warn - warn but proceed (may overwrite)
90
+ # - :ignore - proceed silently (may overwrite)
91
+ # @param category [Symbol, nil] field category for special handling:
92
+ # - nil - regular field (default)
93
+ # - :encrypted - field contains encrypted data
94
+ # - :transient - field is not persisted
95
+ # - Others, depending on features available
96
+ #
97
+ def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
98
+ # Use field type system for consistency
99
+ require_relative '../../field_type'
100
+
101
+ # Create appropriate field type based on category
102
+ field_type = if category == :transient
103
+ require_relative '../../features/transient_fields/transient_field_type'
104
+ TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
105
+ else
106
+ # For regular fields and other categories, create custom field type with category override
107
+ custom_field_type = Class.new(FieldType) do
108
+ define_method :category do
109
+ category || :field
110
+ end
111
+ end
112
+ custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
113
+ end
114
+
115
+ register_field_type(field_type)
116
+ end
117
+
118
+ # Sets or retrieves the suffix for generating Redis keys.
119
+ #
120
+ # @param a [String, Symbol, nil] the suffix to set (optional).
121
+ # @param blk [Proc] a block that returns the suffix (optional).
122
+ # @return [String, Symbol] the current suffix or Familia.default_suffix if none is set.
123
+ #
124
+ def suffix(a = nil, &blk)
125
+ @suffix = a || blk if a || !blk.nil?
126
+ @suffix || Familia.default_suffix
127
+ end
128
+
129
+ # Sets or retrieves the prefix for generating Redis keys.
130
+ #
131
+ # @param a [String, Symbol, nil] the prefix to set (optional).
132
+ # @return [String, Symbol] the current prefix.
133
+ #
134
+ # The exception is only raised when both @prefix is nil/falsy AND name is nil,
135
+ # which typically occurs with anonymous classes that haven't had their prefix
136
+ # explicitly set.
137
+ #
138
+ def prefix(a = nil)
139
+ @prefix = a if a
140
+ @prefix || begin
141
+ if name.nil?
142
+ raise Problem, 'Cannot generate prefix for anonymous class. ' \
143
+ 'Use `prefix` method to set explicitly.'
144
+ end
145
+ name.downcase.gsub('::', Familia.delim).to_sym
146
+ end
147
+ end
148
+
149
+ def logical_database(v = nil)
150
+ Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v.nil?}", caller(0..2) if Familia.debug?
151
+ @logical_database = v unless v.nil?
152
+ @logical_database || parent&.logical_database
153
+ end
154
+
155
+ # Returns the list of field names defined for the class in the order
156
+ # that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
157
+ def fields
158
+ @fields ||= []
159
+ @fields
160
+ end
161
+
162
+ def class_related_fields
163
+ @class_related_fields ||= {}
164
+ @class_related_fields
165
+ end
166
+
167
+ def related_fields
168
+ @related_fields ||= {}
169
+ @related_fields
170
+ end
171
+
172
+ def has_relations?
173
+ @has_relations ||= false
174
+ end
175
+
176
+ # Converts the class name into a string that can be used to look up
177
+ # configuration values. This is particularly useful when mapping
178
+ # familia models with specific database numbers in the configuration.
179
+ #
180
+ # @example V2::Session.config_name => 'session'
181
+ #
182
+ # @return [String] The underscored class name as a string
183
+ def config_name
184
+ name.split('::').last
185
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
186
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
187
+ .downcase
188
+ end
189
+
190
+ def dump_method
191
+ @dump_method || :to_json # Familia.dump_method
192
+ end
193
+
194
+ def load_method
195
+ @load_method || :from_json # Familia.load_method
196
+ end
197
+
198
+ # Storage for field type instances
199
+ def field_types
200
+ @field_types ||= {}
201
+ end
202
+
203
+ # Returns a hash mapping field names to method names for backward compatibility
204
+ def field_method_map
205
+ field_types.transform_values(&:method_name)
206
+ end
207
+
208
+ # Get fields for serialization (excludes transients)
209
+ def persistent_fields
210
+ fields.select do |field|
211
+ field_types[field]&.persistent?
212
+ end
213
+ end
214
+
215
+ # Get fields that are not persisted to the database (transients)
216
+ def transient_fields
217
+ fields.select do |field|
218
+ field_types[field]&.transient?
219
+ end
220
+ end
221
+
222
+ # Register a field type instance with this class
223
+ #
224
+ # This method installs the field type's methods and registers it
225
+ # for later reference. It maintains backward compatibility by
226
+ # creating FieldDefinition objects.
227
+ #
228
+ # @param field_type [FieldType] The field type to register
229
+ #
230
+ def register_field_type(field_type)
231
+ fields << field_type.name
232
+ field_type.install(self)
233
+ # Complete the registration after installation. If we do this beforehand
234
+ # we can run into issues where it looks like it's already installed.
235
+ field_types[field_type.name] = field_type
236
+ end
237
+
238
+ # Create and register a transient field type
239
+ #
240
+ # @param name [Symbol] The field name
241
+ # @param options [Hash] Field options
242
+ #
243
+ def transient_field(name, **)
244
+ require_relative '../../features/transient_fields/transient_field_type'
245
+ field_type = TransientFieldType.new(name, **, fast_method: false)
246
+ register_field_type(field_type)
247
+ end
248
+
249
+ private
250
+
251
+ # Hook to detect silent overwrites and handle conflicts
252
+ def method_added(method_name)
253
+ super
254
+
255
+ # Find the field type that generated this method
256
+ field_type = field_types.values.find { |ft| ft.generated_methods.include?(method_name) }
257
+ return unless field_type
258
+
259
+ case field_type.on_conflict
260
+ when :warn
261
+ warn <<~WARNING
262
+
263
+ WARNING: Method >>> #{method_name} <<< was redefined after field definition.
264
+ Field functionality may be broken. Consider using a different name
265
+ with field(:#{field_type.name}, as: :other_name)
266
+
267
+ Called from:
268
+ #{Familia.pretty_stack(limit: 3)}
269
+
270
+ WARNING
271
+ when :raise
272
+ raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}"
273
+ when :skip, :ignore
274
+ # Do nothing, skip silently
275
+ end
276
+ end
277
+
278
+ def define_attr_accessor_methods(field_name, method_name, on_conflict)
279
+ handle_method_conflict(method_name, on_conflict) do
280
+ # Equivalent to `attr_reader :field_name`
281
+ define_method method_name do
282
+ instance_variable_get(:"@#{field_name}")
283
+ end
284
+ # Equivalent to `attr_writer :field_name=`
285
+ define_method :"#{method_name}=" do |value|
286
+ instance_variable_set(:"@#{field_name}", value)
287
+ end
288
+ end
289
+ end
290
+
291
+ # Fast attribute accessor method for immediate DB persistence.
292
+ #
293
+ # @param field_name [Symbol, String] the name of the horreum model attribute
294
+ # @param method_name [Symbol, String] the name of the regular accessor method
295
+ # @param fast_method_name [Symbol, String] the name of the fast method (must end with '!')
296
+ # @param on_conflict [Symbol] conflict resolution strategy for method name conflicts
297
+ #
298
+ # @return [void]
299
+ #
300
+ # @raise [ArgumentError] if fast_method_name doesn't end with '!'
301
+ #
302
+ # @note Generated method behavior:
303
+ # - Without args: Retrieves current value from Redis
304
+ # - With value: Sets and immediately persists to Redis
305
+ # - Returns boolean indicating success for writes
306
+ # - Bypasses object-level caching and expiration updates
307
+ #
308
+ # @example
309
+ # # Creates a method like: username!(value = nil)
310
+ # define_fast_writer_method(:username, :username, :username!, :raise)
311
+ #
312
+ def define_fast_writer_method(field_name, method_name, fast_method_name, on_conflict)
313
+ raise ArgumentError, 'Must end with !' unless fast_method_name.to_s.end_with?('!')
314
+
315
+ handle_method_conflict(fast_method_name, on_conflict) do
316
+ # Fast attribute accessor method for the '#{field_name}' attribute.
317
+ # This method provides immediate read and write access to the attribute
318
+ # in Redis.
319
+ #
320
+ # When called without arguments, it retrieves the current value of the
321
+ # attribute from Redis.
322
+ # When called with an argument, it immediately persists the new value to
323
+ # Redis.
324
+ #
325
+ # @overload #{method_name}
326
+ # Retrieves the current value of the attribute from Redis.
327
+ # @return [Object] the current value of the attribute.
328
+ #
329
+ # @overload #{method_name}(value)
330
+ # Sets and immediately persists the new value of the attribute to
331
+ # Redis.
332
+ # @param value [Object] the new value to set for the attribute.
333
+ # @return [Object] the newly set value.
334
+ #
335
+ # @raise [ArgumentError] if more than one argument is provided.
336
+ # @raise [RuntimeError] if an exception occurs during the execution of
337
+ # the method.
338
+ #
339
+ # @note This method bypasses any object-level caching and interacts
340
+ # directly with Redis. It does not trigger updates to other attributes
341
+ # or the object's expiration time.
342
+ #
343
+ # @example
344
+ #
345
+ # def field_name!(*args)
346
+ # # Method implementation
347
+ # end
348
+ #
349
+ define_method fast_method_name do |*args|
350
+ # Check if the correct number of arguments is provided (exactly one).
351
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
352
+
353
+ val = args.first
354
+
355
+ # If no value is provided to this fast attribute method, make a call
356
+ # to the db to return the current stored value of the hash field.
357
+ return hget field_name if val.nil?
358
+
359
+ begin
360
+ # Trace the operation if debugging is enabled.
361
+ Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
362
+
363
+ # Convert the provided value to a format suitable for Database storage.
364
+ prepared = serialize_value(val)
365
+ Familia.ld "[.define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
366
+
367
+ # Use the existing accessor method to set the attribute value.
368
+ send :"#{method_name}=", val
369
+
370
+ # Persist the value to Database immediately using the hset command.
371
+ ret = hset field_name, prepared
372
+ ret.zero? || ret.positive?
373
+ rescue Familia::Problem => e
374
+ # Raise a custom error message if an exception occurs during the execution of the method.
375
+ raise "#{fast_method_name} method failed: #{e.message}", e.backtrace
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ # Handles method name conflicts during dynamic method definition.
382
+ #
383
+ # @param method_name [Symbol, String] the method to define
384
+ # @param strategy [Symbol] conflict resolution strategy:
385
+ # - :raise - raise error if method exists (default)
386
+ # - :skip - skip definition if method exists
387
+ # - :warn - warn but proceed (may overwrite)
388
+ # - :overwrite - explicitly remove existing method first
389
+ #
390
+ # @yield the method definition to execute
391
+ #
392
+ # @example
393
+ # handle_method_conflict(:my_method, :skip) do
394
+ # attr_accessor :my_method
395
+ # end
396
+ #
397
+ # @raise [ArgumentError] if strategy invalid or method exists with :raise
398
+ #
399
+ # @private
400
+ def handle_method_conflict(method_name, strategy, &)
401
+ validate_strategy!(strategy)
402
+
403
+ if method_exists?(method_name)
404
+ handle_existing_method(method_name, strategy, &)
405
+ else
406
+ yield
407
+ end
408
+ end
409
+
410
+ def validate_strategy!(strategy)
411
+ return if VALID_STRATEGIES.include?(strategy)
412
+
413
+ raise ArgumentError, "Invalid conflict strategy: #{strategy}. " \
414
+ "Valid strategies: #{VALID_STRATEGIES.join(', ')}"
415
+ end
416
+
417
+ def method_exists?(method_name)
418
+ method_defined?(method_name)
419
+ end
420
+
421
+ def handle_existing_method(method_name, strategy)
422
+ case strategy
423
+ when :raise
424
+ raise_method_exists_error(method_name)
425
+ when :skip
426
+ # Do nothing - skip the definition
427
+ when :warn
428
+ warn_method_exists(method_name)
429
+ yield
430
+ when :overwrite
431
+ remove_method(method_name)
432
+ yield
433
+ end
434
+ end
435
+
436
+ def raise_method_exists_error(method_name)
437
+ location = format_method_location(method_name)
438
+ raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}#{location}"
439
+ end
440
+
441
+ def warn_method_exists(method_name)
442
+ location = format_method_location(method_name)
443
+ caller_info = Familia.pretty_stack(skip: 5, limit: 3)
444
+
445
+ warn <<~WARNING
446
+
447
+ WARNING: Method '#{method_name}' is already defined.
448
+
449
+ Class: #{self}#{location}
450
+
451
+ Called from:
452
+ #{caller_info}
453
+
454
+ WARNING
455
+ end
456
+
457
+ def format_method_location(method_name)
458
+ method_obj = instance_method(method_name)
459
+ source_location = method_obj.source_location
460
+
461
+ return '' unless source_location
462
+
463
+ path = Familia.pretty_path(source_location[0])
464
+ line = source_location[1]
465
+ " (defined at #{path}:#{line})"
466
+ end
467
+ end
468
+ end
469
+ end