familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -29,9 +29,9 @@ end
29
29
  ## Tampered auth tag fails decryption
30
30
  @model.secret = 'valid-secret'
31
31
  @valid_cipher = @model.secret.encrypted_value
32
- @tampered = JSON.parse(@valid_cipher)
32
+ @tampered = Familia::JsonSerializer.parse(@valid_cipher)
33
33
  @tampered['auth_tag'] = Base64.strict_encode64('tampered' * 4)
34
- @model.instance_variable_set(:@secret, @tampered.to_json)
34
+ @model.instance_variable_set(:@secret, Familia::JsonSerializer.dump(@tampered))
35
35
 
36
36
  @model.secret
37
37
  #=!> Familia::EncryptionError
@@ -58,7 +58,7 @@ Familia::Encryption.reset_derivation_count!
58
58
  # Ensure keys are available for this test
59
59
  Familia.config.encryption_keys = @test_keys
60
60
  @error_model = ErrorTest.new(id: 'err-counter')
61
- # Set valid JSON but with invalid base64 data to trigger decrypt failure after parsing
61
+ # UnsortedSet valid JSON but with invalid base64 data to trigger decrypt failure after parsing
62
62
  @error_model.instance_variable_set(:@secret, '{"algorithm":"aes-256-gcm","nonce":"dGVzdA==","ciphertext":"invalid-base64!!!","auth_tag":"dGVzdA==","key_version":"v1"}')
63
63
  begin
64
64
  @error_model.secret
@@ -72,9 +72,9 @@ Familia::Encryption.derivation_count.value
72
72
  # Ensure keys are available for this test
73
73
  Familia.config.encryption_keys = @test_keys
74
74
  @model.secret = 'test-data'
75
- @cipher_data = JSON.parse(@model.secret.encrypted_value)
75
+ @cipher_data = Familia::JsonSerializer.parse(@model.secret.encrypted_value)
76
76
  @cipher_data['algorithm'] = 'unsupported-algorithm'
77
- @model.instance_variable_set(:@secret, @cipher_data.to_json)
77
+ @model.instance_variable_set(:@secret, Familia::JsonSerializer.dump(@cipher_data))
78
78
 
79
79
  @model.secret
80
80
  #=!> Familia::EncryptionError
@@ -93,9 +93,9 @@ Familia.config.current_key_version = @original_version
93
93
  Familia.config.encryption_keys = @test_keys
94
94
  Familia.config.current_key_version = :v1
95
95
  @model.secret = 'test-data'
96
- @cipher_with_bad_version = JSON.parse(@model.secret.encrypted_value)
96
+ @cipher_with_bad_version = Familia::JsonSerializer.parse(@model.secret.encrypted_value)
97
97
  @cipher_with_bad_version['key_version'] = 'nonexistent'
98
- @model.instance_variable_set(:@secret, @cipher_with_bad_version.to_json)
98
+ @model.instance_variable_set(:@secret, Familia::JsonSerializer.dump(@cipher_with_bad_version))
99
99
  @model.secret
100
100
  #=!> Familia::EncryptionError
101
101
  #==> error.message.include?('No key for version: nonexistent')
@@ -162,7 +162,7 @@ internal_empty.nil?
162
162
  ## Consistent behavior across Ruby restart simulation
163
163
  model = PersistenceTestModel.new(user_id: 'persistence-test')
164
164
  model.persistent_data = 'data-to-persist'
165
- Thread.current[:familia_request_cache] = nil if Thread.current[:familia_request_cache]
165
+ Fiber[:familia_request_cache] = nil if Fiber[:familia_request_cache]
166
166
  # With secure-by-default, field access returns ConcealedString
167
167
  model.persistent_data.to_s
168
168
  #=> '[CONCEALED]'
@@ -20,7 +20,7 @@ end
20
20
 
21
21
  ## Multiple encryptions produce unique nonces (concealed behavior)
22
22
  model = NonceTest.new(id: 'nonce-test')
23
- concealed_values = Set.new
23
+ concealed_values = ::Set.new
24
24
 
