familia 2.0.0.pre7 → 2.0.0.pre8

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +3 -3
  4. data/README.md +35 -0
  5. data/docs/wiki/Feature-System-Guide.md +0 -15
  6. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
  7. data/lib/familia/features/external_identifiers.rb +111 -0
  8. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
  9. data/lib/familia/features/object_identifiers.rb +194 -0
  10. data/lib/familia/features/relationships/cascading.rb +0 -1
  11. data/lib/familia/features/relationships/indexing.rb +0 -1
  12. data/lib/familia/features/relationships/membership.rb +0 -1
  13. data/lib/familia/features/relationships/querying.rb +7 -12
  14. data/lib/familia/features/relationships/score_encoding.rb +1 -3
  15. data/lib/familia/features/relationships/tracking.rb +0 -1
  16. data/lib/familia/features/transient_fields.rb +8 -10
  17. data/lib/familia/features.rb +16 -13
  18. data/lib/familia/horreum/core/serialization.rb +2 -5
  19. data/lib/familia/horreum/subclass/definition.rb +34 -0
  20. data/lib/familia/version.rb +1 -3
  21. data/try/core/errors_try.rb +1 -1
  22. data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
  23. data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
  24. data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
  25. data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
  26. data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
  27. data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
  28. data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
  29. data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
  30. data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
  31. data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
  32. data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
  33. data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
  34. data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
  35. data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
  36. data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
  37. data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +1 -1
  38. data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
  39. data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
  40. data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
  41. data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
  42. metadata +38 -31
  43. /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
  44. /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
  45. /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
  46. /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
  47. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
  48. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
  49. /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
  50. /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
  51. /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
  52. /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
  53. /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
  54. /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
  55. /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a93de51c8a35c420067d9e48895af37862866667af224a8e13f44647c4e4c76
4
- data.tar.gz: bbdf64f77c182a0498fc757c393071b04b5da3594b4c7d5c12992fe058b44777
3
+ metadata.gz: c5ab18b134425a370c5d40f464e87747e85e713dc44c2a7906b7b024a66b23a4
4
+ data.tar.gz: f5a2e3fd2ca4553781b7f58f4f0e7a6d5e9228c18f34c7a1f5cf1af7695d580d
5
5
  SHA512:
6
- metadata.gz: 59a39a481db42739bbebabab9211645b2f7e9eb605742ab936c0b65dfc32973931b1b1f8a1c5d725fed5f5eb0ebdbb87c1c405f87108390ad3b6eb8f5ca89c5a
7
- data.tar.gz: 4dc7dededb21bd7ee988a4d260e30eceb43f19be4c3f5d99f87bac5d632af6e517af5eab2633758e0c6b1552a2635bd0e024ace2210ee1ee83362aa811458b8c
6
+ metadata.gz: fba2c7586cb19181461f90ef2718c767de34f034d51ebc23c1643c8b526cc9c28fe64a2c3e3e46857bc1d816c8e9db9f76a6b57510f9b918756558b5e09c6fc9
7
+ data.tar.gz: 2f98337dbe62a833b310e9ddcd5916a7971e2b551a0603dbb153fa3ba5c99a81e9dfe86270a0d0500b359625ea23efbebc58ab26b327a03fc5138ee643475472
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ group :test do
9
9
  gem 'tryouts', path: '../tryouts'
10
10
  gem 'uri-valkey', path: '..//uri-valkey/gems', glob: 'uri-valkey.gemspec'
11
11
  else
12
- gem 'tryouts', '~> 3.5.1', require: false
12
+ gem 'tryouts', '~> 3.5.2', require: false
13
13
  end
14
14
  gem 'concurrent-ruby', '~> 1.3.5', require: false
15
15
  gem 'ruby-prof'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre6)
4
+ familia (2.0.0.pre8)
5
5
  benchmark
6
6
  connection_pool
7
7
  csv
@@ -113,7 +113,7 @@ GEM
113
113
  ruby-progressbar (1.13.0)
114
114
  stackprof (0.2.27)
115
115
  stringio (3.1.7)
116
- tryouts (3.5.1)
116
+ tryouts (3.5.2)
117
117
  concurrent-ruby (~> 1.0)
118
118
  irb
119
119
  minitest (~> 5.0)
