familia 2.0.0.pre10 → 2.0.0.pre12

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -12
  3. data/CLAUDE.md +4 -54
  4. data/Gemfile.lock +1 -1
  5. data/changelog.d/README.md +45 -34
  6. data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
  7. data/docs/archive/FAMILIA_UPDATE.md +1 -1
  8. data/docs/archive/README.md +15 -19
  9. data/docs/guides/Home.md +1 -1
  10. data/docs/guides/Implementation-Guide.md +1 -1
  11. data/docs/guides/relationships-methods.md +1 -1
  12. data/docs/migrating/.gitignore +2 -0
  13. data/docs/migrating/v2.0.0-pre.md +84 -0
  14. data/docs/migrating/v2.0.0-pre11.md +255 -0
  15. data/docs/migrating/v2.0.0-pre12.md +306 -0
  16. data/docs/migrating/v2.0.0-pre5.md +110 -0
  17. data/docs/migrating/v2.0.0-pre6.md +154 -0
  18. data/docs/migrating/v2.0.0-pre7.md +222 -0
  19. data/docs/overview.md +6 -7
  20. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  21. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  22. data/examples/{relationships_basic.rb → relationships.rb} +2 -3
  23. data/examples/safe_dump.rb +281 -0
  24. data/familia.gemspec +4 -4
  25. data/lib/familia/base.rb +52 -0
  26. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  27. data/lib/familia/errors.rb +2 -0
  28. data/lib/familia/features/autoloader.rb +57 -0
  29. data/lib/familia/features/external_identifier.rb +310 -0
  30. data/lib/familia/features/object_identifier.rb +307 -0
  31. data/lib/familia/features/safe_dump.rb +66 -72
  32. data/lib/familia/features.rb +93 -5
  33. data/lib/familia/horreum/subclass/definition.rb +47 -3
  34. data/lib/familia/secure_identifier.rb +51 -75
  35. data/lib/familia/verifiable_identifier.rb +162 -0
  36. data/lib/familia/version.rb +1 -1
  37. data/lib/familia.rb +1 -0
  38. data/setup.cfg +1 -8
  39. data/try/core/secure_identifier_try.rb +47 -18
  40. data/try/core/verifiable_identifier_try.rb +171 -0
  41. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  42. data/try/features/feature_improvements_try.rb +126 -0
  43. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  44. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  45. data/try/features/real_feature_integration_try.rb +7 -6
  46. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  47. data/try/helpers/test_helpers.rb +17 -17
  48. metadata +30 -22
  49. data/changelog.d/fragments/.keep +0 -0
  50. data/changelog.d/template.md.j2 +0 -29
  51. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  52. data/lib/familia/features/external_identifiers.rb +0 -111
  53. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  54. data/lib/familia/features/object_identifiers.rb +0 -194