25
25
  10.times do
26
26
  model.secret = 'same-value'
@@ -190,11 +190,11 @@ end
190
190
 
191
191
  ## JSON serialization prevents leakage by raising error
192
192
  begin
193
- user_json = {
193
+ user_json = Familia::JsonSerializer.dump({
194
194
  id: @user.id,
195
195
  username: @user.username,
196
196
  password: @user.password_hash
197
- }.to_json
197
+ })
198
198
  false
199
199
  rescue Familia::SerializerError
200
200
  true
@@ -203,11 +203,11 @@ end
203
203
 
204
204
  ## JSON serialization with ConcealedString raises error
205
205
  begin
206
- user_json = {
206
+ user_json = Familia::JsonSerializer.dump({
207
207
  id: @user.id,
208
208
  username: @user.username,
209
209
  password: @user.password_hash
210
- }.to_json
210
+ })
211
211
  false
212
212
  rescue Familia::SerializerError => e
213
213
  e.message.include?("ConcealedString")
@@ -294,7 +294,7 @@ api_response = {
294
294
  }
295
295
 
296
296
  begin
297
- @response_json = api_response.to_json
297
+ @response_json = Familia::JsonSerializer.dump(api_response)
298
298
  false
299
299
  rescue Familia::SerializerError
300
300
  true
@@ -311,7 +311,7 @@ api_response = {
311
311
  }
312
312
 
313
313
  begin
314
- @response_json = api_response.to_json
314
+ @response_json = Familia::JsonSerializer.dump(api_response)
315
315
  false
316
316
  rescue Familia::SerializerError
317
317
  true
@@ -328,7 +328,7 @@ api_response = {
328
328
  }
329
329
 
330
330
  begin
331
- @response_json = api_response.to_json
331
+ @response_json = Familia::JsonSerializer.dump(api_response)
332
332
  false
333
333
  rescue Familia::SerializerError => e
334
334
  e.message.include?("ConcealedString")
@@ -69,7 +69,7 @@ hash_result.keys.include?("api_token")
69
69
 
70
70
  ## JSON serialization - to_json (fails for security)
71
71
  begin
72
- @record.api_token.to_json
72
+ Familia::JsonSerializer.dump(@record.api_token)
73
73
  raise "Should have raised SerializerError"
74
74
  rescue Familia::SerializerError => e
75
75
  e.class
@@ -96,7 +96,7 @@ end
96
96
  @record.api_token.to_f
97
97
  #=!> NoMethodError
98
98
 
99
- ## Complex nested JSON structure
99
+ ## Nested JSON with ConcealedString raises error
100
100
  @nested_data = {
101
101
  record: @record,
102
102
  fields: {
@@ -104,23 +104,16 @@ end
104
104
  encrypted: [@record.api_token, @record.secret_notes]
105
105
  }
106
106
  }
107
+ Familia::JsonSerializer.dump(@nested_data)
108
+ #=!> Familia::SerializerError
109
+ #==> error.message.include?("Failed to dump")
107
110
 
108
- begin
109
- @serialized = @nested_data.to_json
110
- false
111
- rescue Familia::SerializerError
112
- true
113
- end
114
- #=> true
115
-
116
- ## Nested JSON with ConcealedString raises error
117
- begin
118
- @nested_data.to_json
119
- false
120
- rescue Familia::SerializerError => e
121
- e.message.include?("ConcealedString cannot be serialized")
122
- end
123
- #=> true
111
+ ## Nested JSON with ConcealedString raises error, plus
112
+ ## Oj strict mode prevents serialization of custom objects
113
+ Familia::JsonSerializer.dump(@record)
114
+ #=:> Familia::SerializerError
115
+ #==> error.message.include?("Failed to dump")
116
+ #==> error.message.include?("strict mode")
124
117
 
125
118
  ## Array of mixed field types safety
126
119
  @mixed_array = [
@@ -131,7 +124,7 @@ end
131
124
  ]
132
125
 
133
126
  begin
134
- @mixed_array.to_json
127
+ Familia::JsonSerializer.dump(@mixed_array)
135
128
  false
