familia 2.0.0.pre15 → 2.0.0.pre16

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,85 @@
1
+ # lib/familia/distinguisher.rb
2
+
3
+ module Familia
4
+ module Distinguisher
5
+ # This method determines the appropriate transformation to apply based on
6
+ # the class of the input argument.
7
+ #
8
+ # @param [Object] value_to_distinguish The value to be processed. Keep in
9
+ # mind that all data is stored as a string so whatever the type
10
+ # of the value, it will be converted to a string.
11
+ # @param [Boolean] strict_values Whether to enforce strict value handling.
12
+ # Defaults to true.
13
+ # @return [String, nil] The processed value as a string or nil for unsupported
14
+ # classes.
15
+ #
16
+ # The method uses a case statement to handle different classes:
17
+ # - For `Symbol`, `String`, `Integer`, and `Float` classes, it traces the
18
+ # operation and converts the value to a string.
19
+ # - For `Familia::Horreum` class, it traces the operation and returns the
20
+ # identifier of the value.
21
+ # - For `TrueClass`, `FalseClass`, and `NilClass`, it traces the operation and
22
+ # converts the value to a string ("true", "false", or "").
23
+ # - For any other class, it traces the operation and returns nil.
24
+ #
25
+ # Alternative names for `value_to_distinguish` could be `input_value`, `value`,
26
+ # or `object`.
27
+ #
28
+ def distinguisher(value_to_distinguish, strict_values: true)
29
+ case value_to_distinguish
30
+ when ::Symbol, ::String, ::Integer, ::Float
31
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'string' if Familia.debug?
32
+
33
+ # Symbols and numerics are naturally serializable to strings
34
+ # so it's a relatively low risk operation.
35
+ value_to_distinguish.to_s
36
+
37
+ when ::TrueClass, ::FalseClass, ::NilClass
38
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'true/false/nil' if Familia.debug?
39
+
40
+ # TrueClass, FalseClass, and NilClass are considered high risk because their
41
+ # original types cannot be reliably determined from their serialized string
42
+ # representations. This can lead to unexpected behavior during deserialization.
43
+ # For instance, a TrueClass value serialized as "true" might be deserialized as
44
+ # a String, causing application errors. Even more problematic, a NilClass value
45
+ # serialized as an empty string makes it impossible to distinguish between a
46
+ # nil value and an empty string upon deserialization. Such scenarios can result
47
+ # in subtle, hard-to-diagnose bugs. To mitigate these risks, we raise an
48
+ # exception when encountering these types unless the strict_values option is
49
+ # explicitly set to false.
50
+ #
51
+ raise Familia::NotDistinguishableError, value_to_distinguish if strict_values
52
+
53
+ value_to_distinguish.to_s #=> "true", "false", ""
54
+
55
+ when Familia::Base, Class
56
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'base' if Familia.debug?
57
+
58
+ # When called with a class we simply transform it to its name. For
59
+ # instances of Familia class, we store the identifier.
60
+ if value_to_distinguish.is_a?(Class)
61
+ value_to_distinguish.name
62
+ else
63
+ value_to_distinguish.identifier
64
+ end
65
+
66
+ else
67
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, "else1 #{strict_values}" if Familia.debug?
68
+
69
+ if value_to_distinguish.class.ancestors.member?(Familia::Base)
70
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'isabase' if Familia.debug?
71
+
72
+ value_to_distinguish.identifier
73
+
74
+ else
75
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, "else2 #{strict_values}" if Familia.debug?
76
+ raise Familia::NotDistinguishableError, value_to_distinguish if strict_values
77
+
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ extend Distinguisher
85
+ end
@@ -5,15 +5,15 @@ module Familia
5
5
  EncryptedData = Data.define(:algorithm, :nonce, :ciphertext, :auth_tag, :key_version) do
6
6
  # Class methods for parsing and validation
7
7
  def self.valid?(json_string)
8
- return true if json_string.nil? # Allow nil values
9
- return false unless json_string.kind_of?(::String)
8
+ return true if json_string.nil? # Allow nil values
9
+ return false unless json_string.is_a?(::String)
10
10
 
