familia 2.0.0.pre10 → 2.0.0.pre13

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -3
  3. data/CHANGELOG.rst +507 -0
  4. data/CLAUDE.md +5 -55
  5. data/Gemfile +1 -6
  6. data/Gemfile.lock +13 -7
  7. data/changelog.d/README.md +45 -34
  8. data/changelog.d/scriv.ini +5 -0
  9. data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
  10. data/docs/archive/FAMILIA_UPDATE.md +1 -1
  11. data/docs/archive/README.md +15 -19
  12. data/docs/guides/Feature-System-Autoloading.md +228 -0
  13. data/docs/guides/Home.md +1 -1
  14. data/docs/guides/Implementation-Guide.md +1 -1
  15. data/docs/guides/relationships-methods.md +1 -1
  16. data/docs/guides/time-utilities.md +221 -0
  17. data/docs/migrating/.gitignore +2 -0
  18. data/docs/migrating/v2.0.0-pre.md +84 -0
  19. data/docs/migrating/v2.0.0-pre11.md +253 -0
  20. data/docs/migrating/v2.0.0-pre12.md +306 -0
  21. data/docs/migrating/v2.0.0-pre13.md +329 -0
  22. data/docs/migrating/v2.0.0-pre5.md +110 -0
  23. data/docs/migrating/v2.0.0-pre6.md +154 -0
  24. data/docs/migrating/v2.0.0-pre7.md +222 -0
  25. data/docs/overview.md +6 -7
  26. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  27. data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
  28. data/examples/autoloader/mega_customer.rb +17 -0
  29. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  30. data/examples/{relationships_basic.rb → relationships.rb} +2 -3
  31. data/examples/safe_dump.rb +281 -0
  32. data/familia.gemspec +5 -4
  33. data/lib/familia/autoloader.rb +53 -0
  34. data/lib/familia/base.rb +57 -0
  35. data/lib/familia/data_type.rb +4 -0
  36. data/lib/familia/encryption/encrypted_data.rb +4 -4
  37. data/lib/familia/encryption/manager.rb +6 -4
  38. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  39. data/lib/familia/encryption.rb +1 -1
  40. data/lib/familia/errors.rb +5 -0
  41. data/lib/familia/features/autoloadable.rb +113 -0
  42. data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
  43. data/lib/familia/features/expiration.rb +4 -0
  44. data/lib/familia/features/external_identifier.rb +310 -0
  45. data/lib/familia/features/object_identifier.rb +307 -0
  46. data/lib/familia/features/quantization.rb +5 -0
  47. data/lib/familia/features/safe_dump.rb +74 -73
  48. data/lib/familia/features.rb +109 -17
  49. data/lib/familia/field_type.rb +2 -0
  50. data/lib/familia/horreum/core/serialization.rb +3 -3
  51. data/lib/familia/horreum/subclass/definition.rb +50 -7
  52. data/lib/familia/horreum.rb +2 -0
  53. data/lib/familia/json_serializer.rb +70 -0
  54. data/lib/familia/logging.rb +12 -10
  55. data/lib/familia/refinements/logger_trace.rb +57 -0
  56. data/lib/familia/refinements/snake_case.rb +40 -0
  57. data/lib/familia/refinements/time_utils.rb +248 -0
  58. data/lib/familia/refinements.rb +3 -49
  59. data/lib/familia/secure_identifier.rb +51 -75
  60. data/lib/familia/utils.rb +2 -0
  61. data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
  62. data/lib/familia/validation.rb +1 -1
  63. data/lib/familia/verifiable_identifier.rb +162 -0
  64. data/lib/familia/version.rb +1 -1
  65. data/lib/familia.rb +15 -2
  66. data/try/core/autoloader_try.rb +112 -0
  67. data/try/core/extensions_try.rb +38 -21
  68. data/try/core/familia_extended_try.rb +4 -3
  69. data/try/core/secure_identifier_try.rb +47 -18
  70. data/try/core/time_utils_try.rb +130 -0
  71. data/try/core/verifiable_identifier_try.rb +171 -0
  72. data/try/data_types/datatype_base_try.rb +3 -2
  73. data/try/features/autoloadable/autoloadable_try.rb +61 -0
  74. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
  75. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
  76. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
  77. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  78. data/try/features/feature_improvements_try.rb +127 -0
  79. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  80. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  81. data/try/features/real_feature_integration_try.rb +8 -7
  82. data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
  83. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  84. data/try/helpers/test_helpers.rb +41 -17
  85. data/try/integration/cross_component_try.rb +3 -1
  86. metadata +61 -26
  87. data/CHANGELOG.md +0 -184
  88. data/changelog.d/fragments/.keep +0 -0
  89. data/changelog.d/template.md.j2 +0 -29
  90. data/lib/familia/core_ext.rb +0 -135
  91. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  92. data/lib/familia/features/external_identifiers.rb +0 -111
  93. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  94. data/lib/familia/features/object_identifiers.rb +0 -194
  95. data/setup.cfg +0 -12