136
129
  rescue Familia::SerializerError
137
130
  true
@@ -140,7 +133,7 @@ end
140
133
 
141
134
  ## Mixed array with ConcealedString raises error
142
135
  begin
143
- @mixed_array.to_json
136
+ Familia::JsonSerializer.dump(@mixed_array)
144
137
  false
145
138
  rescue Familia::SerializerError => e
146
139
  e.message.include?("ConcealedString")
@@ -210,7 +210,7 @@ bug_test_obj.extid
210
210
  delete_test_obj = ExternalIdTest.new(id: 'delete_test', name: 'Delete Test')
211
211
  delete_test_obj.save
212
212
  test_extid = delete_test_obj.extid
213
- # Delete the object directly from Redis to simulate cleanup scenario
213
+ # Delete the object directly from Valkey/Redis to simulate cleanup scenario
214
214
  ExternalIdTest.dbclient.del(delete_test_obj.dbkey)
215
215
  # Now try to find by extid - this should clean up mapping and return nil
216
216
  ExternalIdTest.find_by_extid(test_extid)
@@ -7,7 +7,7 @@ Familia.debug = false
7
7
  # Create test features with dependencies for testing
8
8
  module TestFeatureA
9
9
  def self.included(base)
10
- Familia.trace :included, base, self, caller(1..1) if Familia.debug?
10
+ Familia.trace :included, base, self if Familia.debug?
11
11
  base.extend ClassMethods
12
12
  end
13
13
 
@@ -24,7 +24,7 @@ end
24
24
 
25
25
  module TestFeatureB
26
26
  def self.included(base)
27
- Familia.trace :INCLUDED, base, self, caller(1..1) if Familia.debug?
27
+ Familia.trace :INCLUDED, base, self if Familia.debug?
28
28
  base.extend ClassMethods
29
29
  end
30
30
 
@@ -41,7 +41,7 @@ end
41
41
 
42
42
  module TestFeatureCWithDeps
43
43
  def self.included(base)
44
- Familia.trace :feature_load, base, self, caller(1..1) if Familia.debug?
44
+ Familia.trace :feature_load, base, self if Familia.debug?
45
45
  base.extend ClassMethods
46
46
  end
