familia 2.0.0.pre7 → 2.0.0.pre10
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/.github/workflows/ci.yml +13 -0
- data/.github/workflows/docs.yml +1 -1
- data/.gitignore +9 -9
- data/.rubocop.yml +19 -0
- data/.yardopts +22 -1
- data/CHANGELOG.md +184 -0
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +97 -2
- data/changelog.d/README.md +66 -0
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +29 -0
- data/docs/archive/.gitignore +2 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
- data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
- data/docs/archive/FAMILIA_UPDATE.md +226 -0
- data/docs/archive/README.md +67 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -0
- data/examples/relationships_basic.rb +90 -157
- data/familia.gemspec +4 -4
- data/lib/familia/connection.rb +4 -21
- 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 +160 -176
- data/lib/familia/features/relationships/membership.rb +16 -22
- 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 +61 -22
- data/lib/familia/features/relationships.rb +15 -8
- 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 +36 -0
- data/lib/familia/horreum.rb +15 -24
- data/lib/familia/version.rb +1 -3
- data/setup.cfg +12 -0
- 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/relationships_api_changes_try.rb +339 -0
- 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} +7 -6
- 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 +80 -60
- /data/docs/{wiki → guides}/API-Reference.md +0 -0
- /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
- /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
- /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
- /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
- /data/docs/{wiki → guides}/Home.md +0 -0
- /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
- /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Security-Model.md +0 -0
- /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
- /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
@@ -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
|
data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb}
RENAMED
@@ -4,7 +4,7 @@
|
|
4
4
|
# Validates the implementation of categorical permission management with
|
5
5
|
# two-stage filtering pattern for efficient permission-based queries.
|
6
6
|
|
7
|
-
require_relative '
|
7
|
+
require_relative '../../helpers/test_helpers'
|
8
8
|
|
9
9
|
# Categorical Permission System Setup
|
10
10
|
|