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
@@ -1,18 +1,23 @@
1
1
  # lib/familia/settings.rb
2
2
 
3
+ # Familia
4
+ #
3
5
  module Familia
4
- @delim = ':'
6
+ @delim = ':'.freeze
5
7
  @prefix = nil
6
8
  @suffix = :object
7
9
  @default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
8
10
  @logical_database = nil
9
11
  @encryption_keys = nil
10
12
  @current_key_version = nil
11
- @encryption_personalization = 'FamilialMatters'
13
+ @encryption_personalization = 'FamilialMatters'.freeze
14
+ @pipeline_mode = :warn
12
15
 
16
+ # Familia::Settings
17
+ #
13
18
  module Settings
14
19
  attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix, :encryption_keys,
15
- :current_key_version, :encryption_personalization
20
+ :current_key_version, :encryption_personalization, :transaction_mode
16
21
 
17
22
  def delim(val = nil)
18
23
  @delim = val if val
@@ -35,7 +40,7 @@ module Familia
35
40
  end
36
41
 
37
42
  def logical_database(v = nil)
38
- Familia.trace :DB, dbclient, "#{@logical_database} #{v}", caller(1..1) if Familia.debug?
43
+ Familia.trace :DB, nil, "#{@logical_database} #{v}" if Familia.debug?
39
44
  @logical_database = v unless v.nil?
40
45
  @logical_database
41
46
  end
@@ -61,8 +66,7 @@ module Familia
61
66
  # unique per application even with identical master keys and contexts.
62
67
  # Must be 16 bytes or less (automatically padded with null bytes).
63
68
  #
64
- # @example
65
- # Familia.configure do |config|
69
+ # @example Familia.configure do |config|
66
70
  # config.encryption_personalization = 'MyApp1.0'
67
71
  # end
68
72
  #
@@ -77,8 +81,80 @@ module Familia
77
81
  @encryption_personalization
78
82
  end
79
83
 
80
- def config
84
+ # Controls transaction behavior when connection handlers don't support transactions
85
+ #
86
+ # @param val [Symbol, nil] The transaction mode or nil to get current value
87
+ # @return [Symbol] Current transaction mode (:strict, :warn, :permissive)
88
+ #
89
+ # Available modes:
90
+ # - :warn (default): Log warning and execute commands individually
91
+ # - :strict: Raise OperationModeError when transaction unavailable
92
+ # - :permissive: Silently execute commands individually
93
+ #
94
+ # @example Setting transaction mode
95
+ # Familia.configure do |config|
96
+ # config.transaction_mode = :warn
97
+ # end
98
+ #
99
+ def transaction_mode(val = nil)
100
+ if val
101
+ unless [:strict, :warn, :permissive].include?(val)
102
+ raise ArgumentError, 'Transaction mode must be :strict, :warn, or :permissive'
103
+ end
104
+ @transaction_mode = val
105
+ end
106
+ @transaction_mode || :warn # default to warn mode
107
+ end
108
+
109
+ # Controls pipeline behavior when connection handlers don't support pipelines
110
+ #
111
+ # @param val [Symbol, nil] The pipeline mode or nil to get current value
112
+ # @return [Symbol] Current pipeline mode (:strict, :warn, :permissive)
113
+ #
114
+ # Available modes:
115
+ # - :warn (default): Log warning and execute commands individually
116
+ # - :strict: Raise OperationModeError when pipeline unavailable
117
+ # - :permissive: Silently execute commands individually
118
+ #
119
+ # @example Setting pipeline mode
120
+ # Familia.configure do |config|
121
+ # config.pipeline_mode = :permissive
122
+ # end
123
+ #
124
+ def pipeline_mode(val = nil)
125
+ if val
126
+ unless [:strict, :warn, :permissive].include?(val)
127
+ raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
128
+ end
129
+ @pipeline_mode = val
130
+ end
131
+ @pipeline_mode || :warn # default to warn mode
132
+ end
133
+
134
+ def pipeline_mode=(val)
135
+ unless [:strict, :warn, :permissive].include?(val)
136
+ raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
137
+ end
138
+ @pipeline_mode = val
139
+ end
140
+
141
+ # Configure Familia settings
142
+ #
143
+ # @yield [Settings] self for block-based configuration
144
+ # @return [Settings] self for method chaining
145
+ #
146
+ # @example Block-based configuration
147
+ # Familia.configure do |config|
148
+ # config.redis_uri = "redis://localhost:6379/1"
149
+ # config.ttl = 3600
150
+ # end
151
+ #
152
+ # @example Method chaining
153
+ # Familia.configure.redis_uri = "redis://localhost:6379/1"
154
+ def configure
155
+ yield self if block_given?
81
156
  self