@@ -0,0 +1,307 @@
1
+ # lib/familia/features/object_identifier.rb
2
+
3
+ module Familia
4
+ module Features
5
+ # ObjectIdentifier is a feature that provides unique object identifier management
6
+ # with configurable generation strategies. Object identifiers are crucial for
7
+ # distinguishing objects in distributed systems and providing stable references.
8
+ #
9
+ # Object identifiers are:
10
+ # - Unique across the system
11
+ # - Persistent (stored in Redis/Valkey)
12
+ # - Lazily generated (only when first accessed)
13
+ # - Configurable (multiple generation strategies available)
14
+ # - Preserved during initialization (existing IDs never regenerated)
15
+ #
16
+ # Generation Strategies:
17
+ # - :uuid_v7 (default) - UUID version 7 with embedded timestamp for sortability
18
+ # - :uuid_v4 - UUID version 4 for compatibility with legacy systems
19
+ # - :hex - High-entropy hexadecimal identifier using SecureIdentifier
20
+ # - Proc - Custom generation logic provided as a callable
21
+ #
22
+ # Example Usage:
23
+ #
24
+ # # Default UUID v7 generation
25
+ # class User < Familia::Horreum
26
+ # feature :object_identifier
27
+ # field :email
28
+ # end
29
+ #
30
+ # user = User.new(email: 'user@example.com')
31
+ # user.objid # => "01234567-89ab-7def-8000-123456789abc" (UUID v7)
32
+ #
33
+ # # UUID v4 for legacy compatibility
34
+ # class LegacyUser < Familia::Horreum
35
+ # feature :object_identifier, generator: :uuid_v4
36
+ # field :email
37
+ # end
38
+ #
39
+ # legacy = LegacyUser.new(email: 'legacy@example.com')
40
+ # legacy.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479" (UUID v4)
41
+ #
42
+ # # High-entropy hex for security-sensitive applications
43
+ # class SecureDocument < Familia::Horreum
44
+ # feature :object_identifier, generator: :hex
45
+ # field :title
46
+ # end
47
+ #
48
+ # doc = SecureDocument.new(title: 'Classified')
49
+ # doc.objid # => "a1b2c3d4e5f6..." (256-bit hex)
50
+ #
51
+ # # Custom generation strategy
52
+ # class TimestampedItem < Familia::Horreum
53
+ # feature :object_identifier, generator: -> { "item_#{Time.now.to_i}_#{SecureRandom.hex(4)}" }
54
+ # field :data
55
+ # end
56
+ #
57
+ # item = TimestampedItem.new(data: 'test')
58
+ # item.objid # => "item_1693857600_a1b2c3d4"
59
+ #
60
+ # Data Integrity Guarantees:
61
+ #
62
+ # The feature preserves the object identifier passed during initialization,
63
+ # ensuring that existing objects loaded from Redis maintain their IDs:
64
+ #
65
+ # # Loading existing object from Redis preserves ID
66
+ # existing = User.new(objid: 'existing-uuid-value', email: 'existing@example.com')
67
+ # existing.objid # => "existing-uuid-value" (preserved, not regenerated)
68
+ #
69
+ # Performance Characteristics:
70
+ #
71
+ # - Lazy Generation: IDs generated only when first accessed
72
+ # - Thread-Safe: Generator strategy configured once during initialization
73
+ # - Memory Efficient: No unnecessary ID generation for unused objects
74
+ # - Redis Efficient: Only persists non-nil values to conserve memory
75
+ #
76
+ # Security Considerations:
77
+ #
78
+ # - UUID v7 includes timestamp information (may leak timing data)
79
+ # - UUID v4 provides strong randomness without timing correlation
80
+ # - Hex generator provides maximum entropy (256 bits) for security-critical use cases
81
+ # - Custom generators allow domain-specific security requirements
82
+ #
83
+ module ObjectIdentifier
84
+ DEFAULT_GENERATOR = :uuid_v7
85
+
86
+ def self.included(base)
87
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
88
+ base.extend ClassMethods
89
+
90
+ # Ensure default generator is set in feature options
91
+ base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
92
+
93
+ # Register the objid field using a simple custom field type
94
+ base.register_field_type(ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false))
95
+ end
96
+
97
+ # ObjectIdentifierFieldType - Generate a unique object identifier
98
+ #
99
+ # Object identifier fields automatically generate unique identifiers when first
100
+ # accessed if not already set. The generation strategy is configurable via
101
+ # feature options. These fields preserve any values set during initialization
102
+ # to ensure data integrity when loading existing objects from Redis.
103
+ #
104
+ # The field type tracks the generator used for each objid to provide provenance
105
+ # information for security-sensitive operations like external identifier generation.
106
+ # This ensures that downstream features can validate the source and format of
107
+ # object identifiers without relying on string pattern matching, which cannot
108
+ # reliably distinguish between uuid7, uuid4, or hex formats in all cases.
109
+ #
110
+ # @example Using object identifier fields
111
+ # class User < Familia::Horreum
112
+ # feature :object_identifier, generator: :uuid_v7
113
+ # end
114
+ #
115
+ # user = User.new
116
+ # user.objid # Generates UUID v7 on first access
117
+ # user.objid_generator_used # => :uuid_v7
118
+ #
119
+ # # Loading existing object preserves ID but cannot determine original generator
120
+ # user2 = User.new(objid: "existing-uuid")
121
+ # user2.objid # Returns "existing-uuid", not regenerated
122
+ # user2.objid_generator_used # => nil (unknown provenance)
123
+ #
124
+ class ObjectIdentifierFieldType < Familia::FieldType
125
+ # Override getter to provide lazy generation with configured strategy
126
+ #
127
+ # Generates the identifier using the configured strategy if not already set.
128
+ # This preserves any values set during initialization while providing
129
+ # automatic generation for new objects.
130
+ #
131
+ # @param klass [Class] The class to define the method on
132
+ #
133
+ def define_getter(klass)
134
+ field_name = @name
135
+ method_name = @method_name
136
+
137
+ handle_method_conflict(klass, method_name) do
138
+ klass.define_method method_name do
139
+ # Check if we already have a value (from initialization or previous generation)
140
+ existing_value = instance_variable_get(:"@#{field_name}")
141
+ return existing_value unless existing_value.nil?
142
+
143
+ # Generate new identifier using configured strategy
144
+ generated_id = generate_object_identifier
145
+ instance_variable_set(:"@#{field_name}", generated_id)
146
+
147
+ # Track which generator was used for provenance
148
+ options = self.class.feature_options(:object_identifier)
149
+ generator = options[:generator] || DEFAULT_GENERATOR
150
+ instance_variable_set(:"@#{field_name}_generator_used", generator)
151
+
152
+ generated_id
153
+ end
154
+ end
155
+
156
+ # Define getter for generator provenance tracking
157
+ handle_method_conflict(klass, :"#{method_name}_generator_used") do
158
+ klass.define_method :"#{method_name}_generator_used" do
159
+ instance_variable_get(:"@#{field_name}_generator_used")
160
+ end
161
+ end
162
+ end
163
+
164
+ # Override setter to preserve values during initialization
165
+ #
166
+ # This ensures that values passed during object initialization
167
+ # (e.g., when loading from Redis) are preserved and not overwritten
168
+ # by the lazy generation logic.
169
+ #
170
+ # @param klass [Class] The class to define the method on
171
+ #
172
+ def define_setter(klass)
173
+ field_name = @name
174
+ method_name = @method_name
175
+
176
+ handle_method_conflict(klass, :"#{method_name}=") do
177
+ klass.define_method :"#{method_name}=" do |value|
178
+ instance_variable_set(:"@#{field_name}", value)
179
+
180
+ # When setting objid from external source (e.g., loading from Redis),
181
+ # we cannot determine the original generator, so we clear the provenance
182
+ # tracking to indicate unknown origin. This prevents false assumptions
183
+ # about the security properties of externally-provided identifiers.
184
+ instance_variable_set(:"@#{field_name}_generator_used", nil)
185
+ end
186
+ end
187
+ end
188
+
189
+ # Object identifier fields are persisted to database
190
+ #
191
+ # @return [Boolean] true - An object identifier is always persisted
192
+ #
193
+ def persistent?
194
+ true
195
+ end
196
+
197
+ # Category for object identifier fields
198
+ #
199
+ # @return [Symbol] :object_identifier
200
+ #
201
+ def category
202
+ :object_identifier
203
+ end
204
+ end
205
+
206
+ module ClassMethods
207
+ # Generate a new object identifier using the configured strategy
208
+ #
209
+ # @return [String] A new unique identifier
210
+ #
211
+ def generate_object_identifier
212
+ options = feature_options(:object_identifier)
213
+ generator = options[:generator] || DEFAULT_GENERATOR
214
+
215
+ case generator
216
+ when :uuid_v7
217
+ SecureRandom.uuid_v7
218
+ when :uuid_v4
219
+ SecureRandom.uuid_v4
220
+ when :hex
221
+ Familia.generate_hex_id
222
+ when Proc
223
+ generator.call
224
+ else
225
+ unless generator.respond_to?(:call)
226
+ raise Familia::Problem, "Invalid object identifier generator: #{generator.inspect}"
227
+ end
228
+
229
+ generator.call
230
+
231
+ end
232
+ end
233
+
234
+ # Find an object by its object identifier
235
+ #
236
+ # @param objid [String] The object identifier to search for
237
+ # @return [Object, nil] The object if found, nil otherwise
238
+ #
239
+ def find_by_objid(objid)
240
+ return nil if objid.to_s.empty?
241
+
242
+ if Familia.debug?
243
+ reference = caller(1..1).first
244
+ Familia.trace :FIND_BY_OBJID, Familia.dbclient, objid, reference
245
+ end
246
+
247
+ # Use the object identifier as the key for lookup
248
+ # This is a simple stub implementation - would need more sophisticated
249
+ # search logic in a real application
250
+ find_by_id(objid)
251
+ rescue Familia::NotFound
252
+ nil
253
+ end
254
+ end
255
+
256
+ # Instance method for generating object identifier using configured strategy
257
+ #
258
+ # This method is called by the ObjectIdentifierFieldType when lazy generation
259
+ # is needed. It uses the class-level generator configuration to create new IDs.
260
+ #
261
+ # @return [String] A newly generated unique identifier
262
+ # @private
263
+ #
264
+ def generate_object_identifier
265
+ self.class.generate_object_identifier
266
+ end
267
+
268
+ # Full-length alias for objid for clarity when needed
269
+ #
270
+ # @return [String] The object identifier
271
+ #
272
+ def object_identifier
273
+ objid
274
+ end
275
+
276
+ # Full-length alias setter for objid
277
+ #
278
+ # @param value [String] The object identifier to set
279
+ #
280
+ def object_identifier=(value)
281
+ self.objid = value
282
+ end
283
+
284
+ # Initialize object identifier configuration
285
+ #
286
+ # Called during object initialization to set up the ID generation strategy.
287
+ # This hook is called AFTER field initialization, ensuring that any objid
288
+ # values passed during construction are preserved.
289
+ #
290
+ def init
291
+ super if defined?(super)
292
+
293
+ # The generator strategy is configured at the class level via feature options.
294
+ # We don't need to store it per-instance since it's consistent for the class.
295
+ # The actual generation happens lazily in the getter when needed.
296
+
297
+ return unless Familia.debug?
298
+
299
+ options = self.class.feature_options(:object_identifier)
300
+ generator = options[:generator] || DEFAULT_GENERATOR
301
+ Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
302
+ end
303
+
304
+ Familia::Base.add_feature self, :object_identifier, depends_on: []
305
+ end
306
+ end
307
+ end
@@ -245,11 +245,16 @@ module Familia
245
245
  # NoDefault.qstamp() # Uses 10.minutes as fallback quantum
