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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +507 -0
- data/CLAUDE.md +5 -55
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/changelog.d/README.md +45 -34
- data/changelog.d/scriv.ini +5 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
- data/docs/archive/FAMILIA_UPDATE.md +1 -1
- data/docs/archive/README.md +15 -19
- data/docs/guides/Feature-System-Autoloading.md +228 -0
- data/docs/guides/Home.md +1 -1
- data/docs/guides/Implementation-Guide.md +1 -1
- data/docs/guides/relationships-methods.md +1 -1
- data/docs/guides/time-utilities.md +221 -0
- data/docs/migrating/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre.md +84 -0
- data/docs/migrating/v2.0.0-pre11.md +253 -0
- data/docs/migrating/v2.0.0-pre12.md +306 -0
- data/docs/migrating/v2.0.0-pre13.md +329 -0
- data/docs/migrating/v2.0.0-pre5.md +110 -0
- data/docs/migrating/v2.0.0-pre6.md +154 -0
- data/docs/migrating/v2.0.0-pre7.md +222 -0
- data/docs/overview.md +6 -7
- data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
- data/examples/{relationships_basic.rb → relationships.rb} +2 -3
- data/examples/safe_dump.rb +281 -0
- data/familia.gemspec +5 -4
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +57 -0
- data/lib/familia/data_type.rb +4 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -4
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +5 -0
- data/lib/familia/features/autoloadable.rb +113 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
- data/lib/familia/features/expiration.rb +4 -0
- data/lib/familia/features/external_identifier.rb +310 -0
- data/lib/familia/features/object_identifier.rb +307 -0
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +74 -73
- data/lib/familia/features.rb +109 -17
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +50 -7
- data/lib/familia/horreum.rb +2 -0
- data/lib/familia/json_serializer.rb +70 -0
- data/lib/familia/logging.rb +12 -10
- data/lib/familia/refinements/logger_trace.rb +57 -0
- data/lib/familia/refinements/snake_case.rb +40 -0
- data/lib/familia/refinements/time_utils.rb +248 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/secure_identifier.rb +51 -75
- data/lib/familia/utils.rb +2 -0
- data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
- data/lib/familia/validation.rb +1 -1
- data/lib/familia/verifiable_identifier.rb +162 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -2
- data/try/core/autoloader_try.rb +112 -0
- data/try/core/extensions_try.rb +38 -21
- data/try/core/familia_extended_try.rb +4 -3
- data/try/core/secure_identifier_try.rb +47 -18
- data/try/core/time_utils_try.rb +130 -0
- data/try/core/verifiable_identifier_try.rb +171 -0
- data/try/data_types/datatype_base_try.rb +3 -2
- data/try/features/autoloadable/autoloadable_try.rb +61 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
- data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
- data/try/features/feature_improvements_try.rb +127 -0
- data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
- data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
- data/try/features/real_feature_integration_try.rb +8 -7
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/features/safe_dump/safe_dump_try.rb +8 -9
- data/try/helpers/test_helpers.rb +41 -17
- data/try/integration/cross_component_try.rb +3 -1
- metadata +61 -26
- data/CHANGELOG.md +0 -184
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +0 -29
- data/lib/familia/core_ext.rb +0 -135
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
- data/lib/familia/features/external_identifiers.rb +0 -111
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
- data/lib/familia/features/object_identifiers.rb +0 -194
- 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
|
9
|
-
#
|
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
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
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
|
-
#
|
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
|
-
#
|
42
|
-
#
|
36
|
+
# safe_dump_fields :objid, :updated, :created,
|
37
|
+
# { active: ->(obj) { obj.active? } }
|
43
38
|
#
|
44
|
-
#
|
45
|
-
#
|
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
|
-
|
58
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
72
|
-
|
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
|
-
#
|
76
|
-
#
|
77
|
-
def safe_dump_fields
|
78
|
-
|
79
|
-
|
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
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
data/lib/familia/features.rb
CHANGED
@@ -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
|
111
|
+
# If there's a value provided check that it's a valid feature
|
18
112
|
feature_name = feature_name.to_sym
|
19
|
-
|
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
|
-
#
|
45
|
-
|
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
|
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
|
data/lib/familia/field_type.rb
CHANGED