11
11
  begin
12
12
  parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
13
13
  return false unless parsed.is_a?(Hash)
14
14
 
15
15
  # Check for required fields
16
- required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
16
+ required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
17
17
  result = required_fields.all? { |field| parsed.key?(field) }
18
18
  Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
19
19
  result
@@ -26,9 +26,7 @@ module Familia
26
26
  def self.validate!(json_string)
27
27
  return nil if json_string.nil?
28
28
 
29
- unless json_string.kind_of?(::String)
30
- raise EncryptionError, "Expected JSON string, got #{json_string.class}"
31
- end
29
+ raise EncryptionError, "Expected JSON string, got #{json_string.class}" unless json_string.is_a?(::String)
32
30
 
33
31
  begin
34
32
  parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
@@ -36,16 +34,12 @@ module Familia
36
34
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
37
35
  end
38
36
 
39
- unless parsed.is_a?(Hash)
40
- raise EncryptionError, "Expected JSON object, got #{parsed.class}"
41
- end
37
+ raise EncryptionError, "Expected JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
42
38
 
43
- required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
39
+ required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
44
40
  missing_fields = required_fields.reject { |field| parsed.key?(field) }
45
41
 
46
- unless missing_fields.empty?
47
- raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}"
48
- end
42
+ raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
49
43
 
50
44
  new(**parsed)
51
45
  end
@@ -77,16 +71,12 @@ module Familia
77
71
  end
78
72
 
79
73
  def validate_decryptable!
80
- unless algorithm
81
- raise EncryptionError, "Missing algorithm field"
82
- end
74
+ raise EncryptionError, 'Missing algorithm field' unless algorithm
83
75
 
84
76
  # Ensure Registry is set up before checking algorithms
85
77
  Registry.setup! if Registry.providers.empty?
86
78
 
87
- unless Registry.providers.key?(algorithm)
88
- raise EncryptionError, "Unsupported algorithm: #{algorithm}"
89
- end
79
+ raise EncryptionError, "Unsupported algorithm: #{algorithm}" unless Registry.providers.key?(algorithm)
90
80
 
91
81
  unless nonce && ciphertext && auth_tag && key_version
92
82
  missing = []
@@ -107,22 +97,23 @@ module Familia
107
97
  raise EncryptionError, "Invalid nonce size: expected #{provider.nonce_size}, got #{decoded_nonce.bytesize}"
108
98
  end
109
99
  rescue ArgumentError
110
- raise EncryptionError, "Invalid Base64 encoding in nonce field"
100
+ raise EncryptionError, 'Invalid Base64 encoding in nonce field'
111
101
  end
112
102
 
113
103
  begin
114
- Base64.strict_decode64(ciphertext) # ciphertext can be variable size
104
+ Base64.strict_decode64(ciphertext) # ciphertext can be variable size
115
105
  rescue ArgumentError
116
- raise EncryptionError, "Invalid Base64 encoding in ciphertext field"
106
+ raise EncryptionError, 'Invalid Base64 encoding in ciphertext field'
117
107
  end
118
108
 
119
109
  begin
120
110
  decoded_auth_tag = Base64.strict_decode64(auth_tag)
121
111
  if decoded_auth_tag.bytesize != provider.auth_tag_size
122
- raise EncryptionError, "Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
112
+ raise EncryptionError,
113
+ "Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
123
114
  end
124
115
  rescue ArgumentError
125
- raise EncryptionError, "Invalid Base64 encoding in auth_tag field"
116
+ raise EncryptionError, 'Invalid Base64 encoding in auth_tag field'
126
117
  end
127
118
 
128
119
  # Validate that the key version exists
@@ -39,7 +39,8 @@ module Familia
39
39
  Familia::Encryption.derivation_count.increment
40
40
 
41
41
  begin
42
- data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json, symbolize_names: true))
42
+ data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
43
+ symbolize_names: true))
43
44
 
44
45
  # Validate algorithm support
45
46
  provider = Registry.get(data.algorithm)
