familia 2.0.0.pre8 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.gitignore +9 -9
  5. data/.rubocop.yml +19 -0
  6. data/.yardopts +22 -1
  7. data/CHANGELOG.md +247 -0
  8. data/CLAUDE.md +12 -59
  9. data/Gemfile.lock +1 -1
  10. data/README.md +62 -2
  11. data/changelog.d/README.md +77 -0
  12. data/docs/archive/.gitignore +2 -0
  13. data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
  14. data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
  15. data/docs/archive/FAMILIA_UPDATE.md +226 -0
  16. data/docs/archive/README.md +63 -0
  17. data/docs/guides/.gitignore +2 -0
  18. data/docs/{wiki → guides}/Home.md +1 -1
  19. data/docs/{wiki → guides}/Implementation-Guide.md +1 -1
  20. data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
  21. data/docs/guides/relationships-methods.md +266 -0
  22. data/docs/migrating/.gitignore +2 -0
  23. data/docs/migrating/v2.0.0-pre.md +84 -0
  24. data/docs/migrating/v2.0.0-pre11.md +255 -0
  25. data/docs/migrating/v2.0.0-pre12.md +306 -0
  26. data/docs/migrating/v2.0.0-pre5.md +110 -0
  27. data/docs/migrating/v2.0.0-pre6.md +154 -0
  28. data/docs/migrating/v2.0.0-pre7.md +222 -0
  29. data/docs/overview.md +6 -7
  30. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  31. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  32. data/examples/relationships.rb +205 -0
  33. data/examples/safe_dump.rb +281 -0
  34. data/familia.gemspec +4 -4
  35. data/lib/familia/base.rb +52 -0
  36. data/lib/familia/connection.rb +4 -21
  37. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  38. data/lib/familia/errors.rb +2 -0
  39. data/lib/familia/features/autoloader.rb +57 -0
  40. data/lib/familia/features/external_identifier.rb +310 -0
  41. data/lib/familia/features/object_identifier.rb +307 -0
  42. data/lib/familia/features/relationships/indexing.rb +160 -175
  43. data/lib/familia/features/relationships/membership.rb +16 -21
  44. data/lib/familia/features/relationships/tracking.rb +61 -21
  45. data/lib/familia/features/relationships.rb +15 -8
  46. data/lib/familia/features/safe_dump.rb +66 -72
  47. data/lib/familia/features.rb +93 -5
  48. data/lib/familia/horreum/subclass/definition.rb +49 -3
  49. data/lib/familia/horreum.rb +15 -24
  50. data/lib/familia/secure_identifier.rb +51 -75
  51. data/lib/familia/verifiable_identifier.rb +162 -0
  52. data/lib/familia/version.rb +1 -1
  53. data/lib/familia.rb +1 -0
  54. data/setup.cfg +5 -0
  55. data/try/core/secure_identifier_try.rb +47 -18
  56. data/try/core/verifiable_identifier_try.rb +171 -0
  57. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  58. data/try/features/feature_improvements_try.rb +126 -0
  59. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  60. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  61. data/try/features/real_feature_integration_try.rb +7 -6
  62. data/try/features/relationships/relationships_api_changes_try.rb +339 -0
  63. data/try/features/relationships/relationships_try.rb +6 -5
  64. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  65. data/try/helpers/test_helpers.rb +17 -17
  66. metadata +62 -41
  67. data/examples/relationships_basic.rb +0 -273
  68. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  69. data/lib/familia/features/external_identifiers.rb +0 -111
  70. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  71. data/lib/familia/features/object_identifiers.rb +0 -194
  72. /data/docs/{wiki → guides}/API-Reference.md +0 -0
  73. /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
  74. /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
  75. /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
  76. /data/docs/{wiki → guides}/Feature-System-Guide.md +0 -0
  77. /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
  78. /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
  79. /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
  80. /data/docs/{wiki → guides}/Security-Model.md +0 -0
  81. /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