82
157
  end
158
+ alias config configure
83
159
  end
84
160
  end
data/lib/familia/utils.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  # lib/familia/utils.rb
2
2
 
3
3
  module Familia
4
-
5
4
  # Family-related utility methods
6
5
  #
7
6
  module Utils
8
-
9
7
  using Familia::Refinements::TimeLiterals
10
8
 
11
9
  # Joins array elements with Familia delimiter
@@ -38,10 +36,10 @@ module Familia
38
36
  end
39
37
 
40
38
  # Returns current time in UTC as a float
41
- # @param name [Time] time object (default: current time)
39
+ # @param current_time [Time] time object (default: current time)
42
40
  # @return [Float] time in seconds since epoch
43
- def now(name = Time.now)
44
- name.utc.to_f
41
+ def now(current_time = Time.now)
42
+ current_time.utc.to_f
45
43
  end
46
44
 
47
45
  # A quantized timestamp
@@ -51,12 +49,11 @@ module Familia
51
49
  # @param time [Integer, Float, Time, nil] A specific time to quantize (default: current time).
52
50
  # @return [Integer, String] A unix timestamp or formatted timestamp string.
53
51
  #
54
- # @example
55
- # Familia.qstamp # Returns an integer timestamp rounded to the nearest 10 minutes
52
+ # @example Familia.qstamp # Returns an integer timestamp rounded to the nearest 10 minutes
56
53
  # Familia.qstamp(1.hour) # Uses 1 hour quantum
57
54
  # Familia.qstamp(10.minutes, pattern: '%H:%M') # Returns a formatted string like "12:30"
58
55
  # Familia.qstamp(10.minutes, time: 1302468980) # Quantizes the given Unix timestamp
59
- # Familia.qstamp(10.minutes, time: Time.now) # Quantizes the given Time object
56
+ # Familia.qstamp(10.minutes, time: Familia.now) # Quantizes the given Time object
60
57
  # Familia.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980) # Formats a specific time
61
58
  #
62
59
  def qstamp(quantum = 10.minutes, pattern: nil, time: nil)
@@ -72,84 +69,6 @@ module Familia
72
69
  end
73
70
  end
74
71
 
75
- # This method determines the appropriate transformation to apply based on
76
- # the class of the input argument.
77
- #
78
- # @param [Object] value_to_distinguish The value to be processed. Keep in
79
- # mind that all data is stored as a string so whatever the type
80
- # of the value, it will be converted to a string.
81
- # @param [Boolean] strict_values Whether to enforce strict value handling.
82
- # Defaults to true.
83
- # @return [String, nil] The processed value as a string or nil for unsupported
84
- # classes.
85
- #
86
- # The method uses a case statement to handle different classes:
87
- # - For `Symbol`, `String`, `Integer`, and `Float` classes, it traces the
88
- # operation and converts the value to a string.
89
- # - For `Familia::Horreum` class, it traces the operation and returns the
90
- # identifier of the value.
91
- # - For `TrueClass`, `FalseClass`, and `NilClass`, it traces the operation and
92
- # converts the value to a string ("true", "false", or "").
93
- # - For any other class, it traces the operation and returns nil.
94
- #
95
- # Alternative names for `value_to_distinguish` could be `input_value`, `value`,
96
- # or `object`.
97
- #
98
- def distinguisher(value_to_distinguish, strict_values: true)
99
- case value_to_distinguish
100
- when ::Symbol, ::String, ::Integer, ::Float
101
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'string', caller(1..1) if Familia.debug?
102
-
103
- # Symbols and numerics are naturally serializable to strings
104
- # so it's a relatively low risk operation.
105
- value_to_distinguish.to_s
106
-
107
- when ::TrueClass, ::FalseClass, ::NilClass
108
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'true/false/nil', caller(1..1) if Familia.debug?
109
-
110
- # TrueClass, FalseClass, and NilClass are considered high risk because their
111
- # original types cannot be reliably determined from their serialized string
112
- # representations. This can lead to unexpected behavior during deserialization.
113
- # For instance, a TrueClass value serialized as "true" might be deserialized as
114
- # a String, causing application errors. Even more problematic, a NilClass value
115
- # serialized as an empty string makes it impossible to distinguish between a
116
- # nil value and an empty string upon deserialization. Such scenarios can result
117
- # in subtle, hard-to-diagnose bugs. To mitigate these risks, we raise an
118
- # exception when encountering these types unless the strict_values option is
119
- # explicitly set to false.
120
- #
121
- raise Familia::HighRiskFactor, value_to_distinguish if strict_values
122
-
123
- value_to_distinguish.to_s #=> "true", "false", ""
124
-
125
- when Familia::Base, Class
126
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'base', caller(1..1) if Familia.debug?
127
-
128
- # When called with a class we simply transform it to its name. For
129
- # instances of Familia class, we store the identifier.
130
- if value_to_distinguish.is_a?(Class)
131
- value_to_distinguish.name
132
- else
133
- value_to_distinguish.identifier
134
- end
135
-
136
- else
137
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else1 #{strict_values}", caller(1..1) if Familia.debug?
138
-
139
- if value_to_distinguish.class.ancestors.member?(Familia::Base)
140
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'isabase', caller(1..1) if Familia.debug?
141
-
142
- value_to_distinguish.identifier
143
-
144
- else
145
- Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else2 #{strict_values}", caller(1..1) if Familia.debug?
146
- raise Familia::HighRiskFactor, value_to_distinguish if strict_values
147
-
148
- nil
149
- end
150
- end
151
- end
152
-
153
72
  # Converts an absolute file path to a path relative to the current working