246
246
  #
247
247
  module Quantization
248
+
249
+ using Familia::Refinements::TimeUtils
250
+
248
251
  def self.included(base)
249
252
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
250
253
  base.extend ClassMethods
251
254
  end
252
255
 
256
+ # Familia::Quantization::ClassMethods
257
+ #
253
258
  module ClassMethods
254
259
  # Generates a quantized timestamp based on the given parameters
255
260
  #
@@ -1,12 +1,19 @@
1
1
  # lib/familia/features/safe_dump.rb
2
2
 
3
+
4
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
5
+ #
6
+ # Class instance variables are used here for feature configuration
7
+ # (e.g., @dump_method, @load_method). These are set once and not mutated
8
+ # at runtime, so thread safety is not a concern for this feature.
9
+ #
3
10
  module Familia::Features
4
11
  # SafeDump is a mixin that allows models to define a list of fields that are
5
12
  # safe to dump. This is useful for serializing objects to JSON or other
6
13
  # formats where you want to ensure that only certain fields are exposed.
7
14
  #
8
- # To use SafeDump, include it in your model and define a list of fields that
9
- # are safe to dump. The fields can be either symbols or hashes. If a field is
15
+ # To use SafeDump, include it in your model and use the DSL methods to define
16
+ # safe dump fields. The fields can be either symbols or hashes. If a field is
10
17
  # a symbol, the method with the same name will be called on the object to
