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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. 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
@@ -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
@@ -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.com'
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
- #=> 0
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
- ## create factory method with existence checking
6
- begin
7
- user_class = Class.new(Familia::Horreum) do
8
- identifier_field :email
9
- field :email
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
- ## multiget method is available
22
- begin
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
- #=> true
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
- begin
37
- user_class = Class.new(Familia::Horreum) do
38
- identifier_field :email
39
- field :email
40
- field :name
41
- end
29
+ TestUser
30
+ #==> _.respond_to?(:find_keys)
42
31
 
43
- user_class.respond_to?(:find_keys)
44
- rescue StandardError => e
45
- false
46
- end
47
- #=> true
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