47
47
 
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ # try/features/field_groups_try.rb
4
+
5
+ require_relative '../../lib/familia'
6
+
7
+ # Define test classes in setup section
8
+ class BasicUser < Familia::Horreum
9
+ field_group :personal_info do
10
+ field :name
11
+ field :email
12
+ end
13
+ end
14
+
15
+ class MultiGroupUser < Familia::Horreum
16
+ field_group :personal do
17
+ field :name
18
+ field :email
19
+ end
20
+
21
+ field_group :metadata do
22
+ field :created_at
23
+ field :updated_at
24
+ end
25
+ end
26
+
27
+ class EmptyGroupModel < Familia::Horreum
28
+ field_group :placeholder
29
+ end
30
+
31
+ class TransientModel < Familia::Horreum
32
+ feature :transient_fields
33
+ transient_field :api_key
34
+ transient_field :session_token
35
+ end
36
+
37
+ class EncryptedModel < Familia::Horreum
38
+ feature :encrypted_fields
39
+ encrypted_field :password
40
+ encrypted_field :credit_card
41
+ end
42
+
43
+ class MixedGroupsModel < Familia::Horreum
44
+ feature :transient_fields
45
+ transient_field :temp_data
46
+
47
+ field_group :custom do
48
+ field :custom_field
49
+ end
50
+
51
+ feature :encrypted_fields
52
+ encrypted_field :secret_key
53
+ end
54
+
55
+ class FieldsOutsideGroups < Familia::Horreum
56
+ field :standalone_field
57
+
58
+ field_group :grouped do
59
+ field :grouped_field
60
+ end
61
+ end
62
+
63
+ class NoSuchGroup < Familia::Horreum
64
+ field_group :existing do
65
+ field :name
66
+ end
67
+ end
68
+
69
+ class ParentModel < Familia::Horreum
70
+ field_group :base_fields do
71
+ field :id
72
+ end
73
+ end
74
+
75
+ class ChildModel < ParentModel
76
+ field_group :child_fields do
77
+ field :name
78
+ end
79
+ end
80
+
81
+ # Create instances for testing
82
+ @user = MultiGroupUser.new(name: 'Alice', email: 'alice@example.com', created_at: Time.now.to_i)
83
+ @user2 = BasicUser.new(name: 'Bob', email: 'bob@example.com')
84
+
85
+ ## Manual field groups - basic access via hash
86
+ BasicUser.instance_variable_get(:@field_groups)[:personal_info]
87
+ #=> [:name, :email]
88
+
89
+ ## Multiple groups - access personal group via hash
90
+ MultiGroupUser.instance_variable_get(:@field_groups)[:personal]
91
+ #=> [:name, :email]
92
+
93
+ ## Multiple groups - access metadata group via hash
94
+ MultiGroupUser.instance_variable_get(:@field_groups)[:metadata]
95
+ #=> [:created_at, :updated_at]
96
+
97
+ ## Multiple groups - list all field groups (returns hash)
98
+ MultiGroupUser.field_groups.keys.sort
99
+ #=> [:metadata, :personal]
100
+
101
+ ## Field groups - fields defined inside groups are tracked
102
+ user = MultiGroupUser.new(name: 'Alice', email: 'alice@example.com', created_at: Time.now.to_i)
103
+
104
+ ## Grouped fields - access name field
105
+ @user.name
106
+ #=> 'Alice'
107
+
108
+ ## Grouped fields - access email field
109
+ @user.email
110
+ #=> 'alice@example.com'
111
+
112
+ ## Empty group - access via hash
113
+ EmptyGroupModel.instance_variable_get(:@field_groups)[:placeholder]
114
+ #=> []
115
+
116
+ ## Empty group - list field groups (returns hash)
117
+ EmptyGroupModel.field_groups
118
+ #=> {placeholder: []}
119
+
120
+ ## Empty group - list field group keys
121
+ EmptyGroupModel.field_groups.keys
122
+ #=> [:placeholder]
123
+
124
+ ## Transient feature - access via backward compatible method
125
+ TransientModel.transient_fields
126
+ #=> [:api_key, :session_token]
127
+
128
+ ## Transient feature - access via field_groups hash
129
+ TransientModel.instance_variable_get(:@field_groups)[:transient_fields]
130
+ #=> [:api_key, :session_token]
131
+
132
+ ## Transient feature - field_groups returns hash with content
133
+ TransientModel.field_groups
134
+ #=> {transient_fields: [:api_key, :session_token]}
135
+
136
+ ## Transient feature - list field group keys
137
+ TransientModel.field_groups.keys
138
+ #=> [:transient_fields]
139
+
140
+ ## Encrypted feature - access via backward compatible method
141
+ EncryptedModel.encrypted_fields
142
+ #=> [:password, :credit_card]
143
+
144
+ ## Encrypted feature - access via field_groups hash
145
+ EncryptedModel.instance_variable_get(:@field_groups)[:encrypted_fields]
146
+ #=> [:password, :credit_card]
147
+
148
+ ## Encrypted feature - field_groups returns hash with content
149
+ EncryptedModel.field_groups
150
+ #=> {encrypted_fields: [:password, :credit_card]}
151
+
152
+ ## Encrypted feature - list field group keys
153
+ EncryptedModel.field_groups.keys
154
+ #=> [:encrypted_fields]
155
+
156
+ ## Mixed groups - list all field group keys
157
+ MixedGroupsModel.field_groups.keys.sort
158
+ #=> [:custom, :encrypted_fields, :transient_fields]
159
+
160
+ ## Mixed groups - access custom group via hash
161
+ MixedGroupsModel.instance_variable_get(:@field_groups)[:custom]
162
+ #=> [:custom_field]
163
+
164
+ ## Mixed groups - access transient_fields via backward compatible method
165
+ MixedGroupsModel.transient_fields
166
+ #=> [:temp_data]
167
+
168
+ ## Mixed groups - access encrypted_fields via backward compatible method
169
+ MixedGroupsModel.encrypted_fields
170
+ #=> [:secret_key]
171
+
172
+ ## Error: nested field groups
173
+ class NestedGroupsModel < Familia::Horreum
174
+ field_group :outer do
175
+ field_group :inner do
176
+ field :bad
177
+ end
178
+ end
179
+ end
180
+ #=!> Familia::Problem
181
+
182
+ ## Exception during field_group block resets @current_field_group
183
+ class ErrorDuringGroup < Familia::Horreum
184
+ begin
185
+ field_group :broken do
186
+ field :first_field
187
+ raise StandardError, "Simulated error"
188
+ field :unreachable_field
189
+ end
190
+ rescue StandardError
191
+ # Swallow the error for testing
192
+ end
193
+
194
+ # Field defined after the error should not be in :broken group
195
+ field :after_error
196
+ end
197
+
198
+ ErrorDuringGroup
199
+ #=> ErrorDuringGroup
200
+
201
+ ## Exception handling - broken group has only first_field
202
+ ErrorDuringGroup.instance_variable_get(:@field_groups)[:broken]
203
+ #=> [:first_field]
204
+
205
+ ## Exception handling - after_error field is not in broken group
206
+ ErrorDuringGroup.instance_variable_get(:@field_groups)[:broken].include?(:after_error)
207
+ #=> false
208
+
209
+ ## Exception handling - after_error is in fields list
210
+ ErrorDuringGroup.fields.include?(:after_error)
211
+ #=> true
212
+
213
+ ## Exception handling - current_field_group was reset to nil
214
+ ErrorDuringGroup.instance_variable_get(:@current_field_group)
215
+ #=> nil
216
+
217
+
218
+ ## Fields outside - access grouped field group via hash
219
+ FieldsOutsideGroups.instance_variable_get(:@field_groups)[:grouped]
220
+ #=> [:grouped_field]
221
+
222
+ ## Fields outside - all fields include both grouped and standalone
223
+ FieldsOutsideGroups.fields
224
+ #=> [:standalone_field, :grouped_field]
225
+
226
+ ## Accessing non-existent field group returns nil
227
+ NoSuchGroup.instance_variable_get(:@field_groups)[:nonexistent]
228
+ #=> nil
229
+
230
+ ## Inheritance - parent class has its own field groups
231
+ ParentModel.field_groups
232
+ #=> {base_fields: [:id]}
233
+
234
+ ## Inheritance - child class has its own field groups
235
+ ChildModel.field_groups
236
+ #=> {child_fields: [:name]}
237
+
238
+ ## Normal field access - get name value
239
+ @user2.name
240
+ #=> 'Bob'
241
+
242
+ ## Normal field access - get email value
243
+ @user2.email
244
+ #=> 'bob@example.com'
@@ -7,9 +7,9 @@ Familia.debug = false
7
7
  # Integration test for ObjectIdentifier and ExternalIdentifiers features together