11
18
  # retrieve the value. If the field is a hash, the key is the field name and
12
19
  # the value is a lambda that will be called with the object as an argument.
@@ -19,97 +26,91 @@ module Familia::Features
19
26
  #
20
27
  # feature :safe_dump
21
28
  #
22
- # @safe_dump_fields = [
23
- # :objid,
24
- # :updated,
25
- # :created,
26
- # { :active => ->(obj) { obj.active? } }
27
- # ]
28
- #
29
- # Internally, all fields are normalized to the hash syntax and stored in
30
- # @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
31
- # of symbols in the order they were defined. From the example above, it would
32
- # return `[:objid, :updated, :created, :active]`.
29
+ # safe_dump_field :objid
30
+ # safe_dump_field :updated
31
+ # safe_dump_field :created
32
+ # safe_dump_field :active, ->(obj) { obj.active? }
33
33
  #
34
- # Standalone Usage:
35
- #
36
- # You can also use SafeDump by including it in your model and defining the
37
- # safe dump fields using the class instance variable `@safe_dump_fields`.
38
- #
39
- # Example:
34
+ # Alternatively, you can define multiple fields at once:
40
35
  #
41
- # class MyModel
42
- # include Familia::Features::SafeDump
36
+ # safe_dump_fields :objid, :updated, :created,
37
+ # { active: ->(obj) { obj.active? } }
43
38
  #