@@ -67,15 +68,16 @@ module Familia
67
68
  def decode_and_validate(encoded, expected_size, component)
68
69
  decoded = Base64.strict_decode64(encoded)
69
70
  raise EncryptionError, 'Invalid encrypted data' unless decoded.bytesize == expected_size
71
+
70
72
  decoded
71
- rescue ArgumentError => e
73
+ rescue ArgumentError
72
74
  raise EncryptionError, "Invalid Base64 encoding in #{component} field"
73
75
  end
74
76
 
75
77
  def decode_and_validate_ciphertext(encoded)
76
78
  Base64.strict_decode64(encoded)
77
- rescue ArgumentError => e
78
- raise EncryptionError, "Invalid Base64 encoding in ciphertext field"
79
+ rescue ArgumentError
80
+ raise EncryptionError, 'Invalid Base64 encoding in ciphertext field'
79
81
  end
80
82
 
81
83
  def derive_key(context, version: nil, provider: nil)
@@ -49,7 +49,7 @@ module Familia
49
49
  {
50
50
  ciphertext: ciphertext,
51
51
  auth_tag: cipher.auth_tag,
52
- nonce: nonce
52
+ nonce: nonce,
53
53
  }
54
54
  end
55
55
 
@@ -95,9 +95,7 @@ module Familia
95
95
  validate_key_length!(master_key)
96
96
 
97
97
  raw_personal = personal || Familia.config.encryption_personalization
98
- if raw_personal.include?("\0")
99
- raise EncryptionError, 'Personalization string must not contain null bytes'
100
- end
98
+ raise EncryptionError, 'Personalization string must not contain null bytes' if raw_personal.include?("\0")
101
99
 
102
100
  personal_string = raw_personal.ljust(16, "\0")
103
101
 
@@ -132,8 +130,8 @@ module Familia
132
130
 
133
131
  result = {
134
132
  ciphertext: ciphertext_with_tag[0...-16],
135
- auth_tag: ciphertext_with_tag[-16..-1],
136
- nonce: nonce
133
+ auth_tag: ciphertext_with_tag[-16..],
134
+ nonce: nonce,
137
135
  }
138
136
 
139
137
  # Clear intermediate values
@@ -168,10 +166,10 @@ module Familia
168
166
  def clear_aead_instance(aead_instance)
169
167
  # Attempt to clear RbNaCl's internal key storage
170
168
  # This is a best-effort cleanup since RbNaCl stores keys as strings internally
171
- if aead_instance.instance_variable_defined?(:@key)
172
- internal_key = aead_instance.instance_variable_get(:@key)
173
- secure_wipe(internal_key) if internal_key
174
- end
169
+ return unless aead_instance.instance_variable_defined?(:@key)
170
+
171
+ internal_key = aead_instance.instance_variable_get(:@key)
172
+ secure_wipe(internal_key) if internal_key
175
173
  end
176
174
 
177
175
  def validate_key_length!(key)
@@ -53,8 +53,8 @@ module Familia
53
53
 
54
54
  {
55
55
  ciphertext: ciphertext_with_tag[0...-16],
56
- auth_tag: ciphertext_with_tag[-16..-1],
57
- nonce: nonce
56
+ auth_tag: ciphertext_with_tag[-16..],
57
+ nonce: nonce,
58
58
  }
59
59
  end
60
60
 
@@ -88,9 +88,8 @@ module Familia
88
88
  def derive_key(master_key, context, personal: nil)
89
89
  validate_key_length!(master_key)
90
90
  raw_personal = personal || Familia.config.encryption_personalization
91
- if raw_personal.include?("\0")
92
- raise EncryptionError, 'Personalization string must not contain null bytes'
93
- end
91
+ raise EncryptionError, 'Personalization string must not contain null bytes' if raw_personal.include?("\0")
92
+
94
93
  personal_string = raw_personal.ljust(16, "\0")
95
94
 