@@ -148,7 +148,7 @@ DEPENDENCIES
148
148
  rubocop-thread_safety
149
149
  ruby-prof
150
150
  stackprof
151
- tryouts (~> 3.5.1)
151
+ tryouts (~> 3.5.2)
152
152
  yard (~> 0.9)
153
153
 
154
154
  BUNDLED WITH
data/README.md CHANGED
@@ -118,6 +118,41 @@ user.transaction do |conn|
118
118
  end
119
119
  ```
120
120
 
121
+ ## Organizing Complex Models
122
+
123
+ For large applications, you can organize model complexity using custom features:
124
+
125
+ ### Self-Registering Features
126
+
127
+ ```ruby
128
+ # app/features/customer_management.rb
129
+ module MyApp::Features::CustomerManagement
130
+ Familia::Base.add_feature(self, :customer_management)
131
+
132
+ def self.included(base)
133
+ base.extend(ClassMethods)
134
+ end
135
+
136
+ module ClassMethods
137
+ def create_with_validation(attrs)
138
+ # Complex creation logic
139
+ end
140
+ end
141
+
142
+ def complex_business_method
143
+ # Instance methods
144
+ end
145
+ end
146
+
147
+ # models/customer.rb
148
+ class Customer < Familia::Horreum
149
+ field :email, :name
150
+ feature :customer_management # Clean model definition
151
+ end
152
+ ```
153
+
154
+ This keeps complex models organized while maintaining Familia's clean, declarative style.
155
+
121
156
  ## Conclusion
122
157
 
123
158
  Familia provides a powerful and flexible way to work with Valkey-compatible in Ruby applications. Its features like automatic expiration, safe dumping, and quantization make it suitable for a wide range of use cases, from simple key-value storage to complex time-series data management.
@@ -517,21 +517,6 @@ class ConfigurableModel < Familia::Horreum
517
517
  end
518
518
  ```
519
519
 
520
- ### Runtime Feature Checking
521
-
522
- ```ruby
523
- class Model < Familia::Horreum
524
- feature :expiration
525
-
526
- def available_features
527
- self.class.features_enabled
528
- end
529
- end
530
-
531
- model = Model.new
532
- model.available_features # => [:expiration]
533
- ```
534
-
535
520
  ## Testing Features
536
521
 
537
522
  ### Feature Testing
