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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +35 -0
- data/docs/wiki/Feature-System-Guide.md +0 -15
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/relationships/cascading.rb +0 -1
- data/lib/familia/features/relationships/indexing.rb +0 -1
- data/lib/familia/features/relationships/membership.rb +0 -1
- data/lib/familia/features/relationships/querying.rb +7 -12
- data/lib/familia/features/relationships/score_encoding.rb +1 -3
- data/lib/familia/features/relationships/tracking.rb +0 -1
- data/lib/familia/features/transient_fields.rb +8 -10
- data/lib/familia/features.rb +16 -13
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +34 -0
- data/lib/familia/version.rb +1 -3
- data/try/core/errors_try.rb +1 -1
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
- data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
- data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
- data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
- data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
- data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
- data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +1 -1
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- metadata +38 -31
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5ab18b134425a370c5d40f464e87747e85e713dc44c2a7906b7b024a66b23a4
|
4
|
+
data.tar.gz: f5a2e3fd2ca4553781b7f58f4f0e7a6d5e9228c18f34c7a1f5cf1af7695d580d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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.
|
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
|