154
73
  # directory. This simplifies logging and error reporting by showing
155
74
  # only the relevant parts of file paths instead of lengthy absolute paths.
@@ -191,6 +110,5 @@ module Familia
191
110
  def pretty_stack(skip: 1, limit: 5)
192
111
  caller(skip..(skip + limit + 1)).first(limit).map { |frame| pretty_path(frame) }.join("\n")
193
112
  end
194
-
195
113
  end
196
114
  end
@@ -8,7 +8,7 @@ module Familia
8
8
  # allowing for stateless verification of an identifier's authenticity.
9
9
  module VerifiableIdentifier
10
10
  # By extending SecureIdentifier, we gain access to its instance methods
11
- # (like generate_hex_id) as class methods on this module.
11
+ # (like generate_id) as class methods on this module.
12
12
  extend Familia::SecureIdentifier
13
13
 
14
14
  # The secret key for HMAC generation, loaded from an environment variable.
@@ -33,7 +33,7 @@ module Familia
33
33
  # $ openssl rand -hex 32
34
34
  # > cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d
35
35
  #
36
- # 2. Set it as an environment variable in your production environment:
36
+ # 2. UnsortedSet it as an environment variable in your production environment:
37
37
  # export VERIFIABLE_ID_HMAC_SECRET="cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
38
38
  #
39
39
  SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
@@ -61,8 +61,8 @@ module Familia
61
61
  # base remains as passed in keyword argument or default
62
62
  end
63
63
 
64
- # Re-use generate_hex_id from the SecureIdentifier module.
65
- random_hex = generate_hex_id
64
+ # Re-use generate_id from the SecureIdentifier module.
65
+ random_hex = generate_id(16)
66
66
  tag_hex = generate_tag(random_hex, scope: scope)
67
67
 
68
68
  combined_hex = random_hex + tag_hex
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre14'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre16'.freeze unless defined?(Familia::VERSION)
6
6
  end
data/lib/familia.rb CHANGED
@@ -7,13 +7,14 @@ require 'connection_pool'
7
7
 
8
8
  # OJ configuration is handled internally by Familia::JsonSerializer
9
9
 
10
+ require_relative 'multi_result'
10
11
  require_relative 'familia/refinements'
11
12
  require_relative 'familia/errors'
12
13
  require_relative 'familia/version'
13
14
 
14
- # Familia - A family warehouse for Redis
15
+ # Familia - A family warehouse for Valkey/Redis
15
16
  #
16
- # Familia provides a way to organize and store Ruby objects in Redis.
17
+ # Familia provides a way to organize and store Ruby objects in the database.
17
18
  # It includes various modules and classes to facilitate object-Database interactions.
18
19
  #
19
20
  # @example Basic usage
@@ -32,18 +33,31 @@ require_relative 'familia/version'
32
33
  # @see https://github.com/delano/familia
33
34
  #
34
35
  module Familia