96
95
  RbNaCl::Hash.blake2b(
@@ -18,8 +18,8 @@ module Familia
18
18
  class << self
19
19
  # Enable request-scoped caching (opt-in for performance)
20
20
  def with_request_cache
21
- Thread.current[:familia_request_cache_enabled] = true
22
- Thread.current[:familia_request_cache] = {}
21
+ Fiber[:familia_request_cache_enabled] = true
22
+ Fiber[:familia_request_cache] = {}
23
23
  yield
24
24
  ensure
25
25
  clear_request_cache!
@@ -27,12 +27,12 @@ module Familia
27
27
 
28
28
  # Clear all cached keys and disable caching
29
29
  def clear_request_cache!
30
- if (cache = Thread.current[:familia_request_cache])
30
+ if (cache = Fiber[:familia_request_cache])
31
31
  cache.each_value { |key| secure_wipe(key) }
32
32
  cache.clear
33
33
  end
34
- Thread.current[:familia_request_cache_enabled] = false
35
- Thread.current[:familia_request_cache] = nil
34
+ Fiber[:familia_request_cache_enabled] = false
35
+ Fiber[:familia_request_cache] = nil
36
36
  end
37
37
 
38
38
  private
@@ -43,8 +43,8 @@ module Familia
43
43
  master_key = get_master_key(version)
44
44
 
45
45
  # Only use cache if explicitly enabled for this request
46
- if Thread.current[:familia_request_cache_enabled]
47
- cache = Thread.current[:familia_request_cache] ||= {}
46
+ if Fiber[:familia_request_cache_enabled]
47
+ cache = Fiber[:familia_request_cache] ||= {}
48
48
  cache_key = "#{version}:#{context}"
49
49
 
50
50
  # Return cached key if available (within same request only)
@@ -16,7 +16,6 @@ module Familia
16
16
  class EncryptionError < StandardError; end
17
17
 
18
18
  module Encryption
19
-
20
19
  # Smart facade with provider selection and field-specific encryption
21
20
  #
22
21
  # Usage in EncryptedFieldType can now be more flexible:
@@ -109,7 +108,7 @@ module Familia
109
108
  preferred_available: Registry.default_provider&.class&.name,
110
109
  using_hardware: hardware_acceleration?,
111
110
  key_versions: encryption_keys.keys,
112
- current_version: current_key_version
111
+ current_version: current_key_version,
113
112
  }
114
113
  end
115
114
 
@@ -140,7 +139,7 @@ module Familia
140
139
  results[algo] = {
141
140
  time: time,
142
141
  ops_per_sec: (iterations * 2 / time).round,
143
- priority: provider_class.priority
142
+ priority: provider_class.priority,
144
143
  }
145
144
  end
146
145
 
@@ -10,7 +10,10 @@ module Familia
10
10
 
11
11
  class SerializerError < Problem; end
12
12
 
13
- class HighRiskFactor < Problem
13
+ # Raised when attempting to start transactions or pipelines on connection types that don't support them
14
+ class OperationModeError < Problem; end
15
+
16
+ class NotDistinguishableError < Problem
14
17
  attr_reader :value
15
18
 
16
19
  def initialize(value)
@@ -19,7 +22,7 @@ module Familia
19
22
  end
20
23
 
21
24
  def message
22
- "High risk factor for serialization bugs: #{value}<#{value.class}>"
25
+ "Cannot represent #{value}<#{value.class}> as a string"
23
26
  end
24
27
  end
25
28
 
@@ -36,9 +39,12 @@ module Familia
36
39
  end
37
40
  end
38
41
 
39
- # Set Familia.connection_provider or use middleware to provide connections.
42
+ # UnsortedSet Familia.connection_provider or use middleware to provide connections.
40
43
  class NoConnectionAvailable < Problem; end
41
44
 
45
+ # Raised when a load method fails to find the requested object
46
+ class NotFound < Problem; end
47
+
42
48
  # Raised when attempting to refresh an object whose key doesn't exist in the database
43
49
  class KeyNotFoundError < NonUniqueKey
44
50
  attr_reader :key
@@ -1,12 +1,13 @@
1
1
  # lib/familia/features/autoloader.rb
