familia 2.0.0.pre14 → 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 (276) 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 +66 -6
  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 +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  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 +623 -19
  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 +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -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/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. 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,11 +1,39 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/features/autoloader.rb
2
2
 
3
- module Familia
3
+ # rubocop:disable Style/ClassAndModuleChildren
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
10
+ using Familia::Refinements::StylizeWords
11
+
12
+ # Autoloads feature files when this module is included.
13
+ #
14
+ # Discovers and loads all Ruby files in the features/ directory relative to the
15
+ # including module's location. Typically used by Familia::Features.
16
+ #
17
+ # @param base [Module] the module including this autoloader
18
+ def self.included(base)
19
+ # Get the directory where the including module is defined
20
+ # This should be lib/familia for the Features module
21
+ base_path = File.dirname(caller_locations(1, 1).first.path)
22
+ config_name = normalize_to_config_name(base.name)
23
+
24
+ dir_patterns = [
25
+ File.join(base_path, 'features', '*.rb'),
26
+ File.join(base_path, config_name, 'features', '*.rb'),
27
+ File.join(base_path, config_name, 'features.rb'),
28
+ ]
29
+
30
+ # Ensure the Features module exists within the base module
31
+ base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
32
+
33
+ # Use the shared autoload_files method
34
+ autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
35
+ end
36
+
9
37
  # Autoloads Ruby files matching the given patterns.
10
38
  #
11
39
  # @param patterns [String, Array<String>] file patterns to match (supports Dir.glob patterns)
@@ -15,39 +43,37 @@ module Familia
15
43
  patterns = Array(patterns)
16
44
 
17
45
  patterns.each do |pattern|
46
+ Familia.trace :AUTOLOAD, nil, "[#{log_prefix}] Autoloader loading features from #{pattern}"
18
47
  Dir.glob(pattern).each do |file_path|
19
48
  basename = File.basename(file_path)
20
49
 
21
50
  # Skip excluded files
22
51
  next if exclude.include?(basename)
23
52
 
24
- 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?
25
54
  require File.expand_path(file_path)
26
55
  end
27
56
  end
28
57
  end
29
58
 
30
- # Autoloads feature files when this module is included.
31
- #
32
- # Discovers and loads all Ruby files in the features/ directory relative to the
33
- # including module's location. Typically used by Familia::Features.
34
- #
35
- # @param base [Module] the module including this autoloader
36
- def self.included(base)
37
- # Get the directory where the including module is defined
38
- # This should be lib/familia for the Features module
39
- base_path = File.dirname(caller_locations(1, 1).first.path)
40
- features_dir = File.join(base_path, 'features')
41
-
42
- Familia.ld "[DEBUG] Autoloader loading features from #{features_dir}"
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?
43
73
 
44
- return unless Dir.exist?(features_dir)
45
-
46
- # Use the shared autoload_files method
47
- autoload_files(
48
- File.join(features_dir, '*.rb'),
49
- log_prefix: 'Autoloader'
50
- )
74
+ value.demodularize.snake_case
75
+ end
51
76
  end
52
77
  end
53
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