35
-
36
36
  @debug = ENV['FAMILIA_DEBUG'].to_s.downcase.match?(/^(true|1)$/i).freeze
37
37
  @members = []
38
38
 
39
+ using Refinements::StylizeWords
40
+
39
41
  class << self
40
- attr_accessor :debug
42
+ attr_accessor :debug # rubocop:disable ThreadSafety/ClassAndModuleAttributes
41
43
  attr_reader :members
42
44
 
43
45
  def included(member)
44
46
  raise Problem, "#{member} should subclass Familia::Horreum"
45
47
  end
46
48
 
49
+ def resolve_class(target)
50
+ case target
51
+ when Class
52
+ target
53
+ when ::String, Symbol
54
+ config_name = target.to_s.demodularize.snake_case
55
+ member_by_config_name(config_name)
56
+ else
57
+ raise ArgumentError, "Expected Class, String, or Symbol, got #{target.class}"
58
+ end
59
+ end
60
+
47
61
  # A convenience pattern for configuring Familia.
48
62
  #
49
63
  # @example
@@ -65,6 +79,59 @@ module Familia
65
79
  def debug?
66
80
  @debug == true
67
81
  end
82
+
83
+ # Remove a member class from the members array.
84
+ # Used for test cleanup to prevent anonymous classes from polluting
85
+ # the global registry.
86
+ #
87
+ # @param klass [Class] The class to remove from members
88
+ # @return [Class, nil] The removed class or nil if not found
89
+ def unload_member(klass)
90
+ Familia.ld "[unload_member] Removing #{klass} from members"
91
+ @members.delete(klass)
92
+ end
93
+
94
+ # Remove all anonymous/test classes from members array.
95
+ # Anonymous classes have nil names, which cause issues in member_by_config_name.
96
+ #
97
+ # @return [Array<Class>] The removed anonymous classes
98
+ def clear_anonymous_members
99
+ anonymous_classes = @members.select { |m| m.name.nil? }
100
+ Familia.ld "[clear_anonymous_members] Removing #{anonymous_classes.size} anonymous classes"
101
+ @members.reject! { |m| m.name.nil? }
102
+ anonymous_classes
103
+ end
104
+
105
+ # Check if we're in test mode by looking for test-related constants
106
+ # or environment variables
107
+ #
108
+ # @return [Boolean] true if running in test mode
109
+ def test_mode?
110
+ defined?(Tryouts) || ENV['FAMILIA_TEST_MODE'] == 'true'
111
+ end
112
+
113
+ private
114
+
115
+ # Finds a member class by its symbolized name
116
+ #
117
+ # NOTE: If you are not getting the expected results, check the load order of
118
+ # the models. The one your looking for may not be loaded yet. Currently
119
+ # models are loaded naively -- that is, they are loaded in the order
120
+ # they are defined in the codebase.
121
+ #
122
+ # @param config_name [Symbol, String] The symbolized name of the member class
123
+ # @return [Class, nil] The member class if found, nil otherwise
124
+ #
125
+ # @example
126
+ # Familia.member_by_config_name(:flower) # => Flower class
127
+ # Familia.member_by_config_name('flower') # => Flower class
128
+ # Familia.member_by_config_name(:nonexistent) # => nil
129
+ #
130
+ def member_by_config_name(config_name)
131
+ Familia.ld "[member_by_config_name] #{members.map(&:config_name)} #{config_name}"
132
+
133
+ members.find { |m| m.config_name.to_s.eql?(config_name.to_s) }
134
+ end
68
135
  end
69
136
 
70
137
  require_relative 'familia/secure_identifier'
@@ -72,6 +139,7 @@ module Familia
72
139
  require_relative 'familia/connection'
73
140
  require_relative 'familia/settings'
74
141
  require_relative 'familia/utils'
142
+ require_relative 'familia/distinguisher'
75
143
  require_relative 'familia/json_serializer'
76
144
 
77
145
  extend SecureIdentifier
@@ -82,18 +150,7 @@ module Familia
82
150
  end
83
151
 
84
152
  require_relative 'familia/base'
85
- require_relative 'familia/features/autoloadable'
86
153
  require_relative 'familia/features'
87
154
  require_relative 'familia/data_type'
88
155
  require_relative 'familia/horreum'
89
156
  require_relative 'familia/encryption'