44
- # @safe_dump_fields = [
45
- # :id, :name, { active: ->(obj) { obj.active? } }
46
- # ]
47
- # end
39
+ # Internally, all fields are normalized to the hash syntax and stored in
40
+ # @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
41
+ # of symbols in the order they were defined.
48
42
  #
49
43
  module SafeDump
44
+ include Familia::Features::Autoloadable
45
+ using Familia::Refinements::SnakeCase
46
+
50
47
  @dump_method = :to_json
51
48
  @load_method = :from_json
52
49
 
53
- @safe_dump_fields = []
54
- @safe_dump_field_map = {}
55
-
56
50
  def self.included(base)
57
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
58
- base.extend ClassMethods
59
-
60
- # Optionally define safe_dump_fields in the class to make
61
- # sure we always have an array to work with.
62
- base.instance_variable_set(:@safe_dump_fields, []) unless base.instance_variable_defined?(:@safe_dump_fields)
51
+ # Call the Autoloadable module's included method for post-inclusion setup
52
+ super
63
53
 
64
- # Ditto for the field map
65
- return if base.instance_variable_defined?(:@safe_dump_field_map)
54
+ Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
55
+ base.extend ClassMethods
66
56
 
57
+ # Initialize the safe dump field map
67
58
  base.instance_variable_set(:@safe_dump_field_map, {})