@@ -0,0 +1,310 @@
1
+ # lib/familia/features/external_identifier.rb
2
+
3
+ module Familia
4
+ module Features
5
+ # Familia::Features::ExternalIdentifier
6
+ #
7
+ module ExternalIdentifier
8
+ def self.included(base)
9
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
10
+ base.extend ClassMethods
11
+
12
+ # Ensure default prefix is set in feature options
13
+ base.add_feature_options(:external_identifier, prefix: 'ext')
14
+
15
+ # Add class-level mapping for extid -> id lookups
16
+ base.class_hashkey :extid_lookup
17
+
18
+ # Register the extid field using our custom field type
19
+ base.register_field_type(ExternalIdentifierFieldType.new(:extid, as: :extid, fast_method: false))
20
+ end
21
+
22
+ # Error classes
23
+ class ExternalIdentifierError < FieldTypeError; end
24
+
25
+ # ExternalIdentifierFieldType - Fields that derive deterministic external identifiers
26
+ #
27
+ # External identifier fields derive shorter, public-facing identifiers that are
28
+ # deterministically derived from object identifiers. These IDs are safe for use
29
+ # in URLs, APIs, and other external contexts where shorter IDs are preferred.
30
+ #
31
+ # Key characteristics:
32
+ # - Deterministic generation from objid ensures consistency
33
+ # - Shorter than objid (128-bit vs 256-bit) for external use
34
+ # - Base-36 encoding for URL-safe identifiers
35
+ # - 'ext_' prefix for clear identification as external IDs
36
+ # - Lazy generation preserves values from initialization
37
+ #
38
+ # @example Using external identifier fields
39
+ # class User < Familia::Horreum
40
+ # feature :object_identifier
41
+ # feature :external_identifier
42
+ # field :email
43
+ # end
44
+ #
45
+ # user = User.new(email: 'user@example.com')
46
+ # user.objid # => "01234567-89ab-7def-8000-123456789abc"
47
+ # user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
48
+ #
49
+ # # Same objid always produces same extid
50
+ # user2 = User.new(objid: user.objid, email: 'user@example.com')
51
+ # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
52
+ #
53
+ class ExternalIdentifierFieldType < Familia::FieldType
54
+ # Override getter to provide lazy generation from objid
55
+ #
56
+ # Derives the external identifier deterministically from the object's
57
+ # objid. This ensures consistency - the same objid will always produce
58
+ # the same extid. Only derives when objid is available.
59
+ #
60
+ # @param klass [Class] The class to define the method on
61
+ #
62
+ def define_getter(klass)
63
+ field_name = @name
64
+ method_name = @method_name
65
+
66
+ handle_method_conflict(klass, method_name) do
67
+ klass.define_method method_name do
68
+ # Check if we already have a value (from initialization or previous generation)
69
+ existing_value = instance_variable_get(:"@#{field_name}")
70
+ return existing_value unless existing_value.nil?
71
+
72
+ # Derive external identifier from objid if available
73
+ derived_extid = derive_external_identifier
74
+ return unless derived_extid
75
+
76
+ instance_variable_set(:"@#{field_name}", derived_extid)
77
+
78
+ # Update mapping if we have an identifier
79
+ self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
80
+
81
+ derived_extid
82
+ end
83
+ end
84
+ end
85
+
86
+ # Override setter to preserve values during initialization
87
+ #
88
+ # This ensures that values passed during object initialization
89
+ # (e.g., when loading from Redis) are preserved and not overwritten
90
+ # by the lazy generation logic.
91
+ #
92
+ # @param klass [Class] The class to define the method on
93
+ #
94
+ def define_setter(klass)
95
+ field_name = @name
96
+ method_name = @method_name
97
+
98
+ handle_method_conflict(klass, :"#{method_name}=") do
99
+ klass.define_method :"#{method_name}=" do |value|
100
+ # Remove old mapping if extid is changing
101
+ old_value = instance_variable_get(:"@#{field_name}")
102
+ self.class.extid_lookup.del(old_value) if old_value && old_value != value && respond_to?(:identifier)
103
+
104
+ # Set the new value
105
+ instance_variable_set(:"@#{field_name}", value)
106
+
107
+ # Update mapping if we have both extid and identifier
108
+ return unless value && respond_to?(:identifier) && identifier
109
+
110
+ self.class.extid_lookup[value] = identifier
111
+ end
112
+ end
113
+ end
114
+
115
+ # External identifier fields are persisted to database
116
+ #
117
+ # @return [Boolean] true - external identifiers are always persisted
118
+ #
119
+ def persistent?
120
+ true
121
+ end
122
+
123
+ # Category for external identifier fields
124
+ #
125
+ # @return [Symbol] :external_identifier
126
+ #
127
+ def category
128
+ :external_identifier
129
+ end
130
+ end
131
+
132
+ # ExternalIdentifier::ClassMethods
133
+ #
134
+ module ClassMethods
135
+ # Find an object by its external identifier
136
+ #
137
+ # @param extid [String] The external identifier to search for
138
+ # @return [Object, nil] The object if found, nil otherwise
139
+ #
140
+ def find_by_extid(extid)
141
+ return nil if extid.to_s.empty?
142
+
143
+ if Familia.debug?
144
+ reference = caller(1..1).first
145
+ Familia.trace :FIND_BY_EXTID, Familia.dbclient, extid, reference
146
+ end
147
+
148
+ # Look up the primary ID from the external ID mapping
149
+ primary_id = extid_lookup[extid]
150
+ return nil if primary_id.nil?
151
+
152
+ # Find the object by its primary ID
153
+ find_by_id(primary_id)
154
+ rescue Familia::NotFound
155
+ # If the object was deleted but mapping wasn't cleaned up
156
+ extid_lookup.del(extid)
157
+ nil
158
+ end
159
+ end
160
+
161
+ # Derives a deterministic, public-facing external identifier from the object's
162
+ # internal `objid`.
163
+ #
164
+ # This method uses the `objid`'s high-quality randomness to seed a
165
+ # pseudorandom number generator (PRNG). The PRNG then acts as a complex,
166
+ # deterministic function to produce a new identifier that has no discernible
167
+ # mathematical correlation to the `objid`. This is a security measure to
168
+ # prevent leaking information (like timestamps from UUIDv7) from the internal
169
+ # identifier to the public one.
170
+ #
171
+ # The resulting identifier is always deterministic: the same `objid` will
172
+ # always produce the same `extid`, which is crucial for lookups.
173
+ #
174
+ # @return [String, nil] A prefixed, base36-encoded external identifier, or nil
175
+ # if the `objid` is not present.
176
+ # @raise [ExternalIdentifierError] if the `objid` provenance is unknown.
177
+ def derive_external_identifier
178
+ raise ExternalIdentifierError, 'Missing objid field' unless respond_to?(:objid)
179
+
180
+ current_objid = objid
181
+ return nil if current_objid.nil? || current_objid.to_s.empty?
182
+
183
+ # Validate objid provenance for security guarantees
184
+ validate_objid_provenance!
185
+
186
+ # Normalize the objid to a consistent hex representation first.
187
+ normalized_hex = normalize_objid_to_hex(current_objid)
188
+
189
+ # Use the objid's randomness to create a deterministic, yet secure,
190
+ # external identifier. We do not use SecureRandom here because the output
191
+ # must be deterministic.
192
+ #
193
+ # The process is as follows:
194
+ # 1. The objid (a high-entropy value) is hashed to create a uniform seed.
195
+ # 2. The seed initializes a standard PRNG (Random.new).
196
+ # 3. The PRNG acts as a deterministic function to generate a sequence of
197
+ # bytes that appears random, obscuring the original objid.
198
+
199
+ # 1. Create a high-quality, uniform seed from the objid's entropy.
200
+ seed = Digest::SHA256.digest(normalized_hex)
201
+
202
+ # 2. Initialize a PRNG with the seed. The same seed will always produce
203
+ # the same sequence of "random" numbers.
204
+ prng = Random.new(seed.unpack1('Q>'))
205
+
206
+ # 3. Generate 16 bytes (128 bits) of deterministic output.
207
+ random_bytes = prng.bytes(16)
208
+
209
+ # Encode as a base36 string for a compact, URL-safe identifier.
210
+ # 128 bits is approximately 25 characters in base36.
211
+ external_part = random_bytes.unpack1('H*').to_i(16).to_s(36).rjust(25, '0')
212
+
213
+ # Get prefix from feature options, default to "ext"
214
+ options = self.class.feature_options(:external_identifier)
215
+ prefix = options[:prefix] || 'ext'
216
+
217
+ "#{prefix}_#{external_part}"
218
+ end
219
+
220
+ # Full-length alias for extid for clarity when needed
221
+ #
222
+ # @return [String] The external identifier
223
+ #
224
+ def external_identifier
225
+ extid
226
+ end
227
+
228
+ # Full-length alias setter for extid
229
+ #
230
+ # @param value [String] The external identifier to set
231
+ #
232
+ def external_identifier=(value)
233
+ self.extid = value
234
+ end
235
+
236
+ def destroy!
237
+ # Clean up extid mapping when object is destroyed
238
+ current_extid = instance_variable_get(:@extid)
239
+ self.class.extid_lookup.del(current_extid) if current_extid
240
+
241
+ super if defined?(super)
242
+ end
243
+
244
+ private
245
+
246
+ # Validate that objid comes from a known secure ObjectIdentifier generator
247
+ #
248
+ # This ensures we only derive external identifiers from objid values that
249
+ # have known provenance and security properties. External identifiers derived
250
+ # from objid values of unknown origin cannot provide security guarantees.
251
+ #
252
+ # @raise [ExternalIdentifierError] if objid has unknown provenance
253
+ #
254
+ def validate_objid_provenance!
255
+ # Check if we have provenance information about the objid generator
256
+ generator_used = objid_generator_used
257
+
258
+ if generator_used.nil?
259
+ error_msg = <<~MSG.strip
260
+ Cannot derive external identifier: objid provenance unknown.
261
+ External identifiers can only be derived from objid values created
262
+ by the ObjectIdentifier feature to ensure security guarantees.
263
+ MSG
264
+ raise ExternalIdentifierError, error_msg
265
+ end
266
+
267
+ # Additional validation: ensure the ObjectIdentifier feature is active
268
+ return if self.class.features_enabled.include?(:object_identifier)
269
+
270
+ raise ExternalIdentifierError,
271
+ 'ExternalIdentifier requires ObjectIdentifier feature for secure provenance.'
272
+ end
273
+
274
+ # Normalize objid to hex format based on the known generator type
275
+ #
276
+ # Since we track which generator was used, we can safely normalize the objid
277
+ # to hex format without relying on string pattern matching. This eliminates
278
+ # the ambiguity between uuid7, uuid4, and hex formats.
279
+ #
280
+ # @param objid_value [String] The objid to normalize
281
+ # @return [String] Hex string suitable for SecureIdentifier processing
282
+ #
283
+ def normalize_objid_to_hex(objid_value)
284
+ generator_used = objid_generator_used
285
+
286
+ case generator_used
287
+ when :uuid_v7, :uuid_v4
288
+ # UUID formats: remove hyphens to get 128-bit hex string
289
+ objid_value.delete('-')
290
+ when :hex
291
+ # Already in hex format (256-bit)
292
+ objid_value
293
+ else
294
+ # Custom generator: attempt to normalize, but we can't guarantee format
295
+ normalized = objid_value.to_s.delete('-')
296
+ unless normalized.match?(/\A[0-9a-fA-F]+\z/)
297
+ error_msg = <<~MSG.strip
298
+ Cannot normalize objid from custom generator #{generator_used}:
299
+ value must be in hexadecimal format, got: #{objid_value}
300
+ MSG
301
+ raise ExternalIdentifierError, error_msg
302
+ end
303
+ normalized
304
+ end
305
+ end
306
+
307
+ Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
308
+ end
309
+ end
310
+ end
@@ -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