90
-
91
- # Ensure JSON constant is available for backward compatibility with existing code
92
- # This approach is safer than monkey-patching core classes globally
93
- begin
94
- require 'json'
95
- rescue LoadError
96
- # If json gem is not available, define a minimal JSON constant
97
- # that delegates to Familia::JsonSerializer for compatibility
98
- JSON = Familia::JsonSerializer
99
- end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'concurrent-ruby'
4
4
 
5
- # DatabaseLogger is RedisClient middleware.
5
+ # DatabaseLogger is Valkey/RedisClient middleware.
6
6
  #
7
7
  # This middleware addresses the need for detailed Database command logging, which
8
8
  # was removed from the redis-rb gem due to performance concerns. However, in
@@ -13,6 +13,13 @@ require 'concurrent-ruby'
13
13
  # DatabaseLogger.logger = Logger.new(STDOUT)
14
14
  # RedisClient.register(DatabaseLogger)
15
15
  #
16
+ # @example Capture commands for testing
17
+ # commands = DatabaseLogger.capture_commands do
18
+ # redis.set('key', 'value')
19
+ # redis.get('key')
20
+ # end
21
+ # puts commands.first[:command] # => ["SET", "key", "value"]
22
+ #
16
23
  # @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
17
24
  #
18
25
  # @note While there were concerns about the performance impact of logging in
@@ -22,39 +29,75 @@ require 'concurrent-ruby'
22
29
  # often outweigh the slight performance cost when enabled.
23
30
  module DatabaseLogger
24
31
  @logger = nil
32
+ @commands = []
25
33
 
26
34
  class << self
27
35
  # Gets/sets the logger instance used by DatabaseLogger.
28
36
  # @return [Logger, nil] The current logger instance or nil if not set.
29
37
  attr_accessor :logger
38
+
39
+ # Gets the captured commands for testing purposes.
40
+ # @return [Array] Array of command hashes with :command, :duration, :timestamp
41
+ attr_reader :commands
42
+
43
+ # Clears the captured commands array.
44
+ # @return [Array] Empty array
45
+ def clear_commands
46
+ @commands = []
47
+ end
48
+
49
+ # Captures commands in a block and returns them.
50
+ # This is useful for testing to see what commands were executed.
51
+ #
52
+ # @yield [] The block of code to execute while capturing commands.
53
+ # @return [Array] Array of captured commands with timing information.
54
+ # Each command is a hash with :command, :duration, :timestamp keys.
55
+ #
56
+ # @example Test what Redis commands your code executes
57
+ # commands = DatabaseLogger.capture_commands do
58
+ # my_library_method()
59
+ # end
60
+ # assert_equal "SET", commands.first[:command][0]
61
+ # assert commands.first[:duration] > 0
62
+ def capture_commands
63
+ clear_commands
64
+ yield
65
+ @commands.dup
66
+ end
30
67
  end
31
68
 
32
69
  # Logs the Database command and its execution time.
33
70
  #
34
71
  # This method is called for each Database command when the middleware is active.
35
- # It logs the command and its execution time only if a logger is set.
72
+ # It always captures commands for testing and logs them if a logger is set.
36
73
  #
37
74
  # @param command [Array] The Database command and its arguments.
38
- # @param _config [Hash] The configuration options for the Redis
75
+ # @param _config [Hash] The configuration options for the Valkey/Redis
39
76
  # connection.
40
77
  # @return [Object] The result of the Database command execution.
41
78
  #
42
- # @note The performance impact of this logging is negligible when no logger
43
- # is set, as it quickly returns control to the Database client. When a logger
44
- # is set, the minimal overhead is often offset by the valuable insights
45
- # gained during development and debugging.
79
+ # @note Commands are always captured with minimal overhead for testing purposes.
80
+ # Logging only occurs when DatabaseLogger.logger is set.
46
81
  def call(command, _config)
47
- return yield unless DatabaseLogger.logger
48
-
49
82
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
50
83
  result = yield
51
84
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
52
- DatabaseLogger.logger.debug("Redis: #{command.inspect} (#{duration}µs)")
85
+
86
+ # Always capture commands for testing purposes
87
+ DatabaseLogger.instance_variable_get(:@commands) << {
88
+ command: command.dup,
89
+ duration: duration,
90
+ timestamp: Time.now,
91
+ }
92
+
93
+ # Log if logger is set
94
+ DatabaseLogger.logger&.debug("Redis: #{command.inspect} (#{duration}µs)")
95
+
53
96
  result
54
97
  end
55
98
  end
56
99
 