68
59
  end
69
60
 
61
+ # SafeDump::ClassMethods
62
+ #
63
+ # These methods become available on the model class
70
64
  module ClassMethods
71
- def set_safe_dump_fields(*fields)
72
- @safe_dump_fields = fields
65
+ # Define a single safe dump field
66
+ # @param field_name [Symbol] The name of the field
67
+ # @param callable [Proc, nil] Optional callable to transform the value
68
+ def safe_dump_field(field_name, callable = nil)
69
+ @safe_dump_field_map ||= {}
70
+
71
+ field_name = field_name.to_sym
72
+ field_value = callable || lambda { |obj|
73
+ if obj.respond_to?(:[]) && obj[field_name]
74
+ obj[field_name] # Familia::DataType classes
75
+ elsif obj.respond_to?(field_name)
76
+ obj.send(field_name) # Regular method calls
77
+ end
78
+ }
79
+
80
+ @safe_dump_field_map[field_name] = field_value
73
81
  end
74
82
 
75
- # `SafeDump.safe_dump_fields` returns only the list
76
- # of symbols in the order they were defined.
77
- def safe_dump_fields
78
- @safe_dump_fields.map do |field|
79
- field.is_a?(Symbol) ? field : field.keys.first
83
+ # Define multiple safe dump fields at once
84
+ # @param fields [Array] Mixed array of symbols and hashes
85
+ def safe_dump_fields(*fields)
86
+ # If no arguments, return field names (getter behavior)
87
+ return safe_dump_field_names if fields.empty?
88
+
89
+ # Otherwise, define fields (setter behavior)
90
+ fields.each do |field|
91
+ if field.is_a?(Symbol)
92
+ safe_dump_field(field)
93
+ elsif field.is_a?(Hash)
94
+ field.each do |name, callable|
95
+ safe_dump_field(name, callable)
96
+ end
97
+ end
80
98
  end
81
99
  end
82
100
 
83
- # `SafeDump.safe_dump_field_map` returns the field map
84
- # that is used to dump the fields. The keys are the
85
- # field names and the values are callables that will
86
- # expect to receive the instance object as an argument.
87
- #
88
- # The map is cached on the first call to this method.
89
- #
101
+ # Returns an array of safe dump field names in the order they were defined
102
+ def safe_dump_field_names
103
+ (@safe_dump_field_map || {}).keys
104
+ end
105
+
106
+ # Returns the field map used for dumping
90
107
  def safe_dump_field_map