@@ -0,0 +1,120 @@
1
+ # lib/familia/features/external_identifiers/external_identifier_field_type.rb
2
+
3
+ require 'familia/field_type'
4
+
5
+ module Familia
6
+ module Features
7
+ module ExternalIdentifiers
8
+ # ExternalIdentifierFieldType - Fields that generate deterministic external identifiers
9
+ #
10
+ # External identifier fields generate shorter, public-facing identifiers that are
11
+ # deterministically derived from object identifiers. These IDs are safe for use
12
+ # in URLs, APIs, and other external contexts where shorter IDs are preferred.
13
+ #
14
+ # Key characteristics:
15
+ # - Deterministic generation from objid ensures consistency
16
+ # - Shorter than objid (128-bit vs 256-bit) for external use
17
+ # - Base-36 encoding for URL-safe identifiers
18
+ # - 'ext_' prefix for clear identification as external IDs
19
+ # - Lazy generation preserves values from initialization
20
+ #
21
+ # @example Using external identifier fields
22
+ # class User < Familia::Horreum
23
+ # feature :object_identifiers
24
+ # feature :external_identifiers
25
+ # field :email
26
+ # end
27
+ #
28
+ # user = User.new(email: 'user@example.com')
29
+ # user.objid # => "01234567-89ab-7def-8000-123456789abc"
30
+ # user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
31
+ #
32
+ # # Same objid always produces same extid
33
+ # user2 = User.new(objid: user.objid, email: 'user@example.com')
34
+ # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
35
+ #
36
+ class ExternalIdentifierFieldType < FieldType
37
+ # Override getter to provide lazy generation from objid
38
+ #
39
+ # Generates the external identifier deterministically from the object's
40
+ # objid. This ensures consistency - the same objid will always produce
41
+ # the same extid. Only generates when objid is available.
42
+ #
43
+ # @param klass [Class] The class to define the method on
44
+ #
45
+ def define_getter(klass)
46
+ field_name = @name
47
+ method_name = @method_name
48
+
49
+ handle_method_conflict(klass, method_name) do
50
+ klass.define_method method_name do
51
+ # Check if we already have a value (from initialization or previous generation)
52
+ existing_value = instance_variable_get(:"@#{field_name}")
53
+ return existing_value unless existing_value.nil?
54
+
55
+ # Generate external identifier from objid if available
56
+ generated_extid = generate_external_identifier
57
+ return unless generated_extid
58
+
59
+ instance_variable_set(:"@#{field_name}", generated_extid)
60
+
61
+ # Update mapping if we have an identifier
62
+ if respond_to?(:identifier) && identifier
63
+ self.class.extid_lookup[generated_extid] = identifier
64
+ end
65
+
66
+ generated_extid
67
+ end
68
+ end
69
+ end
70
+
71
+ # Override setter to preserve values during initialization
72
+ #
73
+ # This ensures that values passed during object initialization
74
+ # (e.g., when loading from Redis) are preserved and not overwritten
75
+ # by the lazy generation logic.
76
+ #
77
+ # @param klass [Class] The class to define the method on
78
+ #
79
+ def define_setter(klass)
80
+ field_name = @name
81
+ method_name = @method_name
82
+
83
+ handle_method_conflict(klass, :"#{method_name}=") do
84
+ klass.define_method :"#{method_name}=" do |value|
85
+ # Remove old mapping if extid is changing
86
+ old_value = instance_variable_get(:"@#{field_name}")
87
+ if old_value && old_value != value && respond_to?(:identifier)
88
+ self.class.extid_lookup.del(old_value)
89
+ end
90
+
91
+ # Set the new value
92
+ instance_variable_set(:"@#{field_name}", value)
93
+
94
+ # Update mapping if we have both extid and identifier
95
+ if value && respond_to?(:identifier) && identifier
96
+ self.class.extid_lookup[value] = identifier
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # External identifier fields are persisted to database
103
+ #
104
+ # @return [Boolean] true - external identifiers are always persisted
105
+ #
106
+ def persistent?
107
+ true
108
+ end
109
+
110
+ # Category for external identifier fields
111
+ #
112
+ # @return [Symbol] :external_identifier
113
+ #
114
+ def category
115
+ :external_identifier
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,111 @@
1
+ # lib/familia/features/external_identifiers.rb
2
+
3
+ require_relative 'external_identifiers/external_identifier_field_type'
4
+
5
+ module Familia
6
+ module Features
7
+
8
+ # Familia::Features::ExternalIdentifiers
9
+ #
10
+ module ExternalIdentifiers
11
+ def self.included(base)
12
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
13
+ base.extend ClassMethods
14
+
15
+ # Ensure default prefix is set in feature options
16
+ base.add_feature_options(:external_identifiers, prefix: 'ext')
17
+
18
+ # Add class-level mapping for extid -> id lookups
19
+ base.class_hashkey :extid_lookup
20
+
21
+ # Register the extid field using our custom field type
22
+ base.register_field_type(
23
+ ExternalIdentifiers::ExternalIdentifierFieldType.new(:extid, as: :extid, fast_method: false)
24
+ )
25
+ end
26
+
27
+ # ExternalIdentifiers::ClassMethods
28
+ #
29
+ module ClassMethods
30
+ def generate_extid(objid = nil)
31
+ unless features_enabled.include?(:object_identifiers)
32
+ raise Familia::Problem,
33
+ 'ExternalIdentifiers requires ObjectIdentifiers feature'
34
+ end
35
+ return nil if objid.to_s.empty?
36
+
37
+ objid_hex = objid.to_s.delete('-')
38
+ external_part = Familia.shorten_to_external_id(objid_hex, base: 36)
39
+ prefix = feature_options(:external_identifiers)[:prefix] || 'ext'
40
+ "#{prefix}_#{external_part}"
41
+ end
42
+
43
+ # Find an object by its external identifier
44
+ #
45
+ # @param extid [String] The external identifier to search for
46
+ # @return [Object, nil] The object if found, nil otherwise
47
+ #
48
+ def find_by_extid(extid)
49
+ return nil if extid.to_s.empty?
50
+
51
+ if Familia.debug?
52
+ reference = caller(1..1).first
53
+ Familia.trace :FIND_BY_EXTID, Familia.dbclient, extid, reference
54
+ end
55
+
56
+ # Look up the primary ID from the external ID mapping
57
+ primary_id = extid_lookup[extid]
58
+ return nil if primary_id.nil?
59
+
60
+ # Find the object by its primary ID
61
+ find_by_id(primary_id)
62
+ rescue Familia::NotFound
63
+ # If the object was deleted but mapping wasn't cleaned up
64
+ extid_lookup.del(extid)
65
+ nil
66
+ end
67
+ end
68
+
69
+ # Generate external identifier deterministically from objid
70
+ def generate_external_identifier
71
+ return nil unless respond_to?(:objid)
72
+
73
+ current_objid = objid
74
+ return nil if current_objid.nil? || current_objid.to_s.empty?
75
+
76
+ # Convert objid to hex string for processing
77
+ objid_hex = current_objid.delete('-') # Remove UUID hyphens if present
78
+
79
+ # Generate deterministic external ID using SecureIdentifier
80
+ external_part = Familia.shorten_to_external_id(objid_hex, base: 36)
81
+
82
+ # Get prefix from feature options, default to "ext"
83
+ options = self.class.feature_options(:external_identifiers)
84
+ prefix = options[:prefix] || 'ext'
85
+
86
+ "#{prefix}_#{external_part}"
87
+ end
88
+
89
+ def external_identifier
90
+ extid
91
+ end
92
+
93
+ def init
94
+ super if defined?(super)
95
+ # External IDs are generated from objid, so no additional setup needed
96
+ end
97
+
98
+ def destroy!
99
+ # Clean up extid mapping when object is destroyed
100
+ current_extid = instance_variable_get(:@extid)
101
+ if current_extid
102
+ self.class.extid_lookup.del(current_extid)
103
+ end
104
+
105
+ super if defined?(super)
106
+ end
107
+
108
+ Familia::Base.add_feature self, :external_identifiers, depends_on: [:object_identifiers]
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,91 @@
1
+ # lib/familia/features/object_identifiers/object_identifier_field_type.rb
2
+
3
+ require 'familia/field_type'
4
+
5
+ module Familia
6
+ module Features
7
+ module ObjectIdentifiers
8
+ # ObjectIdentifierFieldType - Fields that generate unique object identifiers
9
+ #
10
+ # Object identifier fields automatically generate unique identifiers when first
11
+ # accessed if not already set. The generation strategy is configurable via
12
+ # feature options. These fields preserve any values set during initialization
13
+ # to ensure data integrity when loading existing objects from Redis.
14
+ #
15
+ # @example Using object identifier fields
16
+ # class User < Familia::Horreum
17
+ # feature :object_identifiers, generator: :uuid_v7
18
+ # end
19
+ #
20
+ # user = User.new
21
+ # user.objid # Generates UUID v7 on first access
22
+ #
23
+ # # Loading existing object preserves ID
24
+ # user2 = User.new(objid: "existing-uuid")
25
+ # user2.objid # Returns "existing-uuid", not regenerated
26
+ #
27
+ class ObjectIdentifierFieldType < FieldType
28
+ # Override getter to provide lazy generation with configured strategy
29
+ #
30
+ # Generates the identifier using the configured strategy if not already set.
31
+ # This preserves any values set during initialization while providing
32
+ # automatic generation for new objects.
33
+ #
34
+ # @param klass [Class] The class to define the method on
35
+ #
36
+ def define_getter(klass)
37
+ field_name = @name
38
+ method_name = @method_name
39
+
40
+ handle_method_conflict(klass, method_name) do
41
+ klass.define_method method_name do
42
+ # Check if we already have a value (from initialization or previous generation)
43
+ existing_value = instance_variable_get(:"@#{field_name}")
44
+ return existing_value unless existing_value.nil?
45
+
46
+ # Generate new identifier using configured strategy
47
+ generated_id = generate_object_identifier
48
+ instance_variable_set(:"@#{field_name}", generated_id)
49
+ generated_id
50
+ end
51
+ end
52
+ end
53
+
54
+ # Override setter to preserve values during initialization
55
+ #
56
+ # This ensures that values passed during object initialization
57
+ # (e.g., when loading from Redis) are preserved and not overwritten
58
+ # by the lazy generation logic.
59
+ #
60
+ # @param klass [Class] The class to define the method on
61
+ #
62
+ def define_setter(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 |value|
68
+ instance_variable_set(:"@#{field_name}", value)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Object identifier fields are persisted to database
74
+ #
75
+ # @return [Boolean] true - object identifiers are always persisted
76
+ #
77
+ def persistent?
78
+ true
79
+ end
80
+
81
+ # Category for object identifier fields
82
+ #
83
+ # @return [Symbol] :object_identifier
84
+ #
85
+ def category
86
+ :object_identifier
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,194 @@
1
+ # lib/familia/features/object_identifiers.rb
2
+
3
+ require_relative 'object_identifiers/object_identifier_field_type'
4
+
5
+ module Familia
6
+ module Features
7
+ # ObjectIdentifiers is a feature that provides unique object identifier management
8
+ # with configurable generation strategies. Object identifiers are crucial for
9
+ # distinguishing objects in distributed systems and providing stable references.
10
+ #
11
+ # Object identifiers are:
12
+ # - Unique across the system
13
+ # - Persistent (stored in Redis/Valkey)
14
+ # - Lazily generated (only when first accessed)
15
+ # - Configurable (multiple generation strategies available)
16
+ # - Preserved during initialization (existing IDs never regenerated)
17
+ #
18
+ # Generation Strategies:
19
+ # - :uuid_v7 (default) - UUID version 7 with embedded timestamp for sortability
20
+ # - :uuid_v4 - UUID version 4 for compatibility with legacy systems
21
+ # - :hex - High-entropy hexadecimal identifier using SecureIdentifier
22
+ # - Proc - Custom generation logic provided as a callable
23
+ #
24
+ # Example Usage:
25
+ #
26
+ # # Default UUID v7 generation
27
+ # class User < Familia::Horreum
28
+ # feature :object_identifiers
29
+ # field :email
30
+ # end
31
+ #
32
+ # user = User.new(email: 'user@example.com')
33
+ # user.objid # => "01234567-89ab-7def-8000-123456789abc" (UUID v7)
34
+ #
35
+ # # UUID v4 for legacy compatibility
36
+ # class LegacyUser < Familia::Horreum
37
+ # feature :object_identifiers, generator: :uuid_v4
38
+ # field :email
39
+ # end
40
+ #
41
+ # legacy = LegacyUser.new(email: 'legacy@example.com')
42
+ # legacy.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479" (UUID v4)
43
+ #
44
+ # # High-entropy hex for security-sensitive applications
45
+ # class SecureDocument < Familia::Horreum
46
+ # feature :object_identifiers, generator: :hex
47
+ # field :title
48
+ # end
49
+ #
50
+ # doc = SecureDocument.new(title: 'Classified')
51
+ # doc.objid # => "a1b2c3d4e5f6..." (256-bit hex)
52
+ #
53
+ # # Custom generation strategy
54
+ # class TimestampedItem < Familia::Horreum
55
+ # feature :object_identifiers, generator: -> { "item_#{Time.now.to_i}_#{SecureRandom.hex(4)}" }
56
+ # field :data
57
+ # end
58
+ #
59
+ # item = TimestampedItem.new(data: 'test')
60
+ # item.objid # => "item_1693857600_a1b2c3d4"
61
+ #
62
+ # Data Integrity Guarantees:
63
+ #
64
+ # The feature preserves object identifiers passed during initialization,
65
+ # ensuring that existing objects loaded from Redis maintain their IDs:
66
+ #
67
+ # # Loading existing object from Redis preserves ID
68
+ # existing = User.new(objid: 'existing-uuid-value', email: 'existing@example.com')
69
+ # existing.objid # => "existing-uuid-value" (preserved, not regenerated)
70
+ #
71
+ # Performance Characteristics:
72
+ #
73
+ # - Lazy Generation: IDs generated only when first accessed
74
+ # - Thread-Safe: Generator strategy configured once during initialization
75
+ # - Memory Efficient: No unnecessary ID generation for unused objects
76
+ # - Redis Efficient: Only persists non-nil values to conserve memory
77
+ #
78
+ # Security Considerations:
79
+ #
80
+ # - UUID v7 includes timestamp information (may leak timing data)
81
+ # - UUID v4 provides strong randomness without timing correlation
82
+ # - Hex generator provides maximum entropy (256 bits) for security-critical use cases
83
+ # - Custom generators allow domain-specific security requirements
84
+ #
85
+ module ObjectIdentifiers
86
+ DEFAULT_GENERATOR = :uuid_v7
87
+
88
+ def self.included(base)
89
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
90
+ base.extend ClassMethods
91
+
92
+ # Ensure default generator is set in feature options
93
+ base.add_feature_options(:object_identifiers, generator: DEFAULT_GENERATOR)
94
+
95
+ # Register the objid field using our custom field type
96
+ base.register_field_type(
97
+ ObjectIdentifiers::ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false)
98
+ )
99
+ end
100
+
101
+ module ClassMethods
102
+ # Generate a new object identifier using the configured strategy
103
+ #
104
+ # @return [String] A new unique identifier
105
+ #
106
+ def generate_objid
107
+ options = feature_options(:object_identifiers)
108
+ generator = options[:generator] || DEFAULT_GENERATOR
109
+
110
+ case generator
111
+ when :uuid_v7
112
+ SecureRandom.uuid_v7
113
+ when :uuid_v4
114
+ SecureRandom.uuid_v4
115
+ when :hex
116
+ Familia.generate_hex_id
117
+ when Proc
118
+ generator.call
119
+ else
120
+ unless generator.respond_to?(:call)
121
+ raise Familia::Problem, "Invalid object identifier generator: #{generator.inspect}"
122
+ end
123
+
124
+ generator.call
125
+
126
+ end
127
+ end
128
+
129
+ # Find an object by its object identifier
130
+ #
131
+ # @param objid [String] The object identifier to search for
132
+ # @return [Object, nil] The object if found, nil otherwise
133
+ #
134
+ def find_by_objid(objid)
135
+ return nil if objid.to_s.empty?
136
+
137
+ if Familia.debug?
138
+ reference = caller(1..1).first
139
+ Familia.trace :FIND_BY_OBJID, Familia.dbclient, objid, reference
140
+ end
141
+
142
+ # Use the object identifier as the key for lookup
143
+ # This is a simple stub implementation - would need more sophisticated
144
+ # search logic in a real application
145
+ find_by_id(objid)
146
+ rescue Familia::NotFound
147
+ nil
148
+ end
149
+ end
150
+
151
+ # Instance method for generating object identifier using configured strategy
152
+ #
153
+ # This method is called by the ObjectIdentifierFieldType when lazy generation
154
+ # is needed. It uses the class-level generator configuration to create new IDs.
155
+ #
156
+ # @return [String] A newly generated unique identifier
157
+ # @private
158
+ #
159
+ def generate_object_identifier
160
+ self.class.generate_objid
161
+ end
162
+
163
+ # Alias for objid for consistency with naming conventions
164
+ #
165
+ # @return [String] The object identifier
166
+ #
167
+ def object_identifier
168
+ objid
169
+ end
170
+
171
+ # Initialize object identifier configuration
172
+ #
173
+ # Called during object initialization to set up the ID generation strategy.
174
+ # This hook is called AFTER field initialization, ensuring that any objid
175
+ # values passed during construction are preserved.
176
+ #
177
+ def init
178
+ super if defined?(super)
179
+
180
+ # The generator strategy is configured at the class level via feature options.
181
+ # We don't need to store it per-instance since it's consistent for the class.
182
+ # The actual generation happens lazily in the getter when needed.
183
+
184
+ return unless Familia.debug?
185
+
186
+ options = self.class.feature_options(:object_identifiers)
187
+ generator = options[:generator] || DEFAULT_GENERATOR
188
+ Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
189
+ end
190
+
191
+ Familia::Base.add_feature self, :object_identifiers, depends_on: []
192
+ end
193
+ end
194
+ end
@@ -431,7 +431,6 @@ module Familia
431
431
  affected_keys
432
432
  end
433
433
  end
434
-
435
434
  end
436
435
  end
437
436
  end
@@ -363,7 +363,6 @@ module Familia
363
363
  dbclient.hexists(index_key, field_value.to_s)
364
364
  end
365
365
  end
366
-
367
366
  end
368
367
  end
369
368
  end
@@ -496,7 +496,6 @@ module Familia
496
496
  true
497
497
  end
498
498
  end
499
-
500
499
  end
501
500
  end
502
501
  end