2
2
 
3
+ # rubocop:disable Style/ClassAndModuleChildren
3
4
  module Familia::Features
4
5
  # Provides autoloading functionality for Ruby files based on patterns and conventions.
5
6
  #
6
7
  # Used by the Features module at library startup to load feature files, and available
7
8
  # as a utility for other modules requiring file autoloading capabilities.
8
9
  module Autoloader
9
- using Familia::Refinements::SnakeCase
10
+ using Familia::Refinements::StylizeWords
10
11
 
11
12
  # Autoloads feature files when this module is included.
12
13
  #
@@ -15,25 +16,22 @@ module Familia::Features
15
16
  #
16
17
  # @param base [Module] the module including this autoloader
17
18
  def self.included(base)
18
-
19
19
  # Get the directory where the including module is defined
20
20
  # This should be lib/familia for the Features module
21
21
  base_path = File.dirname(caller_locations(1, 1).first.path)
22
- model_name = base.name.snake_case
22
+ config_name = normalize_to_config_name(base.name)
23
+
23
24
  dir_patterns = [
24
25
  File.join(base_path, 'features', '*.rb'),
25
- File.join(base_path, model_name, 'features', '*.rb'),
26
- File.join(base_path, model_name, 'features.rb'),
26
+ File.join(base_path, config_name, 'features', '*.rb'),
27
+ File.join(base_path, config_name, 'features.rb'),
27
28
  ]
28
29
 
29
30
  # Ensure the Features module exists within the base module
30
- unless base.const_defined?(:Features) || model_name.eql?('features')
31
- base.const_set(:Features, Module.new)
32
- end
33
-
31
+ base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
34
32
 
35
33
  # Use the shared autoload_files method
36
- autoload_files(dir_patterns, log_prefix: "Autoloader[#{model_name}]")
34
+ autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
37
35
  end
38
36
 
39
37
  # Autoloads Ruby files matching the given patterns.
@@ -45,17 +43,37 @@ module Familia::Features
45
43
  patterns = Array(patterns)
46
44
 
47
45
  patterns.each do |pattern|
48
- Familia.ld "[#{log_prefix}] Autoloader loading features from #{pattern}"
46
+ Familia.trace :AUTOLOAD, nil, "[#{log_prefix}] Autoloader loading features from #{pattern}"
49
47
  Dir.glob(pattern).each do |file_path|
50
48
  basename = File.basename(file_path)
51
49
 
52
50
  # Skip excluded files
53
51
  next if exclude.include?(basename)
54
52
 
55
- Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{file_path}", caller(1..1) if Familia.debug?
53
+ Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{basename}" if Familia.debug?
56
54
  require File.expand_path(file_path)
57
55
  end
58
56
  end
59
57
  end
58
+
59
+ class << self
60
+ # Converts the value into a string that can be used to look up configuration
61
+ # values or system paths. This replicates the normalization done by the
62
+ # Familia::Horreum model class config_name method.
63
+ #
64
+ # @see Familia::Horreum::DefinitionMethods#config_name
65
+ #
66
+ # NOTE: We don't call that existing method directly b/c Autoloader is meant
67
+ # to work for any class/module that matches `dir_patterns` (see `included`).
68
+ #
69
+ # @param value [String] the value to normalize (typically a class name)
70
+ # @return [String] the underscored value as a string
71
+ def normalize_to_config_name(value)
72
+ return nil if value.nil?
73
+
74
+ value.demodularize.snake_case
75
+ end
76
+ end
60
77
  end
61
78
  end
79
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -161,8 +161,8 @@ class ConcealedString
161
161
  # Prevent accidental exposure through string conversion and serialization
162
162
  #
163
163
  # Ruby has two string conversion methods with different purposes:
164
- # - to_s: explicit conversion (obj.to_s, string interpolation "#{obj}")
165
- # - to_str: implicit coercion (File.read(obj), "prefix" + obj)
164
+ # - to_s: explicit conversion (`obj.to_s`, string interpolation `"#{obj}"`)
165
+ # - to_str: implicit coercion (`File.read(obj)`, `"prefix" + obj`)
166
166
  #
