familia 2.0.0.pre3 → 2.0.0.pre5
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/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/class_methods_try.rb +27 -36
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +85 -18
@@ -0,0 +1,260 @@
|
|
1
|
+
# try/features/transient_fields_integration_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
class SecretService < Familia::Horreum
|
6
|
+
feature :transient_fields
|
7
|
+
identifier_field :service_id
|
8
|
+
|
9
|
+
field :service_id
|
10
|
+
field :name
|
11
|
+
field :endpoint_url
|
12
|
+
transient_field :api_key
|
13
|
+
transient_field :password
|
14
|
+
transient_field :secret_token, as: :token
|
15
|
+
end
|
16
|
+
|
17
|
+
@service = SecretService.new
|
18
|
+
@service.service_id = 'test_service_1'
|
19
|
+
@service.name = 'Test API Service'
|
20
|
+
@service.endpoint_url = 'https://api.example.com'
|
21
|
+
@service.api_key = 'sk-1234567890abcdef'
|
22
|
+
@service.password = 'super_secret_password'
|
23
|
+
@service.token = 'token-xyz789'
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
## Class includes transient_fields feature
|
28
|
+
SecretService.feature(:transient_fields)
|
29
|
+
defined?(SecretService.feature)
|
30
|
+
#=> "method"
|
31
|
+
|
32
|
+
## Class has correct field definitions
|
33
|
+
SecretService.fields.sort
|
34
|
+
#=> [:api_key, :endpoint_url, :name, :password, :secret_token, :service_id]
|
35
|
+
|
36
|
+
## Persistent fields exclude transient ones
|
37
|
+
SecretService.persistent_fields.sort
|
38
|
+
#=> [:endpoint_url, :name, :service_id]
|
39
|
+
|
40
|
+
## Transient field definitions have correct category
|
41
|
+
SecretService.field_types[:api_key].category
|
42
|
+
#=> :transient
|
43
|
+
|
44
|
+
## Password field definition has correct category
|
45
|
+
SecretService.field_types[:password].category
|
46
|
+
#=> :transient
|
47
|
+
|
48
|
+
## Secret token field definition has correct category
|
49
|
+
SecretService.field_types[:secret_token].category
|
50
|
+
#=> :transient
|
51
|
+
|
52
|
+
## Regular field definition has correct category
|
53
|
+
SecretService.field_types[:name].category
|
54
|
+
#=> :field
|
55
|
+
|
56
|
+
## Transient field stores RedactedString object for api_key
|
57
|
+
@service.api_key.class
|
58
|
+
#=> RedactedString
|
59
|
+
|
60
|
+
## Transient field stores RedactedString object for password
|
61
|
+
@service.password.class
|
62
|
+
#=> RedactedString
|
63
|
+
|
64
|
+
## Transient field stores RedactedString object for token alias
|
65
|
+
@service.token.class
|
66
|
+
#=> RedactedString
|
67
|
+
|
68
|
+
## Regular field stores normal string value for name
|
69
|
+
@service.name.class
|
70
|
+
#=> String
|
71
|
+
|
72
|
+
## Regular field stores normal string value for endpoint_url
|
73
|
+
@service.endpoint_url.class
|
74
|
+
#=> String
|
75
|
+
|
76
|
+
## Transient field value is redacted in string representation
|
77
|
+
@service.api_key.to_s
|
78
|
+
#=> "[REDACTED]"
|
79
|
+
|
80
|
+
## Transient field value is redacted in inspect output
|
81
|
+
@service.password.inspect
|
82
|
+
#=> "[REDACTED]"
|
83
|
+
|
84
|
+
## Transient field can expose value securely through block
|
85
|
+
result = nil
|
86
|
+
@service.api_key.expose { |val| result = val.dup }
|
87
|
+
result
|
88
|
+
#=> "sk-1234567890abcdef"
|
89
|
+
|
90
|
+
## Transient field with custom method name exposes value correctly
|
91
|
+
result = nil
|
92
|
+
@service.token.expose { |val| result = val.dup }
|
93
|
+
result
|
94
|
+
#=> "token-xyz789"
|
95
|
+
|
96
|
+
## Setting transient field with existing RedactedString works
|
97
|
+
already_redacted = RedactedString.new("already_wrapped")
|
98
|
+
@service.password = already_redacted
|
99
|
+
@service.password.class
|
100
|
+
#=> RedactedString
|
101
|
+
|
102
|
+
## Serialization to_h only includes persistent fields
|
103
|
+
hash_result = @service.to_h
|
104
|
+
hash_result.keys.sort
|
105
|
+
#=> [:endpoint_url, :name, :service_id]
|
106
|
+
|
107
|
+
## Serialization to_h excludes api_key transient field
|
108
|
+
hash_result = @service.to_h
|
109
|
+
hash_result.key?("api_key")
|
110
|
+
#=> false
|
111
|
+
|
112
|
+
## Serialization to_h excludes password transient field
|
113
|
+
hash_result = @service.to_h
|
114
|
+
hash_result.key?("password")
|
115
|
+
#=> false
|
116
|
+
|
117
|
+
## Serialization to_h excludes secret_token transient field
|
118
|
+
hash_result = @service.to_h
|
119
|
+
hash_result.key?("secret_token")
|
120
|
+
#=> false
|
121
|
+
|
122
|
+
## Serialization to_a only includes persistent field values
|
123
|
+
array_result = @service.to_a
|
124
|
+
array_result.length
|
125
|
+
#=> 3
|
126
|
+
|
127
|
+
## Array contains values in persistent field order
|
128
|
+
array_result = @service.to_a
|
129
|
+
persistent_fields_values = SecretService.persistent_fields.map { |f| @service.send(SecretService.field_types[f].method_name) }
|
130
|
+
array_result == persistent_fields_values
|
131
|
+
#=> true
|
132
|
+
|
133
|
+
## Database persistence only stores persistent fields
|
134
|
+
@service.save
|
135
|
+
raw_data = @service.hgetall
|
136
|
+
raw_data.keys.sort
|
137
|
+
#=> ["endpoint_url", "name", "service_id"]
|
138
|
+
|
139
|
+
## Raw Database data excludes api_key transient field
|
140
|
+
raw_data = @service.hgetall
|
141
|
+
raw_data.key?("api_key")
|
142
|
+
#=> false
|
143
|
+
|
144
|
+
## Raw Database data excludes password transient field
|
145
|
+
raw_data = @service.hgetall
|
146
|
+
raw_data.key?("password")
|
147
|
+
#=> false
|
148
|
+
|
149
|
+
## Raw Database data excludes secret_token transient field
|
150
|
+
raw_data = @service.hgetall
|
151
|
+
raw_data.key?("secret_token")
|
152
|
+
#=> false
|
153
|
+
|
154
|
+
## Database refresh only loads persistent fields name
|
155
|
+
fresh_service = SecretService.new
|
156
|
+
fresh_service.service_id = 'test_service_1'
|
157
|
+
fresh_service.refresh!
|
158
|
+
fresh_service.name
|
159
|
+
#=> "Test API Service"
|
160
|
+
|
161
|
+
## Database refresh only loads persistent fields endpoint_url
|
162
|
+
fresh_service = SecretService.new
|
163
|
+
fresh_service.service_id = 'test_service_1'
|
164
|
+
fresh_service.refresh!
|
165
|
+
fresh_service.endpoint_url
|
166
|
+
#=> "https://api.example.com"
|
167
|
+
|
168
|
+
## Database refresh leaves transient api_key as nil
|
169
|
+
fresh_service = SecretService.new
|
170
|
+
fresh_service.service_id = 'test_service_1'
|
171
|
+
fresh_service.refresh!
|
172
|
+
fresh_service.api_key
|
173
|
+
#=> nil
|
174
|
+
|
175
|
+
## Database refresh leaves transient password as nil
|
176
|
+
fresh_service = SecretService.new
|
177
|
+
fresh_service.service_id = 'test_service_1'
|
178
|
+
fresh_service.refresh!
|
179
|
+
fresh_service.password
|
180
|
+
#=> nil
|
181
|
+
|
182
|
+
## Database refresh leaves transient token as nil
|
183
|
+
fresh_service = SecretService.new
|
184
|
+
fresh_service.service_id = 'test_service_1'
|
185
|
+
fresh_service.refresh!
|
186
|
+
fresh_service.token
|
187
|
+
#=> nil
|
188
|
+
|
189
|
+
## String interpolation with transient field shows redacted value
|
190
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
191
|
+
log_message.include?("[REDACTED]")
|
192
|
+
#=> true
|
193
|
+
|
194
|
+
## String interpolation with transient field hides actual value
|
195
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
196
|
+
log_message.include?("sk-1234567890abcdef")
|
197
|
+
#=> false
|
198
|
+
|
199
|
+
## Hash containing transient field shows redacted in string output
|
200
|
+
config_hash = {
|
201
|
+
service: @service.name,
|
202
|
+
key: @service.api_key,
|
203
|
+
url: @service.endpoint_url
|
204
|
+
}
|
205
|
+
config_hash.to_s.include?("[REDACTED]")
|
206
|
+
#=> true
|
207
|
+
|
208
|
+
## Hash containing transient field hides actual value in string output
|
209
|
+
config_hash = {
|
210
|
+
service: @service.name,
|
211
|
+
key: @service.api_key,
|
212
|
+
url: @service.endpoint_url
|
213
|
+
}
|
214
|
+
config_hash.to_s.include?("sk-1234567890abcdef")
|
215
|
+
#=> false
|
216
|
+
|
217
|
+
## Exception messages with transient fields are safe
|
218
|
+
begin
|
219
|
+
raise StandardError, "Failed to authenticate with key: #{@service.api_key}"
|
220
|
+
rescue => e
|
221
|
+
e.message.include?("[REDACTED]")
|
222
|
+
end
|
223
|
+
#=> true
|
224
|
+
|
225
|
+
## Multiple transient field assignment creates RedactedString instances
|
226
|
+
new_service = SecretService.new
|
227
|
+
new_service.service_id = 'test_service_2'
|
228
|
+
new_service.name = 'Another Service'
|
229
|
+
new_service.api_key = 'new-api-key-123'
|
230
|
+
new_service.password = 'new-password-456'
|
231
|
+
new_service.token = 'new-token-789'
|
232
|
+
[new_service.api_key, new_service.password, new_service.token].all? { |f| f.is_a?(RedactedString) }
|
233
|
+
#=> true
|
234
|
+
|
235
|
+
## Transient field can be set to nil value
|
236
|
+
new_service = SecretService.new
|
237
|
+
new_service.api_key = nil
|
238
|
+
new_service.api_key
|
239
|
+
#=> nil
|
240
|
+
|
241
|
+
## Persistent field definitions are correctly identified
|
242
|
+
SecretService.field_types.values.select(&:persistent?).map(&:name).sort
|
243
|
+
#=> [:endpoint_url, :name, :service_id]
|
244
|
+
|
245
|
+
## Transient field definitions are correctly identified
|
246
|
+
transient_fields = SecretService.field_types.values.reject(&:persistent?).map(&:name).sort
|
247
|
+
transient_fields
|
248
|
+
#=> [:api_key, :password, :secret_token]
|
249
|
+
|
250
|
+
|
251
|
+
# TEARDOWN
|
252
|
+
|
253
|
+
# Clean up Database
|
254
|
+
@service.destroy! if @service.exists?
|
255
|
+
|
256
|
+
# Clean up any test objects
|
257
|
+
@service = nil
|
258
|
+
|
259
|
+
# Force garbage collection to trigger any finalizers
|
260
|
+
GC.start
|
data/try/helpers/test_helpers.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# e.g. FAMILIA_TRACE=1 FAMILIA_DEBUG=1 bundle exec try
|
6
6
|
|
7
7
|
require 'digest'
|
8
|
+
|
8
9
|
require_relative '../../lib/familia'
|
9
10
|
|
10
11
|
Familia.enable_database_logging = true
|
@@ -162,3 +163,44 @@ class Limiter < Familia::Horreum
|
|
162
163
|
@name
|
163
164
|
end
|
164
165
|
end
|
166
|
+
|
167
|
+
# # In test:
|
168
|
+
# using RedactedStringTestHelper
|
169
|
+
|
170
|
+
# secret = RedactedString.new("test-key")
|
171
|
+
# expect(secret.raw).to eq("test-key")
|
172
|
+
#
|
173
|
+
# Or with rack
|
174
|
+
#
|
175
|
+
# post '/vault' do
|
176
|
+
# passphrase = RedactedString.new(request.params['passphrase'])
|
177
|
+
# passphrase.expose do |plain|
|
178
|
+
# vault.unlock(plain)
|
179
|
+
# end
|
180
|
+
# # passphrase wiped
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# NOTE: This will do nothing unless RedactedString is already requried
|
184
|
+
unless defined?(RedactedString)
|
185
|
+
require_relative '../../lib/familia/features/transient_fields/redacted_string'
|
186
|
+
end
|
187
|
+
module RedactedStringTestHelper
|
188
|
+
refine RedactedString do
|
189
|
+
def raw
|
190
|
+
# Only available when refinement is used
|
191
|
+
@value
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
unless defined?(SingleUseRedactedString)
|
197
|
+
require_relative '../../lib/familia/features/transient_fields/single_use_redacted_string'
|
198
|
+
end
|
199
|
+
module SingleUseRedactedStringTestHelper
|
200
|
+
refine SingleUseRedactedString do
|
201
|
+
def raw
|
202
|
+
# Only available when refinement is used
|
203
|
+
@value
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
data/try/horreum/base_try.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# try/horreum/base_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
7
6
|
|
8
|
-
@identifier = 'tryouts-27@onetimesecret.
|
7
|
+
@identifier = 'tryouts-27@onetimesecret.dev'
|
9
8
|
@customer = Customer.new @identifier
|
10
9
|
@hashkey = Familia::HashKey.new 'tryouts-27'
|
11
10
|
|
@@ -55,7 +54,7 @@ Familia.debug = false
|
|
55
54
|
## Horreum object fields have a fast attribute method (1 of 2)
|
56
55
|
Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
|
57
56
|
@customer.name! 'Jane Doe'
|
58
|
-
#=>
|
57
|
+
#=> true
|
59
58
|
|
60
59
|
## Horreum object fields have a fast attribute method (2 of 2)
|
61
60
|
@customer.refresh!
|
@@ -116,3 +115,158 @@ class ArrayIdentifierTest < Familia::Horreum
|
|
116
115
|
field :name
|
117
116
|
end
|
118
117
|
#=!> Familia::Problem
|
118
|
+
|
119
|
+
## Redefining a field method after it can give a warning
|
120
|
+
class FieldRedefine < Familia::Horreum
|
121
|
+
identifier_field :email
|
122
|
+
field :name
|
123
|
+
field :uniquefieldname, on_conflict: :warn
|
124
|
+
|
125
|
+
def uniquefieldname
|
126
|
+
true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
#=2> /WARNING/
|
130
|
+
#=2> /uniquefieldname/
|
131
|
+
|
132
|
+
## Defining a field with the same name as an existing method can give a warning
|
133
|
+
class ::FieldRedefine2 < Familia::Horreum
|
134
|
+
identifier_field :email
|
135
|
+
field :name
|
136
|
+
|
137
|
+
def uniquefieldname
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
field :uniquefieldname, on_conflict: :warn
|
142
|
+
end
|
143
|
+
#=2> /WARNING/
|
144
|
+
#=2> /uniquefieldname/
|
145
|
+
|
146
|
+
## Redefining a field method after it can raise an error
|
147
|
+
class FieldRedefine3 < Familia::Horreum
|
148
|
+
identifier_field :email
|
149
|
+
field :name
|
150
|
+
field :uniquefieldname, on_conflict: :raise
|
151
|
+
|
152
|
+
def uniquefieldname
|
153
|
+
true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
#=!> ArgumentError
|
157
|
+
|
158
|
+
## Defining a field with the same name as an existing method can raise an error
|
159
|
+
class FieldRedefine4 < Familia::Horreum
|
160
|
+
identifier_field :email
|
161
|
+
field :name
|
162
|
+
|
163
|
+
def uniquefieldname
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
field :uniquefieldname, on_conflict: :raise
|
168
|
+
end
|
169
|
+
#=!> ArgumentError
|
170
|
+
|
171
|
+
## Field aliasing works with 'as' parameter
|
172
|
+
class AliasedFieldTest < Familia::Horreum
|
173
|
+
identifier_field :email
|
174
|
+
field :email
|
175
|
+
field :display_size, as: :width
|
176
|
+
end
|
177
|
+
@aliased = AliasedFieldTest.new email: 'test@example.com'
|
178
|
+
@aliased.width = 42
|
179
|
+
@aliased.width
|
180
|
+
#=> 42
|
181
|
+
|
182
|
+
## Aliased field getter method uses alias name
|
183
|
+
@aliased.respond_to?(:width)
|
184
|
+
#=> true
|
185
|
+
|
186
|
+
## Aliased field setter method uses alias name
|
187
|
+
@aliased.respond_to?(:width=)
|
188
|
+
#=> true
|
189
|
+
|
190
|
+
## Original field name is not accessible as method
|
191
|
+
@aliased.respond_to?(:display_size)
|
192
|
+
#=> false
|
193
|
+
|
194
|
+
## Aliased field fast method works correctly
|
195
|
+
@aliased.save
|
196
|
+
@aliased.display_size! 100
|
197
|
+
#=> true
|
198
|
+
|
199
|
+
## Aliased field refresh works correctly
|
200
|
+
@aliased.width = 50 # unsaved change
|
201
|
+
@aliased.refresh!
|
202
|
+
@aliased.width
|
203
|
+
#=> "100"
|
204
|
+
|
205
|
+
## Fast method with custom name
|
206
|
+
class CustomFastMethodTest < Familia::Horreum
|
207
|
+
identifier_field :email
|
208
|
+
field :score, fast_method: :score_now!
|
209
|
+
field :email
|
210
|
+
end
|
211
|
+
@custom_fast = CustomFastMethodTest.new email: 'fast@example.com'
|
212
|
+
@custom_fast.respond_to?(:score_now!)
|
213
|
+
#=> true
|
214
|
+
|
215
|
+
## Custom fast method works
|
216
|
+
@custom_fast.save
|
217
|
+
@custom_fast.score_now! 75
|
218
|
+
#=> true
|
219
|
+
|
220
|
+
## Field with :warn conflict handling allows redefinition with warning
|
221
|
+
class WarnConflictTest < Familia::Horreum
|
222
|
+
identifier_field :email
|
223
|
+
field :email
|
224
|
+
field :test_method, on_conflict: :warn
|
225
|
+
|
226
|
+
def test_method
|
227
|
+
"original"
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
@warn_test = WarnConflictTest.new email: 'warn@example.com'
|
232
|
+
@warn_test.test_method
|
233
|
+
#=> "original"
|
234
|
+
|
235
|
+
## Field with :skip conflict handling skips redefinition silently
|
236
|
+
class SkipConflictTest < Familia::Horreum
|
237
|
+
identifier_field :email
|
238
|
+
field :email
|
239
|
+
|
240
|
+
def skip_method
|
241
|
+
"original"
|
242
|
+
end
|
243
|
+
|
244
|
+
field :skip_method, on_conflict: :skip
|
245
|
+
end
|
246
|
+
@skip_test = SkipConflictTest.new email: 'skip@example.com'
|
247
|
+
@skip_test.skip_method
|
248
|
+
#=> "original"
|
249
|
+
|
250
|
+
## Combined aliasing and custom fast method
|
251
|
+
class CombinedTest < Familia::Horreum
|
252
|
+
identifier_field :email
|
253
|
+
field :internal_count, as: :count, fast_method: :count_immediately!
|
254
|
+
field :email
|
255
|
+
end
|
256
|
+
@combined = CombinedTest.new email: 'combined1@example.com'
|
257
|
+
@combined.count = 10
|
258
|
+
@combined.count
|
259
|
+
#=> 10
|
260
|
+
|
261
|
+
## Combined test fast method works
|
262
|
+
@combined.save
|
263
|
+
@combined.count_immediately! 20
|
264
|
+
@combined.count
|
265
|
+
#=> 20
|
266
|
+
|
267
|
+
## Combined test refresh works
|
268
|
+
combined = CombinedTest.new email: 'combined2@example.com'
|
269
|
+
combined.count = 5 # unsaved change
|
270
|
+
combined.refresh!
|
271
|
+
combined.count
|
272
|
+
#=> 5
|
@@ -1,47 +1,38 @@
|
|
1
|
+
# try/horreum/class_methods_try.rb
|
2
|
+
|
1
3
|
# Test Horreum class methods
|
2
4
|
|
3
5
|
require_relative '../helpers/test_helpers'
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
field :name
|
11
|
-
field :age
|
12
|
-
end
|
13
|
-
|
14
|
-
result = user_class.respond_to?(:create) && user_class.respond_to?(:exists?)
|
15
|
-
result
|
16
|
-
rescue StandardError => e
|
17
|
-
false
|
7
|
+
TestUser = Class.new(Familia::Horreum) do
|
8
|
+
identifier_field :email
|
9
|
+
field :email
|
10
|
+
field :name
|
11
|
+
field :age
|
18
12
|
end
|
19
|
-
#=> true
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
user_class = Class.new(Familia::Horreum) do
|
24
|
-
identifier_field :email
|
25
|
-
field :email
|
26
|
-
field :name
|
14
|
+
module AnotherModuleName
|
15
|
+
AnotherTestUser = Class.new(Familia::Horreum) do
|
27
16
|
end
|
28
|
-
|
29
|
-
user_class.respond_to?(:multiget)
|
30
|
-
rescue StandardError => e
|
31
|
-
false
|
32
17
|
end
|
33
|
-
|
18
|
+
|
19
|
+
## create factory method with existence checking
|
20
|
+
TestUser
|
21
|
+
#==> _.respond_to?(:create)
|
22
|
+
#==> _.respond_to?(:exists?)
|
23
|
+
|
24
|
+
## multiget method is available
|
25
|
+
TestUser
|
26
|
+
#==> _.respond_to?(:multiget)
|
34
27
|
|
35
28
|
## find_keys method is available
|
36
|
-
|
37
|
-
|
38
|
-
identifier_field :email
|
39
|
-
field :email
|
40
|
-
field :name
|
41
|
-
end
|
29
|
+
TestUser
|
30
|
+
#==> _.respond_to?(:find_keys)
|
42
31
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
32
|
+
## config name turns a top-level class into a symbol
|
33
|
+
TestUser.config_name.to_sym
|
34
|
+
#=> :test_user
|
35
|
+
|
36
|
+
## config name turns the fully qualified class into a symbol, but just the right most class
|
37
|
+
AnotherModuleName::AnotherTestUser.config_name.to_sym
|
38
|
+
#=> :another_test_user
|