@@ -1,273 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Basic Relationships Example
4
- # This example demonstrates the core features of Familia's relationships system
5
-
6
- require_relative '../lib/familia'
7
-
8
- # Configure Familia for the example
9
- Familia.configure do |config|
10
- config.redis_uri = ENV.fetch('REDIS_URI', 'redis://localhost:6379/15')
11
- end
12
-
13
- puts "=== Familia Relationships Basic Example ==="
14
- puts
15
-
16
- # Define our model classes
17
- class Customer < Familia::Horreum
18
- feature :relationships
19
-
20
- identifier_field :custid
21
- field :custid, :name, :email, :plan
22
-
23
- # Define collections for tracking relationships
24
- set :domains # Simple set of domain IDs
25
- list :projects # Ordered list of project IDs
26
- sorted_set :activity # Activity feed with timestamps
27
-
28
- # Create indexes for fast lookups
29
- indexed_by :email_lookup, field: :email
30
- indexed_by :plan_lookup, field: :plan
31
-
32
- # Track in global collections
33
- tracked_in :all_customers, type: :sorted_set, score: :created_at
34
-
35
- def created_at
36
- Time.now.to_i
37
- end
38
- end
39
-
40
- class Domain < Familia::Horreum
41
- feature :relationships
42
-
43
- identifier_field :domain_id
44
- field :domain_id, :name, :dns_zone, :status
45
-
46
- # Declare membership in customer collections
47
- member_of Customer, :domains, type: :set
48
-
49
- # Track domains by status
50
- tracked_in :active_domains, type: :sorted_set,
51
- score: ->(domain) { domain.status == 'active' ? Time.now.to_i : 0 }
52
- end
53
-
54
- class Project < Familia::Horreum
55
- feature :relationships
56
-
57
- identifier_field :project_id
58
- field :project_id, :name, :priority
59
-
60
- # Member of customer projects list (ordered)
61
- member_of Customer, :projects, type: :list
62
- end
63
-
64
- puts "=== 1. Basic Object Creation ==="
65
-
66
- # Create some sample objects
67
- customer = Customer.new(
68
- custid: "cust_#{SecureRandom.hex(4)}",
69
- name: "Acme Corporation",
70
- email: "admin@acme.com",
71
- plan: "enterprise"
72
- )
73
-
74
- domain1 = Domain.new(
75
- domain_id: "dom_#{SecureRandom.hex(4)}",
76
- name: "acme.com",
77
- dns_zone: "acme.com.",
78
- status: "active"
79
- )
80
-
81
- domain2 = Domain.new(
82
- domain_id: "dom_#{SecureRandom.hex(4)}",
83
- name: "staging.acme.com",
84
- dns_zone: "staging.acme.com.",
85
- status: "active"
86
- )
87
-
88
- project = Project.new(
89
- project_id: "proj_#{SecureRandom.hex(4)}",
90
- name: "Website Redesign",
91
- priority: "high"
92
- )
93
-
94
- puts "✓ Created customer: #{customer.name} (#{customer.custid})"
95
- puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
96
- puts "✓ Created project: #{project.name}"
97
- puts
98
-
99
- puts "=== 2. Establishing Relationships ==="
100
-
101
- # Add objects to indexed lookups
102
- Customer.add_to_email_lookup(customer)
103
- Customer.add_to_plan_lookup(customer)
104
- puts "✓ Added customer to email and plan indexes"
105
-
106
- # Add customer to global tracking
107
- Customer.add_to_all_customers(customer)
108
- puts "✓ Added customer to global customer tracking"
109
-
110
- # Establish member_of relationships (bidirectional)
111
- domain1.add_to_customer_domains(customer.custid)
112
- customer.domains.add(domain1.identifier)
113
-
114
- domain2.add_to_customer_domains(customer.custid)
115
- customer.domains.add(domain2.identifier)
116
-
117
- project.add_to_customer_projects(customer.custid)
118
- customer.projects.add(project.identifier)
119
-
120
- puts "✓ Established domain ownership relationships"
121
- puts "✓ Established project ownership relationships"
122
-
123
- # Track domains in status collections
124
- Domain.add_to_active_domains(domain1)
125
- Domain.add_to_active_domains(domain2)
126
- puts "✓ Added domains to active tracking"
127
- puts
128
-
129
- puts "=== 3. Querying Relationships ==="
130
-
131
- # Test indexed lookups
132
- found_customer_id = Customer.email_lookup.get(customer.email)
133
- puts "Email lookup for #{customer.email}: #{found_customer_id}"
134
-
135
- enterprise_customers = Customer.plan_lookup.get("enterprise")
136
- puts "Enterprise customer found: #{enterprise_customers}"
137
-
138
- # Test membership queries
139
- puts "\nDomain membership checks:"
140
- puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer.custid)}"
141
- puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer.custid)}"
142
-
143
- puts "\nCustomer collections:"
144
- puts " Customer has #{customer.domains.size} domains"
145
- puts " Customer has #{customer.projects.size} projects"
146
- puts " Domain IDs: #{customer.domains.members}"
147
- puts " Project IDs: #{customer.projects.members}"
148
-
149
- # Test tracked_in collections
150
- all_customers_count = Customer.all_customers.size
151
- puts "\nGlobal tracking:"
152
- puts " Total customers in system: #{all_customers_count}"
153
-
154
- active_domains_count = Domain.active_domains.size
155
- puts " Active domains in system: #{active_domains_count}"
156
- puts
157
-
158
- puts "=== 4. Range Queries ==="
159
-
160
- # Get recent customers (last 24 hours)
161
- yesterday = (Time.now - 24.hours).to_i
162
- recent_customers = Customer.all_customers.range_by_score(yesterday, '+inf')
163
- puts "Recent customers (last 24h): #{recent_customers.size}"
164
-
165
- # Get all active domains by score
166
- active_domain_scores = Domain.active_domains.range_by_score(1, '+inf', with_scores: true)
167
- puts "Active domains with timestamps:"
168
- active_domain_scores.each do |domain_id, timestamp|
169
- puts " #{domain_id}: active since #{Time.at(timestamp.to_i)}"
170
- end
171
- puts
172
-
173
- puts "=== 5. Batch Operations ==="
174
-
175
- # Create additional test data
176
- additional_customers = []
177
- 3.times do |i|
178
- cust = Customer.new(
179
- custid: "batch_cust_#{i}",
180
- name: "Customer #{i}",
181
- email: "customer#{i}@example.com",
182
- plan: i.even? ? "basic" : "premium"
183
- )
184
- additional_customers << cust
185
-
186
- # Add to indexes and tracking
187
- Customer.add_to_email_lookup(cust)
188
- Customer.add_to_plan_lookup(cust)
189
- Customer.add_to_all_customers(cust)
190
- end
191
-
192
- puts "✓ Created and indexed #{additional_customers.size} additional customers"
193
-
194
- # Query by plan
195
- basic_customers = Customer.plan_lookup.get("basic")
196
- premium_customers = Customer.plan_lookup.get("premium")
197
- enterprise_customers = Customer.plan_lookup.get("enterprise")
198
-
199
- puts "\nCustomer distribution by plan:"
200
- puts " Basic: #{basic_customers ? 1 : 0} customers"
201
- puts " Premium: #{premium_customers ? 1 : 0} customers"
202
- puts " Enterprise: #{enterprise_customers ? 1 : 0} customers"
203
- puts
204
-
205
- puts "=== 6. Relationship Cleanup ==="
206
-
207
- # Remove relationships
208
- puts "Cleaning up relationships..."
209
-
210
- # Remove from member_of relationships
211
- domain1.remove_from_customer_domains(customer.custid)
212
- customer.domains.remove(domain1.identifier)
213
- puts "✓ Removed #{domain1.name} from customer domains"
214
-
215
- # Remove from tracking collections
216
- Domain.active_domains.remove(domain2.identifier)
217
- puts "✓ Removed #{domain2.name} from active domains"
218
-
219
- # Verify cleanup
220
- puts "\nAfter cleanup:"
221
- puts " Customer domains: #{customer.domains.size}"
222
- puts " Active domains: #{Domain.active_domains.size}"
223
- puts
224
-
225
- puts "=== 7. Advanced Usage - Permission Encoding ==="
226
-
227
- # Demonstrate basic score encoding for permissions
228
- class DocumentAccess
229
- # Permission flags (powers of 2)
230
- READ = 1
231
- WRITE = 2
232
- DELETE = 4
233
- ADMIN = 8
234
-
235
- def self.encode_permissions(timestamp, permissions)
236
- "#{timestamp}.#{permissions}".to_f
237
- end
238
-
239
- def self.decode_permissions(score)
240
- parts = score.to_s.split('.')
241
- timestamp = parts[0].to_i
242
- permissions = parts[1] ? parts[1].to_i : 0
243
- [timestamp, permissions]
244
- end
245
- end
246
-
247
- # Example permission encoding
248
- now = Time.now.to_i
249
- read_write_permissions = DocumentAccess::READ | DocumentAccess::WRITE # 3
250
- admin_permissions = DocumentAccess::ADMIN # 8
251
-
252
- encoded_score = DocumentAccess.encode_permissions(now, read_write_permissions)
253
- timestamp, permissions = DocumentAccess.decode_permissions(encoded_score)
254
-
255
- puts "Permission encoding example:"
256
- puts " Original: timestamp=#{now}, permissions=#{read_write_permissions}"
257
- puts " Encoded score: #{encoded_score}"
258
- puts " Decoded: timestamp=#{timestamp}, permissions=#{permissions}"
259
- puts " Has read access: #{(permissions & DocumentAccess::READ) != 0}"
260
- puts " Has write access: #{(permissions & DocumentAccess::WRITE) != 0}"
261
- puts " Has delete access: #{(permissions & DocumentAccess::DELETE) != 0}"
262
- puts
263
-
264
- puts "=== Example Complete! ==="
265
- puts
266
- puts "Key takeaways:"
267
- puts "• tracked_in: Use for activity feeds, leaderboards, time-series data"
268
- puts "• indexed_by: Use for fast O(1) lookups by field values"
269
- puts "• member_of: Use for bidirectional ownership/membership"
270
- puts "• Score encoding: Combine timestamps with metadata for rich queries"
271
- puts "• Batch operations: Use Redis pipelines for efficiency"
272
- puts
273
- puts "See docs/wiki/Relationships-Guide.md for comprehensive documentation"
@@ -1,120 +0,0 @@
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
@@ -1,111 +0,0 @@
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
@@ -1,91 +0,0 @@
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