8
8
 
9
9
  # Class using both features with defaults
10
- class IntegrationTest < Familia::Horreum
10
+ class ::IntegrationTest < Familia::Horreum
11
11
  feature :object_identifier
12
- feature :external_identifier # This depends on :object_identifier
12
+ feature :external_identifier # This depends on :object_identifier
13
13
  identifier_field :id
14
14
  field :id
15
15
  field :name
@@ -17,7 +17,7 @@ class IntegrationTest < Familia::Horreum
17
17
  end
18
18
 
19
19
  # Class with custom configurations for both features
20
- class CustomIntegrationTest < Familia::Horreum
20
+ class ::CustomIntegrationTest < Familia::Horreum
21
21
  feature :object_identifier, generator: :hex
22
22
  feature :external_identifier, prefix: 'custom'
23
23
  identifier_field :id
@@ -25,8 +25,8 @@ class CustomIntegrationTest < Familia::Horreum
25
25
  field :name
26
26
  end
27
27
 
28
- # Class testing full lifecycle with Redis persistence
29
- class PersistenceTest < Familia::Horreum
28
+ # Class testing full lifecycle with Valkey/Redis persistence
29
+ class ::PersistenceTest < Familia::Horreum
30
30
  feature :object_identifier
31
31
  feature :external_identifier
