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