167
167
  # We implement to_s for safe logging/debugging but deliberately omit to_str
168
168
  # to prevent encrypted data from being used where strings are expected.
@@ -268,7 +268,7 @@ class ConcealedString
268
268
 
269
269
  # Prevent exposure in JSON serialization - fail closed for security
270
270
  def to_json(*)
271
- raise Familia::SerializerError, "ConcealedString cannot be serialized to JSON"
271
+ raise Familia::SerializerError, 'ConcealedString cannot be serialized to JSON'
272
272
  end
273
273
 
274
274
  # Prevent exposure in Rails serialization (as_json -> to_json)
@@ -291,5 +291,4 @@ class ConcealedString
291
291
  def encrypted_json?(data)
292
292
  Familia::Encryption::EncryptedData.valid?(data)
293
293
  end
294
-
295
294
  end
@@ -58,7 +58,7 @@ module Familia
58
58
 
59
59
  # If we have a raw string (from direct instance variable manipulation),
60
60
  # wrap it in ConcealedString which will trigger validation
61
- if concealed.kind_of?(::String) && !concealed.is_a?(ConcealedString)
61
+ if concealed.is_a?(::String) && !concealed.is_a?(ConcealedString)
62
62
  # This happens when someone directly sets the instance variable
63
63
  # (e.g., during tampering tests). Wrapping in ConcealedString
64
64
  # will trigger validate_decryptable! and catch invalid algorithms
@@ -75,7 +75,8 @@ module Familia
75
75
  # Context validation: detect cross-context attacks
76
76
  # Only validate if we have a proper ConcealedString instance
77
77
  if concealed.is_a?(ConcealedString) && !concealed.belongs_to_context?(self, field_name)
78
- raise Familia::EncryptionError, "Context isolation violation: encrypted field '#{field_name}' does not belong to #{self.class.name}:#{self.identifier}"
78
+ raise Familia::EncryptionError,
79
+ "Context isolation violation: encrypted field '#{field_name}' does not belong to #{self.class.name}:#{identifier}"
79
80
  end
80
81
 
81
82
  concealed
@@ -90,13 +91,13 @@ module Familia
90
91
  field_name = @name
91
92
  method_name = @method_name
92
93
  fast_method_name = @fast_method_name
93
- field_type = self
94
+ self
94
95
 
95
96
  handle_method_conflict(klass, fast_method_name) do
96
97
  klass.define_method fast_method_name do |val|
97
98
  raise ArgumentError, "#{fast_method_name} requires a value" if val.nil?
98
99
 
99
- # Set via the setter method to get proper ConcealedString wrapping
100
+ # UnsortedSet via the setter method to get proper ConcealedString wrapping
100
101
  send(:"#{method_name}=", val) if method_name
101
102
 
102
103
  # Get the ConcealedString and extract encrypted data for storage
@@ -203,18 +204,16 @@ module Familia
203
204
  if @aad_fields.empty?
204
205
  # When no AAD fields specified, use class:field:identifier
205
206
  base_components.join(':')
206
- else
207
+ elsif record.exists?
207
208
  # For unsaved records, don't enforce AAD fields since they can change
208
209
  # For saved records, include field values for tamper protection
209
- if record.exists?
210
- # Include specified field values in AAD for persisted records
211
- values = @aad_fields.map { |field| record.send(field) }
212
- all_components = [*base_components, *values].compact
213
- Digest::SHA256.hexdigest(all_components.join(':'))
214
- else
215
- # For unsaved records, only use class:field:identifier for context isolation
216
- base_components.join(':')
217
- end
210
+ values = @aad_fields.map { |field| record.send(field) }
211
+ all_components = [*base_components, *values].compact
212
+ Digest::SHA256.hexdigest(all_components.join(':'))
213
+ # Include specified field values in AAD for persisted records
214
+ else
215
+ # For unsaved records, only use class:field:identifier for context isolation
216
+ base_components.join(':')
218
217
  end
219
218
  end
220
219
  end