familia 2.0.0.pre6 → 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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -13
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +3 -3
  9. data/README.md +35 -0
  10. data/docs/wiki/Feature-System-Guide.md +36 -20
  11. data/docs/wiki/Home.md +30 -20
  12. data/docs/wiki/Relationships-Guide.md +684 -0
  13. data/examples/bit_encoding_integration.rb +237 -0
  14. data/examples/redis_command_validation_example.rb +231 -0
  15. data/examples/relationships_basic.rb +273 -0
  16. data/lib/familia/connection.rb +3 -3
  17. data/lib/familia/data_type.rb +7 -4
  18. data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
  19. data/lib/familia/features/encrypted_fields.rb +413 -4
  20. data/lib/familia/features/expiration.rb +319 -33
  21. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
  22. data/lib/familia/features/external_identifiers.rb +111 -0
  23. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
  24. data/lib/familia/features/object_identifiers.rb +194 -0
  25. data/lib/familia/features/quantization.rb +385 -44
  26. data/lib/familia/features/relationships/cascading.rb +437 -0
  27. data/lib/familia/features/relationships/indexing.rb +369 -0
  28. data/lib/familia/features/relationships/membership.rb +502 -0
  29. data/lib/familia/features/relationships/permission_management.rb +264 -0
  30. data/lib/familia/features/relationships/querying.rb +615 -0
  31. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  32. data/lib/familia/features/relationships/score_encoding.rb +440 -0
  33. data/lib/familia/features/relationships/tracking.rb +378 -0
  34. data/lib/familia/features/relationships.rb +466 -0
  35. data/lib/familia/features/transient_fields.rb +190 -10
  36. data/lib/familia/features.rb +18 -14
  37. data/lib/familia/horreum/core/serialization.rb +2 -5
  38. data/lib/familia/horreum/subclass/definition.rb +35 -1
  39. data/lib/familia/validation/command_recorder.rb +336 -0
  40. data/lib/familia/validation/expectations.rb +519 -0
  41. data/lib/familia/validation/test_helpers.rb +443 -0
  42. data/lib/familia/validation/validator.rb +412 -0
  43. data/lib/familia/validation.rb +140 -0
  44. data/lib/familia/version.rb +1 -3
  45. data/try/core/errors_try.rb +1 -1
  46. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  47. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  48. data/try/edge_cases/string_coercion_try.rb +2 -0
  49. data/try/encryption/encryption_core_try.rb +3 -1
  50. data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +3 -0
  51. data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +1 -0
  52. data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
  53. data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
  54. data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
  55. data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
  56. data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
  57. data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
  58. data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
  59. data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
  60. data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
  61. data/try/features/relationships/categorical_permissions_try.rb +515 -0
  62. data/try/features/relationships/relationships_edge_cases_try.rb +145 -0
  63. data/try/features/relationships/relationships_performance_minimal_try.rb +132 -0
  64. data/try/features/relationships/relationships_performance_simple_try.rb +155 -0
  65. data/try/features/relationships/relationships_performance_try.rb +420 -0
  66. data/try/features/relationships/relationships_performance_working_try.rb +144 -0
  67. data/try/features/relationships/relationships_try.rb +237 -0
  68. data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
  69. data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +4 -1
  70. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  71. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  72. data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
  73. data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
  74. data/try/helpers/test_helpers.rb +1 -1
  75. data/try/horreum/base_try.rb +14 -8
  76. data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
  77. data/try/horreum/relations_try.rb +1 -1
  78. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  79. data/try/validation/command_validation_try.rb.disabled +207 -0
  80. data/try/validation/performance_validation_try.rb.disabled +324 -0
  81. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  82. metadata +62 -27
  83. data/docs/wiki/RelatableObjects-Guide.md +0 -563
  84. data/lib/familia/features/relatable_objects.rb +0 -125
  85. data/try/features/relatable_objects_try.rb +0 -220
  86. /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
  87. /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
  88. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
  89. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
  90. /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
  91. /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
  92. /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
  93. /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
  94. /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
  95. /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
  96. /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -0,0 +1,203 @@