32
32
  identifier_field :id
@@ -79,7 +79,7 @@ obj.extid == original_extid
79
79
  #==> true
80
80
 
81
81
  ## Custom objid uses hex format (64 chars for 256-bit)
82
- @custom_obj.objid.match(/\A[0-9a-f]{64}\z/)
82
+ @custom_obj.objid =~ /\A[0-9a-f]{64}\z/
83
83
  #=*> nil
84
84
 
85
85
  ## Custom extid uses custom prefix
@@ -90,19 +90,20 @@ obj.extid == original_extid
90
90
  persistence_obj = PersistenceTest.new
91
91
  persistence_obj.id = 'persistence_test'
92
92
  persistence_obj.name = 'Persistence Test Object'
93
- persistence_obj.created_at = Time.now.to_i
93
+ persistence_obj.created_at = Familia.now.to_i
94
94
  original_objid = persistence_obj.objid
95
95
  original_extid = persistence_obj.extid
96
96
  persistence_obj.save
97
97
 
98
- # Load from Redis
99
- loaded_obj = PersistenceTest.new(id: 'persistence_test')
98
+ # Load from Valkey/Redis
99
+ loaded_obj = PersistenceTest.load(id: 'persistence_test')
100
+ #=> nil
100
101
 
101
102
  ## objid persists after save/load
102
103
  persistence_obj = PersistenceTest.new
103
104
  persistence_obj.id = 'persistence_test'
104
105
  persistence_obj.name = 'Persistence Test Object'
105
- persistence_obj.created_at = Time.now.to_i
106
+ persistence_obj.created_at = Familia.now.to_i
106
107
  original_objid = persistence_obj.objid
107
108
  persistence_obj.save
108
109
  loaded_obj = PersistenceTest.new(id: 'persistence_test')
@@ -113,7 +114,7 @@ loaded_obj.objid == original_objid
113
114
  persistence_obj = PersistenceTest.new
114
115
  persistence_obj.id = 'persistence_test'
115
116
  persistence_obj.name = 'Persistence Test Object'
116
- persistence_obj.created_at = Time.now.to_i
117
+ persistence_obj.created_at = Familia.now.to_i
117
118
  original_extid = persistence_obj.extid
118
119
  persistence_obj.save
119
120
  loaded_obj = PersistenceTest.new(id: 'persistence_test')
@@ -138,7 +139,7 @@ lazy_obj.instance_variable_get(:@extid)
138
139
 
139
140
  ## Accessing extid triggers objid generation if needed
140
141
  lazy_obj2 = IntegrationTest.new
141
- lazy_obj2.extid # This should trigger objid generation too
142
+ lazy_obj2.extid # This should trigger objid generation too
142
143
  lazy_obj2.instance_variable_get(:@objid)
143
144
  #=*> nil
144
145
 
@@ -182,12 +183,12 @@ CustomIntegrationTest.feature_options(:external_identifier)[:prefix]
182
183
 
183
184
  ## objid is URL-safe (UUID format)
184
185
  obj = IntegrationTest.new
185
- obj.objid.match(/\A[A-Za-z0-9\-]+\z/)
186
+ obj.objid =~ /\A[A-Za-z0-9-]+\z/
186
187
  #=*> nil
187
188
 
188
189
  ## extid is URL-safe (base36 format)
189
190
  obj = IntegrationTest.new
190
- obj.extid.match(/\A[a-z0-9_]+\z/)
191
+ obj.extid =~ /\A[a-z0-9_]+\z/
191
192
  #=*> nil
192
193
 
193
194
  ## Data integrity preserved during complex initialization
@@ -197,27 +198,16 @@ complex_obj = IntegrationTest.new(
197
198
  email: 'complex@test.com',
198
199
  objid: 'preset_objid_123',
199
200
  extid: 'preset_ext_456'
200
- )
201
+ ).save
202
+ #=> true
201
203
 