91
- return @safe_dump_field_map if @safe_dump_field_map.any?
92
-
93
- # Operate directly on the @safe_dump_fields array to
94
- # build the map. This way we'll get the elements defined
95
- # in the hash syntax (i.e. since the safe_dump_fields getter
96
- # method returns only the symbols).
97
- @safe_dump_field_map = @safe_dump_fields.each_with_object({}) do |el, map|
98
- if el.is_a?(Symbol)
99
- field_name = el
100
- callable = lambda { |obj|
101
- if obj.respond_to?(:[]) && obj[field_name]
102
- obj[field_name] # Familia::DataType classes
103
- elsif obj.respond_to?(field_name)
104
- obj.send(field_name) # Onetime::Models::RedisHash classes via method_missing 😩
105
- end
106
- }
107
- else
108
- field_name = el.keys.first
109
- callable = el.values.first
110
- end
111
- map[field_name] = callable
112
- end
108
+ @safe_dump_field_map || {}
109
+ end
110
+
111
+ # Legacy method for setting safe dump fields (for backward compatibility)
112
+ def set_safe_dump_fields(*fields)
113
+ safe_dump_fields(*fields)
113
114
  end
114
115
  end
115
116
 
@@ -155,5 +156,5 @@ module Familia::Features
155
156
 
156
157
  Familia::Base.add_feature self, :safe_dump
157
158
  end
158
- # end SafeDump
159
159
  end
160
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
@@ -1,22 +1,117 @@
1
1
  # lib/familia/features.rb
2
2
 
3
+ # Load the Autoloader first, then use it to load all other features
4
+ require_relative 'autoloader'
5
+
3
6
  module Familia
4
7
  FeatureDefinition = Data.define(:name, :depends_on)
5
8
 
6
9
  # Familia::Features
7
10
  #
11
+ # This module provides the feature system for Familia classes. Features are
12
+ # modular capabilities that can be mixed into classes with configurable options.
13
+ # Features provide a powerful way to:
14
+ #
15
+ # - **Add new methods**: Both class and instance methods can be added
16
+ # - **Override existing methods**: Extend or replace default behavior
17
+ # - **Add new fields**: Define additional data storage capabilities
18
+ # - **Manage complexity**: Large, complex model classes can use features to
19
+ # organize functionality into focused, reusable modules
20
+ #
21
+ # ## Feature Options Storage
22
+ #
23
+ # Feature options are stored **per-class** using class-level instance variables.
24
+ # This means each Familia::Horreum subclass maintains its own isolated set of
25
+ # feature options. When you enable a feature with options in different models,
26
+ # each model stores its own separate configuration without interference.
27
+ #
28
+ # ## Project Organization with Autoloadable
29
+ #
30
+ # For large projects, use {Familia::Features::Autoloadable} to automatically load
31
+ # project-specific features from a dedicated directory structure. This helps
32
+ # organize complex models by separating features into individual files.
33
+ #
34
+ # @example Different models with different feature options
35
+ # class UserModel < Familia::Horreum
36
+ # feature :object_identifier, generator: :uuid_v4
37
+ # end
38
+ #
39
+ # class SessionModel < Familia::Horreum
40
+ # feature :object_identifier, generator: :hex
41
+ # end
42
+ #
43
+ # UserModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
44
+ # SessionModel.feature_options(:object_identifier) #=> {generator: :hex}
45
+ #
46
+ # @example Using features for complexity management
47
+ # class ComplexModel < Familia::Horreum
48
+ # # Organize functionality using features
49
+ # feature :expiration # TTL management
50
+ # feature :safe_dump # API-safe serialization
51
+ # feature :relationships # CRUD operations for related objects
52
+ # feature :custom_validation # Project-specific validation logic
53
+ # feature :audit_trail # Change tracking
54
+ # end
55
+ #
56
+ # @example Project-specific features with autoloader
57
+ # # In your model file: app/models/customer.rb
58
+ # class Customer < Familia::Horreum
59
+ # module Features
60
+ # include Familia::Features::Autoloadable
61
+ # # Automatically loads all .rb files from app/models/customer/features/
62
+ # end
63
+ # end
64
+ #
65
+ # @see Familia::Features::Autoloadable For automatic feature loading
66
+ #
8
67
  module Features
68
+ include Familia::Autoloader
69
+
9
70
  @features_enabled = nil
10
71
  attr_reader :features_enabled
11
72
 