1
+ # try/features/external_identifiers_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Test ExternalIdentifiers feature functionality
8
+
9
+ # Basic class using external identifiers
10
+ class ExternalIdTest < Familia::Horreum
11
+ feature :object_identifiers
12
+ feature :external_identifiers
13
+ identifier_field :id
14
+ field :id
15
+ field :name
16
+ end
17
+
18
+ # Class with custom prefix
19
+ class CustomPrefixTest < Familia::Horreum
20
+ feature :object_identifiers
21
+ feature :external_identifiers, prefix: 'cust'
22
+ identifier_field :id
23
+ field :id
24
+ field :name
25
+ end
26
+
27
+ # Class testing data integrity preservation
28
+ class ExternalDataIntegrityTest < Familia::Horreum
29
+ feature :object_identifiers
30
+ feature :external_identifiers
31
+ identifier_field :id
32
+ field :id
33
+ field :name
34
+ end
35
+
36
+ # Test with existing external ID during initialization
37
+ @existing_ext_obj = ExternalDataIntegrityTest.new(id: 'test_id', extid: 'preset_ext_123', name: 'Preset External')
38
+
39
+ # Test objects for lazy generation and complex initialization
40
+ @lazy_obj = ExternalIdTest.new
41
+ @complex_obj = ExternalIdTest.new(id: 'complex_ext', name: 'Complex External')
42
+
43
+ ## Feature depends on object_identifiers
44
+ ExternalIdTest.features_enabled.include?(:object_identifiers)
45
+ #==> true
46
+
47
+ ## External identifiers feature is included
48
+ ExternalIdTest.features_enabled.include?(:external_identifiers)
49
+ #==> true
50
+
51
+ ## Class has extid field defined
52
+ ExternalIdTest.respond_to?(:extid)
53
+ #==> true
54
+
55
+ ## Object has extid accessor
56
+ obj = ExternalIdTest.new
57
+ obj.respond_to?(:extid)
58
+ #==> true
59
+
60
+ ## External ID is generated from objid deterministically
61
+ obj = ExternalIdTest.new
62
+ obj.id = 'test_obj'
63
+ obj.name = 'Test Object'
64
+ objid = obj.objid
65
+ extid = obj.extid
66
+ # Same objid should always produce same extid
67
+ obj2 = ExternalIdTest.new
68
+ obj2.instance_variable_set(:@objid, objid)
69
+ obj2.extid == extid
70
+ #==> true
71
+
72
+ ## External ID uses default 'ext' prefix
73
+ obj = ExternalIdTest.new
74
+ obj.extid.start_with?('ext_')
75
+ #==> true
76
+
77
+ ## Custom prefix class uses specified prefix
78
+ custom_obj = CustomPrefixTest.new
79
+ custom_obj.extid.start_with?('cust_')
80
+ #==> true
81
+
82
+ ## External ID is URL-safe base-36 format
83
+ obj = ExternalIdTest.new
84
+ extid = obj.extid
85
+ extid.match(/\Aext_[0-9a-z]+\z/)
86
+ #=*> nil
87
+
88
+ ## Custom prefix external ID format is correct
89
+ custom_obj = CustomPrefixTest.new
90
+ extid = custom_obj.extid
91
+ extid.match(/\Acust_[0-9a-z]+\z/)
92
+ #=*> nil
93
+
94
+ ## External ID is lazy - not generated until accessed
95
+ @lazy_obj.instance_variable_get(:@extid)
96
+ #=> nil
97
+
98
+ ## External ID is generated when first accessed
99
+ @lazy_obj.extid
100
+ @lazy_obj.instance_variable_get(:@extid)
101
+ #=*> nil
102
+
103
+ ## External ID value is stable across multiple calls
104
+ first_call = @lazy_obj.extid
105
+ second_call = @lazy_obj.extid
106
+ first_call == second_call
107
+ #==> true
108
+
109
+ ## Data integrity: preset extid is preserved
110
+ @existing_ext_obj.extid
111
+ #=> "preset_ext_123"
112
+
113
+ ## Data integrity: preset extid not regenerated
114
+ @existing_ext_obj.instance_variable_get(:@extid)
115
+ #=> "preset_ext_123"
116
+
117
+ ## find_by_extid class method exists
118
+ ExternalIdTest.respond_to?(:find_by_extid)
119
+ #==> true
120
+
121
+ ## find_by_extid returns correct type
122
+ result = ExternalIdTest.find_by_extid('nonexistent')
123
+ result.is_a?(ExternalIdTest) || result.nil?
124
+ #==> true
125
+
126
+ ## External ID is deterministic from objid
127
+ test_objid = "01234567-89ab-7def-8fed-cba987654321"
128
+ obj1 = ExternalIdTest.new
129
+ obj1.instance_variable_set(:@objid, test_objid)
130
+ obj2 = ExternalIdTest.new
131
+ obj2.instance_variable_set(:@objid, test_objid)
132
+ obj1.extid == obj2.extid
133
+ #==> true
134
+
135
+ ## External ID is different from objid
136
+ obj = ExternalIdTest.new
137
+ obj.objid != obj.extid
138
+ #==> true
139
+
140
+ ## External ID persists through save/load cycle
141
+ save_obj = ExternalIdTest.new
142
+ save_obj.id = 'ext_save_test'
143
+ save_obj.name = 'External Save Test'
144
+ original_extid = save_obj.extid
145
+ save_obj.save
146
+ loaded_obj = ExternalIdTest.new(id: 'ext_save_test')
147
+ loaded_obj.extid == original_extid
148
+ #==> true
149
+
150
+ ## Different objids produce different external IDs
151
+ obj1 = ExternalIdTest.new
152
+ obj2 = ExternalIdTest.new
153
+ obj1.extid != obj2.extid
154
+ #==> true
155
+
156
+ ## extid field type is ExternalIdentifierFieldType
157
+ ExternalIdTest.field_types[:extid]
158
+ #=:> Familia::Features::ExternalIdentifiers::ExternalIdentifierFieldType
159
+
160
+ ## Feature options contain correct prefix
161
+ ExternalIdTest.feature_options(:external_identifiers)[:prefix]
162
+ #=> "ext"
163
+
164
+ ## Custom prefix feature options
165
+ CustomPrefixTest.feature_options(:external_identifiers)[:prefix]
166
+ #=> "cust"
167
+
168
+ ## External ID is shorter than UUID objid
169
+ obj = ExternalIdTest.new
170
+ obj.extid.length < obj.objid.length
171
+ #==> true
172
+
173
+ ## External ID contains only lowercase alphanumeric after prefix
174
+ obj = ExternalIdTest.new
175
+ extid_suffix = obj.extid.split('_', 2)[1]
176
+ extid_suffix.match(/\A[0-9a-z]+\z/)
177
+ #=*> nil
178
+
179
+ ## Complex initialization preserves lazy generation
180
+ @complex_obj.instance_variable_get(:@extid)
181
+ #=> nil
182
+
183
+ ## External ID generation after complex initialization
184
+ @complex_obj.extid
185
+ #=*> nil
186
+
187
+ ## find_by_extid works with saved objects
188
+ @test_obj = ExternalIdTest.new(id: 'findable_test', name: 'Test Object')
189
+ @test_obj.save
190
+ found_obj = ExternalIdTest.find_by_extid(@test_obj.extid)
191
+ found_obj&.id
192
+ #=> "findable_test"
193
+
194
+ ## find_by_extid returns nil for nonexistent extids
195
+ ExternalIdTest.find_by_extid('nonexistent_extid')
196
+ #=> nil
197
+
198
+ ## extid_lookup mapping is maintained
199
+ ExternalIdTest.extid_lookup[@test_obj.extid]
200
+ #=> "findable_test"
201
+
202
+ # Cleanup test objects
203
+ @test_obj.destroy! rescue nil
@@ -0,0 +1,289 @@
1
+ # try/features/object_identifiers_integration_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Integration test for ObjectIdentifiers and ExternalIdentifiers features together
8
+
9
+ # Class using both features with defaults
10
+ class IntegrationTest < Familia::Horreum
11
+ feature :object_identifiers
12
+ feature :external_identifiers # This depends on :object_identifiers
13
+ identifier_field :id
14
+ field :id
15
+ field :name
16
+ field :email
17
+ end
18
+
19
+ # Class with custom configurations for both features
20
+ class CustomIntegrationTest < Familia::Horreum
21
+ feature :object_identifiers, generator: :hex
22
+ feature :external_identifiers, prefix: 'custom'
23
+ identifier_field :id
24
+ field :id
25
+ field :name
26
+ end
27
+
28
+ # Class testing full lifecycle with Redis persistence
29
+ class PersistenceTest < Familia::Horreum
30
+ feature :object_identifiers
31
+ feature :external_identifiers
32
+ identifier_field :id
33
+ field :id
34
+ field :name
35
+ field :created_at
36
+ end
37
+
38
+ # Setup test objects
39
+ @integration_obj = IntegrationTest.new(id: 'integration_1', name: 'Integration Test', email: 'test@example.com')
40
+ @custom_obj = CustomIntegrationTest.new(id: 'custom_1', name: 'Custom Test')
41
+
42
+ ## Object identifiers feature is automatically included
43
+ IntegrationTest.features_enabled.include?(:object_identifiers)
44
+ #==> true
45
+
46
+ ## External identifiers feature is included
47
+ IntegrationTest.features_enabled.include?(:external_identifiers)
48
+ #==> true
49
+
50
+ ## Object responds to objid accessor
51
+ obj = IntegrationTest.new
52
+ obj.respond_to?(:objid)
53
+ #==> true
54
+
55
+ ## Object responds to extid accessor
56
+ obj = IntegrationTest.new
57
+ obj.respond_to?(:extid)
58
+ #==> true
59
+
60
+ ## Class responds to find_by_objid method
61
+ IntegrationTest.respond_to?(:find_by_objid)
62
+ #==> true
63
+
64
+ ## Class responds to find_by_extid method
65
+ IntegrationTest.respond_to?(:find_by_extid)
66
+ #==> true
67
+
68
+ ## objid and extid are different values
69
+ obj = IntegrationTest.new
70
+ obj.objid != obj.extid
71
+ #==> true
72
+
73
+ ## extid is deterministically generated from objid
74
+ obj = IntegrationTest.new
75
+ original_objid = obj.objid
76
+ original_extid = obj.extid
77
+ # Create new object with same objid
78
+ obj2 = IntegrationTest.new
79
+ obj2.instance_variable_set(:@objid, original_objid)
80
+ obj2.extid == original_extid
81
+ #==> true
82
+
83
+ ## Custom objid uses hex format (64 chars for 256-bit)
84
+ @custom_obj.objid.match(/\A[0-9a-f]{64}\z/)
85
+ #=*> nil
86
+
87
+ ## Custom extid uses custom prefix
88
+ @custom_obj.extid.start_with?('custom_')
89
+ #==> true
90
+
91
+ ## Both IDs persist through save/load cycle
92
+ persistence_obj = PersistenceTest.new
93
+ persistence_obj.id = 'persistence_test'
94
+ persistence_obj.name = 'Persistence Test Object'
95
+ persistence_obj.created_at = Time.now.to_i
96
+ original_objid = persistence_obj.objid
97
+ original_extid = persistence_obj.extid
98
+ persistence_obj.save
99
+
100
+ # Load from Redis
101
+ loaded_obj = PersistenceTest.new(id: 'persistence_test')
102
+
103
+ ## objid persists after save/load
104
+ persistence_obj = PersistenceTest.new
105
+ persistence_obj.id = 'persistence_test'
106
+ persistence_obj.name = 'Persistence Test Object'
107
+ persistence_obj.created_at = Time.now.to_i
108
+ original_objid = persistence_obj.objid
109
+ persistence_obj.save
110
+ loaded_obj = PersistenceTest.new(id: 'persistence_test')
111
+ loaded_obj.objid == original_objid
112
+ #==> true
113
+
114
+ ## extid persists after save/load
115
+ persistence_obj = PersistenceTest.new
116
+ persistence_obj.id = 'persistence_test'
117
+ persistence_obj.name = 'Persistence Test Object'
118
+ persistence_obj.created_at = Time.now.to_i
119
+ original_extid = persistence_obj.extid
120
+ persistence_obj.save
121
+ loaded_obj = PersistenceTest.new(id: 'persistence_test')
122
+ loaded_obj.extid == original_extid
123
+ #==> true
124
+
125
+ ## objid instance variable starts nil (lazy generation)
126
+ lazy_obj = IntegrationTest.new
127
+ lazy_obj.instance_variable_get(:@objid)
128
+ #=> nil
129
+
130
+ ## extid instance variable starts nil (lazy generation)
131
+ lazy_obj = IntegrationTest.new
132
+ lazy_obj.instance_variable_get(:@extid)
133
+ #=> nil
134
+
135
+ ## Accessing objid first doesn't trigger extid generation
136
+ lazy_obj = IntegrationTest.new
137
+ lazy_obj.objid
138
+ lazy_obj.instance_variable_get(:@extid)
139
+ #=> nil
140
+
141
+ ## Accessing extid triggers objid generation if needed
142
+ lazy_obj2 = IntegrationTest.new
143
+ lazy_obj2.extid # This should trigger objid generation too
144
+ lazy_obj2.instance_variable_get(:@objid)
145
+ #=*> nil
146
+
147
+ ## Check field types objid
148
+ IntegrationTest.field_types[:objid].is_a?(Familia::Features::ObjectIdentifiers::ObjectIdentifierFieldType)
149
+ #==> true
150
+
151
+ ## ObjectIdentifier fields have correct types in field registry
152
+ IntegrationTest.field_types[:objid].class.ancestors.include?(Familia::Features::ObjectIdentifiers::ObjectIdentifierFieldType)
153
+ #==> true
154
+
155
+ ## ExternalIdentifier fields have correct types in field registry
156
+ IntegrationTest.field_types[:extid].class.ancestors.include?(Familia::Features::ExternalIdentifiers::ExternalIdentifierFieldType)
157
+ #==> true
158
+
159
+ ## Object identifiers options are preserved
160
+ opts = IntegrationTest.feature_options
161
+ opts.key?(:object_identifiers)
162
+ #==> true
163
+
164
+ ## External identifiers options are preserved
165
+ opts = IntegrationTest.feature_options
166
+ opts.key?(:external_identifiers)
167
+ #==> true
168
+
169
+ ## Generator default configuration is applied correctly
170
+ IntegrationTest.feature_options(:object_identifiers)[:generator]
171
+ #=> :uuid_v7
172
+
173
+ ## Prefix default configuration is applied correctly
174
+ IntegrationTest.feature_options(:external_identifiers)[:prefix]
175
+ #=> "ext"
176
+
177
+ ## Custom generator configuration is applied correctly
178
+ CustomIntegrationTest.feature_options(:object_identifiers)[:generator]
179
+ #=> :hex
180
+
181
+ ## Custom prefix configuration is applied correctly
182
+ CustomIntegrationTest.feature_options(:external_identifiers)[:prefix]
183
+ #=> "custom"
184
+
185
+ ## objid is URL-safe (UUID format)
186
+ obj = IntegrationTest.new
187
+ obj.objid.match(/\A[A-Za-z0-9\-]+\z/)
188
+ #=*> nil
189
+
190
+ ## extid is URL-safe (base36 format)
191
+ obj = IntegrationTest.new
192
+ obj.extid.match(/\A[a-z0-9_]+\z/)
193
+ #=*> nil
194
+
195
+ ## Data integrity preserved during complex initialization
196
+ complex_obj = IntegrationTest.new(
197
+ id: 'complex_integration',
198
+ name: 'Complex Integration',
199
+ email: 'complex@test.com',
200
+ objid: 'preset_objid_123',
201
+ extid: 'preset_ext_456'
202
+ )
203
+
204
+ ## Preset objid value is preserved
205
+ complex_obj = IntegrationTest.new(
206
+ id: 'complex_integration',
207
+ name: 'Complex Integration',
208
+ email: 'complex@test.com',
209
+ objid: 'preset_objid_123',
210
+ extid: 'preset_ext_456'
211
+ )
212
+ complex_obj.objid
213
+ #=> 'preset_objid_123'
214
+
215
+ ## Preset extid value is preserved
216
+ complex_obj = IntegrationTest.new(
217
+ id: 'complex_integration',
218
+ name: 'Complex Integration',
219
+ email: 'complex@test.com',
220
+ objid: 'preset_objid_123',
221
+ extid: 'preset_ext_456'
222
+ )
223
+ complex_obj.extid
224
+ #=> 'preset_ext_456'
225
+
226
+ ## find_by methods are available (stub implementations)
227
+ search_obj = IntegrationTest.new
228
+ search_obj.id = 'search_test'
229
+ search_obj.save
230
+
231
+ ## find_by_objid returns nil (stub implementation)
232
+ search_obj = IntegrationTest.new
233
+ search_obj.id = 'search_test'
234
+ search_obj.save
235
+ found_by_objid = IntegrationTest.find_by_objid(search_obj.objid)
236
+ found_by_objid
237
+ #=> nil
238
+
239
+ ## find_by_extid works with real implementation
240
+ @search_obj = IntegrationTest.new
241
+ @search_obj.id = 'search_test'
242
+ @search_obj.save
243
+ found_by_extid = IntegrationTest.find_by_extid(@search_obj.extid)
244
+ found_by_extid&.id
245
+ #=> "search_test"
246
+
247
+ ## Both IDs remain stable across multiple accesses
248
+ stability_obj = IntegrationTest.new
249
+ first_objid = stability_obj.objid
250
+ first_extid = stability_obj.extid
251
+ second_objid = stability_obj.objid
252
+ second_extid = stability_obj.extid
253
+
254
+ ## objid remains stable across accesses
255
+ stability_obj = IntegrationTest.new
256
+ first_objid = stability_obj.objid
257
+ second_objid = stability_obj.objid
258
+ first_objid == second_objid
259
+ #==> true
260
+
261
+ ## extid remains stable across accesses
262
+ stability_obj = IntegrationTest.new
263
+ first_extid = stability_obj.extid
264
+ second_extid = stability_obj.extid
265
+ first_extid == second_extid
266
+ #==> true
267
+
268
+ ## Feature dependency is enforced (external_identifiers requires object_identifiers)
269
+ # This is automatically handled by the feature system
270
+ IntegrationTest.features_enabled.include?(:object_identifiers)
271
+ #==> true
272
+
273
+ ## Objects work with existing Horreum save pattern
274
+ obj = IntegrationTest.new
275
+ obj.respond_to?(:save)
276
+ #==> true
277
+
278
+ ## Objects work with existing Horreum exists pattern
279
+ obj = IntegrationTest.new
280
+ obj.respond_to?(:exists?)
281
+ #==> true
282
+
283
+ ## Objects work with existing Horreum delete pattern
284
+ obj = IntegrationTest.new
285
+ obj.respond_to?(:delete!)
286
+ #==> true
287
+
288
+ # Cleanup test objects
289
+ @search_obj.destroy! rescue nil
@@ -0,0 +1,191 @@
1
+ # try/features/object_identifiers_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Test ObjectIdentifiers feature functionality
8
+
9
+ # Basic class using default UUID v7 generator
10
+ class BasicObjectTest < Familia::Horreum
11
+ feature :object_identifiers
12
+ identifier_field :id
13
+ field :id
14
+ field :name
15
+ end
16
+
17
+ # Class using UUID v4 generator
18
+ class UuidV4Test < Familia::Horreum
19
+ feature :object_identifiers, generator: :uuid_v4
20
+ identifier_field :id
21
+ field :id
22
+ field :name
23
+ end
24
+
25
+ # Class using hex generator
26
+ class HexTest < Familia::Horreum
27
+ feature :object_identifiers, generator: :hex
28
+ identifier_field :id
29
+ field :id
30
+ field :name
31
+ end
32
+
33
+ # Class using custom proc generator
34
+ class CustomProcTest < Familia::Horreum
35
+ feature :object_identifiers, generator: -> { "custom_#{SecureRandom.hex(4)}" }
36
+ identifier_field :id
37
+ field :id
38
+ field :name
39
+ end
40
+
41
+ # Class testing data integrity preservation
42
+ class DataIntegrityTest < Familia::Horreum
43
+ feature :object_identifiers
44
+ identifier_field :id
45
+ field :id
46
+ field :name
47
+ end
48
+
49
+ # Test with existing object ID during initialization
50
+ @existing_obj = DataIntegrityTest.new(id: 'test_id', objid: 'preset_id_123', name: 'Preset Object')
51
+
52
+ ## Feature is available on class
53
+ BasicObjectTest.features_enabled.include?(:object_identifiers)
54
+ #==> true
55
+
56
+ ## Class has objid field defined
57
+ BasicObjectTest.respond_to?(:objid)
58
+ #==> true
59
+
60
+ ## Object has objid accessor
61
+ obj = BasicObjectTest.new
62
+ obj.respond_to?(:objid)
63
+ #==> true
64
+
65
+ ## Default generator creates UUID v7 format
66
+ obj = BasicObjectTest.new
67
+ obj.name = 'Test Object'
68
+ objid = obj.objid
69
+ objid.is_a?(String) && objid.length == 36 && objid.include?('-')
70
+ #==> true
71
+
72
+ ## UUID v7 objid has correct format (8-4-4-4-12 characters)
73
+ obj = BasicObjectTest.new
74
+ obj.objid.match(/\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/)
75
+ #=*> nil
76
+
77
+ ## UUID v4 generator creates correct format
78
+ v4_obj = UuidV4Test.new
79
+ v4_objid = v4_obj.objid
80
+ v4_objid.match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/)
81
+ #=*> nil
82
+
83
+ ## Hex generator creates hex string
84
+ hex_obj = HexTest.new
85
+ hex_objid = hex_obj.objid
86
+ hex_objid.is_a?(String) && hex_objid.length == 16 && hex_objid.match(/\A[0-9a-f]+\z/)
87
+ #==> true
88
+
89
+ ## Custom proc generator works
90
+ custom_obj = CustomProcTest.new
91
+ custom_objid = custom_obj.objid
92
+ custom_objid.start_with?('custom_') && custom_objid.length == 15
93
+ #==> true
94
+
95
+ ## objid is lazy - not generated until accessed
96
+ lazy_obj = BasicObjectTest.new
97
+ lazy_obj.instance_variable_get(:@objid)
98
+ #=> nil
99
+
100
+ ## objid is generated when first accessed
101
+ lazy_obj = BasicObjectTest.new
102
+ lazy_obj.objid
103
+ lazy_obj
104
+ #=*> _.instance_variable_get(:@objid)
105
+
106
+ ## objid is generated when first accessed (alternatve testcase expectation)
107
+ lazy_obj = BasicObjectTest.new
108
+ lazy_obj.objid
109
+ lazy_obj.instance_variable_get(:@objid)
110
+ #=<> nil
111
+
112
+ ## objid value is stable across multiple calls
113
+ lazy_obj = BasicObjectTest.new
114
+ first_call = lazy_obj.objid
115
+ second_call = lazy_obj.objid
116
+ first_call == second_call
117
+ #==> true
118
+
119
+ ## Data integrity: preset objid is preserved
120
+ @existing_obj.objid
121
+ #=> "preset_id_123"
122
+
123
+ ## Data integrity: preset objid not regenerated
124
+ @existing_obj.instance_variable_get(:@objid)
125
+ #=> "preset_id_123"
126
+
127
+ ## find_by_objid class method exists
128
+ BasicObjectTest.respond_to?(:find_by_objid)
129
+ #==> true
130
+
131
+ ## find_by_objid returns correct type (stub for now)
132
+ BasicObjectTest.find_by_objid('nonexistent')
133
+ #=> nil
134
+
135
+ ## Generated objid is URL-safe (no special chars except hyphens)
136
+ url_obj = BasicObjectTest.new
137
+ objid = url_obj.objid
138
+ objid
139
+ #=*> _.match(/\A[A-Za-z0-9\-]+\z/)
140
+
141
+ ## Different objects get different objids
142
+ obj1 = BasicObjectTest.new
143
+ obj2 = BasicObjectTest.new
144
+ obj1.objid != obj2.objid
145
+ #==> true
146
+
147
+ ## objid persists through save/load cycle
148
+ save_obj = BasicObjectTest.new
149
+ save_obj.id = 'save_test'
150
+ save_obj.name = 'Save Test'
151
+ original_objid = save_obj.objid
152
+ save_obj.save
153
+ loaded_obj = BasicObjectTest.new(id: 'save_test')
154
+ loaded_obj.objid == original_objid
155
+ #==> true
156
+
157
+ ## Class with different generator has different objid pattern
158
+ basic_obj = BasicObjectTest.new
159
+ hex_obj = HexTest.new
160
+ basic_obj.objid.include?('-') && !hex_obj.objid.include?('-')
161
+ #==> true
162
+
163
+ ## objid field type is ObjectIdentifierFieldType
164
+ BasicObjectTest.field_types[:objid]
165
+ #=:> Familia::Features::ObjectIdentifiers::ObjectIdentifierFieldType
166
+
167
+ ## Generator configuration is accessible through feature options
168
+ BasicObjectTest.feature_options(:object_identifiers)[:generator]
169
+ #=> :uuid_v7
170
+
171
+ ## UUID v4 class has correct generator configured
172
+ UuidV4Test.feature_options(:object_identifiers)[:generator]
173
+ #=> :uuid_v4
174
+
175
+ ## Hex class has correct generator configured
176
+ HexTest.feature_options(:object_identifiers)[:generator]
177
+ #=> :hex
178
+
179
+ ## Custom proc class has proc generator
180
+ CustomProcTest.feature_options(:object_identifiers)[:generator]
181
+ #=:> Proc
182
+
183
+ ## Empty initialization preserves nil objid for lazy generation
184
+ empty_obj = BasicObjectTest.new
185
+ empty_obj.instance_variable_get(:@objid)
186
+ #=> nil
187
+
188
+ ## Objid generation works with complex initialization
189
+ complex_obj = BasicObjectTest.new(id: 'complex', name: 'Complex Object')
190
+ complex_obj
191
+ #=*> _.objid
@@ -1,6 +1,6 @@
1
1
  # try/features/quantization_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6