57
- # DatabaseCommandCounter is RedisClient middleware.
100
+ # DatabaseCommandCounter is Valkey/RedisClient middleware.
58
101
  #
59
102
  # This middleware counts the number of Database commands executed. It can be
60
103
  # useful for performance monitoring and debugging, allowing you to track
@@ -66,7 +109,6 @@ end
66
109
  #
67
110
  # @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
68
111
  #
69
- # rubocop:disable ThreadSafety/ClassInstanceVariable
70
112
  module DatabaseCommandCounter
71
113
  @count = Concurrent::AtomicFixnum.new(0)
72
114
 
@@ -75,11 +117,11 @@ module DatabaseCommandCounter
75
117
  # a configuration where there's a connection to each logical db, there's only
76
118
  # one when the connection is made. When using a provider of via thread local
77
119
  # it could theoretically double the number of statements executed.
78
- @skip_commands = Set.new(['SELECT']).freeze
120
+ @skip_commands = ::Set.new(['SELECT']).freeze
79
121
 
80
122
  class << self
81
123
  # Gets the set of commands to skip counting.
82
- # @return [Set] The commands that won't be counted.
124
+ # @return [UnsortedSet] The commands that won't be counted.
83
125
  attr_reader :skip_commands
84
126
 
85
127
  # Gets the current count of Database commands executed.
@@ -1,30 +1,29 @@
1
- # lib/familia/multi_result.rb
1
+ # lib/multi_result.rb
2
2
 
3
- # The magical MultiResult, keeper of Redis's deepest secrets!
3
+ # Represents the result of a Valkey/Redis transaction operation.
4
4
  #
5
- # This quirky little class wraps up the outcome of a Database "transaction"
6
- # (or as I like to call it, a "Database dance party") with a bow made of
7
- # pure Ruby delight. It knows if your commands were successful and
8
- # keeps the results safe in its pocket dimension.
5
+ # This class encapsulates the outcome of a Database transaction,
6
+ # providing access to both the success status and the individual
7
+ # command results returned by the transaction.
9
8
  #
10
- # @attr_reader success [Boolean] The golden ticket! True if all your
11
- # Database wishes came true in the transaction.
12
- # @attr_reader results [Array<String>] A mystical array of return values,
13
- # each one a whisper from the Database gods.
9
+ # @attr_reader success [Boolean] Indicates whether all commands
10
+ # in the transaction completed successfully.
11
+ # @attr_reader results [Array<String>] Array of return values
12
+ # from the Database commands executed in the transaction.
14
13
  #
15
- # @example Summoning a MultiResult from the void
14
+ # @example Creating a MultiResult instance
16
15
  # result = MultiResult.new(true, ["OK", "OK"])
17
16
  #
18
- # @example Divining the success of your Database ritual
17
+ # @example Checking transaction success
19
18
  # if result.successful?
20
- # puts "Huzzah! The Database spirits smile upon you!"
19
+ # puts "Transaction completed successfully"
21
20
  # else
22
- # puts "Alas! The Database gremlins have conspired against us!"
21
+ # puts "Transaction failed"
23
22
  # end
24
23
  #
25
- # @example Peering into the raw essence of results
24
+ # @example Accessing individual command results
26
25
  # result.results.each_with_index do |value, index|
27
- # puts "Command #{index + 1} whispered back: #{value}"
26
+ # puts "Command #{index + 1} returned: #{value}"
28
27
  # end
29
28
  #
30
29
  class MultiResult
@@ -58,6 +57,13 @@ class MultiResult
58
57
  end
59
58
  alias to_a tuple
60
59
 
60
+ # Returns the number of results in the multi-operation.
61
+ #
62
+ # @return [Integer] The number of individual command results returned by the transaction.
63
+ def size
64
+ results.size
65
+ end
66
+
61
67
  def to_h
62
68
  { success: successful?, results: results }
63
69
  end
@@ -69,4 +75,5 @@ class MultiResult
69
75
  @success
70
76
  end
71
77
  alias success? successful?
78
+ alias areyouhappynow? successful?
72
79
  end
@@ -24,7 +24,7 @@ rescue StandardError => e
24
24
  end
25
25
  #=> false
26
26
 
27
- ## custom Redis URI configuration doesn't always work
27
+ ## custom Valkey/Redis URI configuration doesn't always work
28
28
  begin
29
29
  # Test with custom URI
30
30
  original_uri = Familia.uri