73
+ # Enables a feature for the current class with optional configuration.
74
+ #
75
+ # Features are modular capabilities that can be mixed into Familia::Horreum
76
+ # classes. Each feature can be configured with options that are stored
77
+ # **per-class**, ensuring complete isolation between different models.
78
+ #
79
+ # @param feature_name [Symbol, String, nil] the name of the feature to enable.
80
+ # If nil, returns the list of currently enabled features.
81
+ # @param options [Hash] configuration options for the feature. These are
82
+ # stored per-class and do not interfere with other models' configurations.
83
+ # @return [Array, nil] the list of enabled features if feature_name is nil,
84
+ # otherwise nil
85
+ #
86
+ # @example Enable feature without options
87
+ # class User < Familia::Horreum
88
+ # feature :expiration
89
+ # end
90
+ #
91
+ # @example Enable feature with options (per-class storage)
92
+ # class User < Familia::Horreum
93
+ # feature :object_identifier, generator: :uuid_v4
94
+ # end
95
+ #
96
+ # class Session < Familia::Horreum
97
+ # feature :object_identifier, generator: :hex # Different options
98
+ # end
99
+ #
100
+ # # Each class maintains separate options:
101
+ # User.feature_options(:object_identifier) #=> {generator: :uuid_v4}
102
+ # Session.feature_options(:object_identifier) #=> {generator: :hex}
103
+ #
104
+ # @raise [Familia::Problem] if the feature is not supported
105
+ #
12
106
  def feature(feature_name = nil, **options)
13
107
  @features_enabled ||= []
14
108
 
15
109
  return features_enabled if feature_name.nil?
16
110
 
17
- # If there's a value provied check that it's a valid feature
111
+ # If there's a value provided check that it's a valid feature
18
112
  feature_name = feature_name.to_sym
19
- unless Familia::Base.features_available.key?(feature_name)
113
+ feature_class = Familia::Base.find_feature(feature_name, self)
114
+ unless feature_class
20
115
  raise Familia::Problem, "Unsupported feature: #{feature_name}"
21
116
  end
22
117
 
@@ -41,15 +136,22 @@ module Familia
41
136
  # Add it to the list available features_enabled for Familia::Base classes.
42
137
  features_enabled << feature_name
43
138
 
44
- # Store feature options if any were provided using the new pattern
45
- if options.any?
139
+ # Always capture and store the calling location for every feature
140
+ calling_location = caller_locations(1, 1)&.first
141
+ options[:calling_location] = calling_location&.path
142
+
143
+ # Add feature options if the class supports them (Horreum classes)
144
+ if respond_to?(:add_feature_options)
46
145
  add_feature_options(feature_name, **options)
47
146
  end
48
147
 
49
- klass = Familia::Base.features_available[feature_name]
50
-
51
148
  # Extend the Familia::Base subclass (e.g. Customer) with the feature module
52
- include klass
149
+ include feature_class
150
+
151
+ # Trigger post-inclusion autoloading for features that support it
152
+ if feature_class.respond_to?(:post_inclusion_autoload)
153
+ feature_class.post_inclusion_autoload(self, feature_name, options)
154
+ end
53
155
 
54
156
  # NOTE: Do we want to extend Familia::DataType here? That would make it
55
157
  # possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
@@ -64,13 +166,3 @@ module Familia
64
166
  end
65
167
  end
66
168
  end
67
-
68
- # Load all feature files from the features directory
69
- features_dir = File.join(__dir__, 'features')
70
- Familia.ld "[DEBUG] Loading features from #{features_dir}"
71
- if Dir.exist?(features_dir)
72
- Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
73
- Familia.ld "[DEBUG] Loading feature #{feature_file}"
74
- require_relative feature_file
75
- end
76
- end
@@ -29,6 +29,8 @@ module Familia
29
29
  class FieldType
30
30
  attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict, :loggable
31
31
 
32
+ using Familia::Refinements::TimeUtils
33
+
32
34
  # Initialize a new field type
33
35
  #
34
36
  # @param name [Symbol] The field name