202
204
  ## Preset objid value is preserved
203
- complex_obj = IntegrationTest.new(
204
- id: 'complex_integration',
205
- name: 'Complex Integration',
206
- email: 'complex@test.com',
207
- objid: 'preset_objid_123',
208
- extid: 'preset_ext_456'
209
- )
205
+ complex_obj = IntegrationTest.load('complex_integration')
210
206
  complex_obj.objid
211
207
  #=> 'preset_objid_123'
212
208
 
213
209
  ## Preset extid value is preserved
214
- complex_obj = IntegrationTest.new(
215
- id: 'complex_integration',
216
- name: 'Complex Integration',
217
- email: 'complex@test.com',
218
- objid: 'preset_objid_123',
219
- extid: 'preset_ext_456'
220
- )
210
+ complex_obj = IntegrationTest.load('complex_integration')
221
211
  complex_obj.extid
222
212
  #=> 'preset_ext_456'
223
213
 
@@ -225,6 +215,7 @@ complex_obj.extid
225
215
  search_obj = IntegrationTest.new
226
216
  search_obj.id = 'search_test'
227
217
  search_obj.save
218
+ #=> true
228
219
 
229
220
  ## find_by_objid returns nil (stub implementation)
230
221
  search_obj = IntegrationTest.new
@@ -232,10 +223,10 @@ search_obj.id = 'search_test'
232
223
  search_obj.save
233
224
  found_by_objid = IntegrationTest.find_by_objid(search_obj.objid)
234
225
  found_by_objid
235
- #=> nil
226
+ #=:> IntegrationTest
236
227
 
237
228
  ## find_by_extid works with real implementation
238
- @search_obj = IntegrationTest.new
229
+ @search_obj = IntegrationTest.new name: 'Tucker', email: 'tucker@example.com'
239
230
  @search_obj.id = 'search_test'
240
231
  @search_obj.save
241
232
  found_by_extid = IntegrationTest.find_by_extid(@search_obj.extid)
@@ -248,8 +239,7 @@ first_objid = stability_obj.objid
248
239
  first_extid = stability_obj.extid
249
240
  second_objid = stability_obj.objid
250
241
  second_extid = stability_obj.extid
251
-
252
- ## objid remains stable across accesses
242
+ # objid remains stable across accesses
253
243
  stability_obj = IntegrationTest.new
254
244
  first_objid = stability_obj.objid
255
245
  second_objid = stability_obj.objid
@@ -284,4 +274,8 @@ obj.respond_to?(:delete!)
284
274
  #==> true
285
275
 
286
276
  # Cleanup test objects
287
- @search_obj.destroy! rescue nil
277
+ begin
278
+ @search_obj.destroy!
279
+ rescue StandardError
280
+ nil
281
+ end
@@ -189,3 +189,13 @@ empty_obj.instance_variable_get(:@objid)
189
189
  complex_obj = BasicObjectTest.new(id: 'complex', name: 'Complex Object')
190
190
  complex_obj
191
191
  #=*> _.objid
192
+
193
+ ## Test objid_lookup mapping when identifier set after objid generation (race condition fix)
194
+ # Create object without identifier, access objid first, then set identifier
195
+ race_obj = BasicObjectTest.new
196
+ generated_objid = race_obj.objid # Generate objid before setting identifier
197
+ race_obj.id = "race_test_123" # Set identifier after objid exists
198
+ race_obj.save # Save so find_by_objid can locate it
199
+ found = BasicObjectTest.find_by_objid(generated_objid)
200
+ found && found.id == "race_test_123"
201
+ #=> true
@@ -85,5 +85,5 @@ custom_stamp
85
85
  #=> "2023061514"
86
86
 
87
87
  # Cleanup
88
- @test_obj.id = 'quantized_test_obj' # Set identifier before cleanup
88
+ @test_obj.id = 'quantized_test_obj' # UnsortedSet identifier before cleanup
89
89
  @test_obj.destroy! if @test_obj