familia 2.0.0.pre17 → 2.0.0.pre19

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +118 -6
  3. data/CLAUDE.md +43 -11
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +52 -0
  7. data/bin/irb +1 -1
  8. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  9. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  10. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  11. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/guides/feature-expiration.md +18 -18
  14. data/docs/migrating/v2.0.0-pre18.md +58 -0
  15. data/docs/migrating/v2.0.0-pre19.md +197 -0
  16. data/docs/qodo-merge-compliance.md +96 -0
  17. data/examples/datatype_standalone.rb +281 -0
  18. data/lib/familia/base.rb +0 -2
  19. data/lib/familia/connection/behavior.rb +252 -0
  20. data/lib/familia/connection/handlers.rb +95 -0
  21. data/lib/familia/connection/middleware.rb +58 -4
  22. data/lib/familia/connection/operation_core.rb +1 -1
  23. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  24. data/lib/familia/connection/transaction_core.rb +7 -9
  25. data/lib/familia/connection.rb +2 -1
  26. data/lib/familia/data_type/connection.rb +151 -7
  27. data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
  28. data/lib/familia/data_type/serialization.rb +9 -5
  29. data/lib/familia/data_type/types/hashkey.rb +1 -1
  30. data/lib/familia/data_type.rb +2 -2
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/errors.rb +51 -14
  34. data/lib/familia/features/autoloader.rb +3 -1
  35. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  36. data/lib/familia/features/expiration/extensions.rb +8 -10
  37. data/lib/familia/features/expiration.rb +19 -19
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
  40. data/lib/familia/features/relationships/indexing.rb +37 -42
  41. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/field_type.rb +2 -1
  44. data/lib/familia/horreum/connection.rb +11 -35
  45. data/lib/familia/horreum/database_commands.rb +130 -11
  46. data/lib/familia/horreum/definition.rb +8 -38
  47. data/lib/familia/horreum/management.rb +38 -27
  48. data/lib/familia/horreum/persistence.rb +191 -67
  49. data/lib/familia/horreum/serialization.rb +94 -73
  50. data/lib/familia/horreum/utils.rb +0 -8
  51. data/lib/familia/horreum.rb +41 -18
  52. data/lib/familia/identifier_extractor.rb +60 -0
  53. data/lib/familia/logging.rb +268 -112
  54. data/lib/familia/refinements.rb +0 -1
  55. data/lib/familia/settings.rb +7 -7
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -2
  58. data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
  59. data/pr_agent.toml +31 -0
  60. data/pr_compliance_checklist.yaml +45 -0
  61. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  62. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  63. data/try/edge_cases/json_serialization_try.rb +2 -2
  64. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  65. data/try/edge_cases/race_conditions_try.rb +1 -1
  66. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  67. data/try/edge_cases/string_coercion_try.rb +5 -5
  68. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  69. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  70. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  71. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  72. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  77. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  79. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  80. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  81. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  82. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  83. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  84. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  85. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  86. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  87. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  88. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  89. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  94. data/try/features/expiration/expiration_try.rb +2 -2
  95. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  96. data/try/features/feature_dependencies_try.rb +1 -1
  97. data/try/features/feature_improvements_try.rb +1 -1
  98. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  99. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  100. data/try/features/quantization/quantization_try.rb +1 -1
  101. data/try/features/real_feature_integration_try.rb +17 -14
  102. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  103. data/try/features/relationships/indexing_try.rb +34 -5
  104. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  105. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  106. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  107. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  108. data/try/features/relationships/relationships_api_changes_try.rb +5 -5
  109. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  110. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  111. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  112. data/try/features/relationships/relationships_performance_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  114. data/try/features/relationships/relationships_try.rb +1 -1
  115. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  116. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  117. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  118. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  119. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  121. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  122. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
  123. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  124. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  125. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  126. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
  127. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
  128. data/try/{core → integration/connection}/pools_try.rb +1 -1
  129. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  130. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  131. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  135. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  136. data/try/{core → integration}/create_method_try.rb +23 -23
  137. data/try/integration/cross_component_try.rb +1 -1
  138. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  139. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  140. data/try/{core → integration}/database_consistency_try.rb +11 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
  144. data/try/{models → integration/models}/customer_try.rb +1 -1
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +2 -2
  147. data/try/{core → integration}/persistence_operations_try.rb +163 -11
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  156. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  157. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  158. data/try/{core → unit/core}/connection_try.rb +1 -1
  159. data/try/{core → unit/core}/errors_try.rb +1 -1
  160. data/try/{core → unit/core}/extensions_try.rb +1 -1
  161. data/try/unit/core/familia_logger_try.rb +110 -0
  162. data/try/{core → unit/core}/familia_try.rb +1 -1
  163. data/try/{core → unit/core}/middleware_try.rb +41 -1
  164. data/try/{core → unit/core}/settings_try.rb +1 -1
  165. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  166. data/try/{core → unit/core}/tools_try.rb +1 -1
  167. data/try/{core → unit/core}/utils_try.rb +17 -14
  168. data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
  169. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  176. data/try/{data_types → unit/data_types}/string_try.rb +2 -2
  177. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  178. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
  179. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +4 -4
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
  182. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  183. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
  193. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  194. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  195. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  196. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  198. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  199. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  200. metadata +147 -126
  201. data/lib/familia/distinguisher.rb +0 -85
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/try/{debugging → support/debugging}/README.md +0 -0
  205. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  206. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  207. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  227. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  228. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  229. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  230. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  231. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  233. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  234. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  235. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -51,7 +51,6 @@ module Familia
51
51
  include Familia::Base
52
52
  include Familia::Horreum::Persistence
53
53
  include Familia::Horreum::Serialization
54
- include Familia::Horreum::Connection
55
54
  include Familia::Horreum::DatabaseCommands
56
55
  include Familia::Horreum::Settings
57
56
  include Familia::Horreum::Utils
@@ -226,23 +225,43 @@ module Familia
226
225
  # Default values are intentionally NOT set here
227
226
  end
228
227
 
229
- # Implementing classes can define an init method to do any
230
- # additional initialization. Notice that this is called
231
- # after the fields are set.
228
+ # Implementing classes can define an init method to do any additional
229
+ # initialization. Notice that this is called AFTER fields are set from
230
+ # kwargs, so kwargs have been consumed and are no longer available.
231
+ #
232
+ # IMPORTANT: Use ||= in init to apply defaults without overriding:
233
+ # def init
234
+ # @email ||= email # Preserves value already set
235
+ # @status ||= 'pending' # Applies default if nil
236
+ # end
237
+ #
232
238
  init
233
239
  end
234
240
 
235
- # Override this method in subclasses for custom initialization logic.
236
- # This is called AFTER fields are set and relatives are initialized.
241
+ # Initialization method called at the end of initialize
242
+ #
243
+ # Override this method to apply defaults, run validations, or setup
244
+ # callbacks. It's recommended to call super as other modules like
245
+ # features can also override init.
246
+ #
247
+ # IMPORTANT: The init method receieves no arguments. By the time this runs,
248
+ # all arguments to initialize have already been consumed and used to set
249
+ # fields. Use the ||= operator to preserve values already set:
237
250
  #
238
- # DO NOT override initialize() - use this init() hook instead.
251
+ # def init(email: nil, user_id: nil, **kwargs)
252
+ # @email ||= email # Preserves value from new()
253
+ # @user_id ||= user_id # Preserves value from new()
254
+ # @created_at ||= Familia.now # Applies default if not set
239
255
  #
240
- # Example:
241
- # def init(name = nil)
242
- # @name = name || SecureRandom.hex(4)
256
+ # # Example of additional initialization logic
257
+ # validate_email_format if @email
258
+ # setup_callbacks
243
259
  # end
244
- def init(*args, **kwargs)
245
- # Default no-op
260
+ #
261
+ # @return [void]
262
+ #
263
+ def init
264
+ # Default no-op - override in subclasses
246
265
  end
247
266
 
248
267
  # Sets up related Database objects for the instance
@@ -299,7 +318,9 @@ module Familia
299
318
 
300
319
  def initialize_with_keyword_args_deserialize_value(**fields)
301
320
  # Deserialize Database string values back to their original types
302
- deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
321
+ deserialized_fields = fields.each_with_object({}) do |(field_name, value), hsh|
322
+ hsh[field_name] = deserialize_value(value, field_name: field_name)
323
+ end
303
324
  initialize_with_keyword_args(**deserialized_fields)
304
325
  end
305
326
 
@@ -308,7 +329,7 @@ module Familia
308
329
  #
309
330
  # This method is part of horreum.rb rather than serialization.rb because it
310
331
  # operates solely on the provided values and doesn't query Database or other
311
- # external sources. That's why it's called "optimistic" refresh: it assumes
332
+ # external sources. That's why it's called "naive" refresh: it assumes
312
333
  # the provided values are correct and updates the object accordingly.
313
334
  #
314
335
  # @see #refresh!
@@ -316,8 +337,8 @@ module Familia
316
337
  # @param fields [Hash] A hash of field names and their new values to update
317
338
  # the object with.
318
339
  # @return [Array] The list of field names that were updated.
319
- def optimistic_refresh(**fields)
320
- Familia.ld "[optimistic_refresh] #{self.class} #{dbkey} #{fields.keys}"
340
+ def naive_refresh(**fields)
341
+ Familia.ld "[naive_refresh] #{self.class} #{dbkey} #{fields.keys}"
321
342
  initialize_with_keyword_args_deserialize_value(**fields)
322
343
  end
323
344
 
@@ -401,8 +422,10 @@ module Familia
401
422
  self.class.fields.filter_map do |field|
402
423
  # Database will give us field names as strings back, but internally
403
424
  # we use symbols. So we check for both.
404
- value = fields[field.to_sym] || fields[field.to_s]
405
- if value
425
+ # Use fetch with default to avoid || operator which skips false values
426
+ value = fields.fetch(field.to_sym) { fields[field.to_s] }
427
+ # Check for nil explicitly to allow false and 0 values
428
+ unless value.nil?
406
429
  # Use the mapped method name, not the field name
407
430
  method_name = self.class.field_method_map[field] || field
408
431
  send(:"#{method_name}=", value)
@@ -0,0 +1,60 @@
1
+ # lib/familia/identifier_extractor.rb
2
+
3
+ module Familia
4
+ # IdentifierExtractor - Extracts identifiers from Familia objects for storage
5
+ #
6
+ # This module provides a focused mechanism for converting object references
7
+ # into Redis-storable strings. It handles two primary cases:
8
+ #
9
+ # 1. Class references: Customer → "Customer"
10
+ # 2. Familia::Base instances: customer_obj → customer_obj.identifier
11
+ #
12
+ # This is primarily used by DataType serialization when storing object
13
+ # references in Redis data structures (lists, sets, zsets). It extracts
14
+ # the identifier rather than serializing the entire object.
15
+ #
16
+ # @example With class_zset
17
+ # class Customer < Familia::Horreum
18
+ # class_zset :instances, class: self
19
+ # end
20
+ # # When adding: Customer.instances.add(customer_obj)
21
+ # # Stores: customer_obj.identifier (e.g., "customer_123")
22
+ #
23
+ module IdentifierExtractor
24
+ # Extracts a Redis-storable identifier from a Familia object or class.
25
+ #
26
+ # @param value [Object] The value to extract an identifier from
27
+ # @return [String] The extracted identifier or class name
28
+ # @raise [Familia::NotDistinguishableError] If value is not a Class or Familia::Base
29
+ #
30
+ def identifier_extractor(value, strict_values: true)
31
+ case value
32
+ when ::Symbol, ::String, ::Integer, ::Float
33
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'simple_value' if Familia.debug?
34
+ # DataTypes (lists, sets, zsets) can store simple values directly
35
+ # Convert to string for Redis storage
36
+ value.to_s
37
+
38
+ when Class
39
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'class' if Familia.debug?
40
+ value.name
41
+
42
+ when Familia::Base
43
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_instance' if Familia.debug?
44
+ value.identifier
45
+
46
+ else
47
+ # Check if value's class inherits from Familia::Base
48
+ if value.class.ancestors.member?(Familia::Base)
49
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_ancestor' if Familia.debug?
50
+ value.identifier
51
+ else
52
+ Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'error' if Familia.debug?
53
+ raise Familia::NotDistinguishableError, value
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ extend IdentifierExtractor
60
+ end
@@ -3,151 +3,307 @@
3
3
  require 'pathname'
4
4
  require 'logger'
5
5
 
6
+ # Familia - Logbook
7
+ #
6
8
  module Familia
7
- @logger = Logger.new($stdout)
8
- @logger.progname = name
9
- @logger.formatter = proc do |severity, datetime, _progname, msg|
10
- severity_letter = severity[0] # Get the first letter of the severity
11
- pid = Process.pid
12
- thread_id = Thread.current.object_id
13
- fiber_id = Fiber.current.object_id
14
- full_path, line = caller(5..5).first.split(':')[0..1]
15
- parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
16
- relative_path = full_path.sub(parent_path.to_s, 'familia')
17
- utc_datetime = datetime.utc.strftime('%m-%d %H:%M:%S.%6N')
18
-
19
- # Get the severity letter from the thread local variable or use
20
- # the default. The thread local variable is set in the trace
21
- # method in the Familia::Refinements::LoggerTrace module. The name of the
22
- # variable `severity_letter` is arbitrary and could be anything.
23
- severity_letter = Fiber[:severity_letter] || severity_letter
24
-
25
- "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}/#{fiber_id}: #{msg} [#{relative_path}:#{line}]\n"
9
+ # Custom Logger subclass with TRACE level support.
10
+ #
11
+ # FamiliaLogger extends Ruby's standard Logger with a TRACE level for
12
+ # extremely detailed debugging output. The TRACE level is numerically
13
+ # equal to DEBUG (0) but distinguishes itself via a thread-local marker
14
+ # that the LogFormatter uses to output 'T' instead of 'D'.
15
+ #
16
+ # @example Basic usage
17
+ # logger = Familia::FamiliaLogger.new($stderr)
18
+ # logger.level = Familia::FamiliaLogger::TRACE
19
+ # logger.trace "Detailed trace message"
20
+ # # => T, 10-05 20:43:09.843 pid:123 [456/789]: Detailed trace message
21
+ #
22
+ # @example With progname
23
+ # logger.trace("MyApp") { "Trace with progname" }
24
+ #
25
+ # @see Familia::LogFormatter
26
+ #
27
+ class FamiliaLogger < Logger
28
+ # TRACE severity level (numerically equal to DEBUG=0).
29
+ #
30
+ # Uses the same numeric level as DEBUG but signals via thread-local
31
+ # marker to output 'T' prefix instead of 'D'. This approach works
32
+ # around Logger's limitation with negative severity values.
33
+ #
34
+ # Standard Logger levels: DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4, UNKNOWN=5
35
+ TRACE = 0
36
+
37
+ # Log a TRACE level message.
38
+ #
39
+ # This method behaves like the standard Logger methods (debug, info, etc.)
40
+ # but outputs with a 'T' severity letter when used with LogFormatter.
41
+ #
42
+ # @param progname [String, nil] Program name to include in log output
43
+ # @yield Block that returns the message to log (lazy evaluation)
44
+ # @return [true] Always returns true
45
+ #
46
+ # @example Simple message
47
+ # logger.trace("Entering complex calculation")
48
+ #
49
+ # @example With block for lazy evaluation
50
+ # logger.trace { "Expensive: #{expensive_debug_info}" }
51
+ #
52
+ # @example With progname
53
+ # logger.trace("MyApp") { "Application trace" }
54
+ #
55
+ # @note Sets Fiber[:familia_trace_mode] during execution to
56
+ # signal LogFormatter to output 'T' instead of 'D'
57
+ #
58
+ def trace(progname = nil, &)
59
+ # Store marker in thread-local to signal this is TRACE not DEBUG
60
+ # Track whether we set the flag to avoid clearing it in nested calls
61
+ was_already_tracing = Fiber[:familia_trace_mode]
62
+ Fiber[:familia_trace_mode] = true
63
+ add(TRACE, nil, progname, &)
64
+ ensure
65
+ # Only clear the flag if we set it (not already tracing)
66
+ Fiber[:familia_trace_mode] = false unless was_already_tracing
67
+ end
26
68
  end
27
69
 
28
- # The Logging module provides a set of methods and constants for logging messages
29
- # at various levels of severity. It is designed to be used with the Ruby Logger class
30
- # to facilitate logging in applications.
70
+ # Custom formatter for Familia logger output.
71
+ #
72
+ # LogFormatter produces structured log output with severity letters,
73
+ # timestamps, process/thread/fiber IDs, and the log message.
31
74
  #
32
- # == Constants:
33
- # Logger::TRACE::
34
- # A custom log level for trace messages, typically used for very detailed
35
- # debugging information.
75
+ # Output format:
76
+ # SEVERITY, MM-DD HH:MM:SS.mmm pid:PID [THREAD_ID/FIBER_ID]: MESSAGE
36
77
  #
37
- # == Methods:
38
- # trace::
39
- # Logs a message at the TRACE level. This method is only available if the
40
- # Familia::Refinements::LoggerTrace is used.
78
+ # @example Output
79
+ # I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Connection established
80
+ # T, 10-05 20:43:10.123 pid:12345 [67890/54321]: [LOAD] redis -> user:123
41
81
  #
42
- # debug::
43
- # Logs a message at the DEBUG level. This is used for low-level system information
44
- # for debugging purposes.
82
+ # Severity letters:
83
+ # T = TRACE (when Fiber[:familia_trace_mode] is set, or level 0 when not using FamiliaLogger)
84
+ # D = DEBUG
85
+ # I = INFO
86
+ # W = WARN
87
+ # E = ERROR
88
+ # F = FATAL
89
+ # U = UNKNOWN
45
90
  #
46
- # info::
47
- # Logs a message at the INFO level. This is used for general information about
48
- # system operation.
91
+ # @example Use with FamiliaLogger for TRACE support
92
+ # logger = Familia::FamiliaLogger.new($stderr)
93
+ # logger.formatter = Familia::LogFormatter.new
94
+ # logger.trace("Trace message") # => T, ...
95
+ # logger.debug("Debug message") # => D, ...
49
96
  #
50
- # warn::
51
- # Logs a message at the WARN level. This is used for warning messages, typically
52
- # for non-critical issues that require attention.
97
+ # @example Use with standard Logger (level 0 becomes 'T')
98
+ # logger = Logger.new($stderr)
99
+ # logger.formatter = Familia::LogFormatter.new
100
+ # logger.debug("Debug message") # => T, ... (because DEBUG=0)
53
101
  #
54
- # error::
55
- # Logs a message at the ERROR level. This is used for error messages, typically
56
- # for critical issues that require immediate attention.
102
+ # @note When used with FamiliaLogger, checks Fiber[:familia_trace_mode] to
103
+ # distinguish TRACE from DEBUG. When used with standard Logger, treats
104
+ # level 0 as TRACE since DEBUG and TRACE share the same numeric level.
57
105
  #
58
- # fatal::
59
- # Logs a message at the FATAL level. This is used for very severe error events
60
- # that will presumably lead the application to abort.
106
+ # @see FamiliaLogger#trace
107
+ #
108
+ class LogFormatter < Logger::Formatter
109
+ # Severity string to letter mapping.
110
+ #
111
+ # Maps severity string labels to single-letter codes for compact output.
112
+ # Note: TRACE is handled via Fiber check in #call for FamiliaLogger.
113
+ SEVERITY_LETTERS = {
114
+ 'DEBUG' => 'D',
115
+ 'INFO' => 'I',
116
+ 'WARN' => 'W',
117
+ 'ERROR' => 'E',
118
+ 'FATAL' => 'F',
119
+ 'UNKNOWN' => 'U',
120
+ 'ANY' => 'T' # ANY is Logger's label for severity < 0, treat as TRACE
121
+ }.freeze
122
+
123
+ # Format a log message with severity, timestamp, and context.
124
+ #
125
+ # @param severity [String] Severity label (e.g., "INFO", "DEBUG", "UNKNOWN")
126
+ # @param datetime [Time] Timestamp of the log message
127
+ # @param _progname [String] Program name (unused, kept for Logger compatibility)
128
+ # @param msg [String] The log message
129
+ # @return [String] Formatted log line with newline
130
+ #
131
+ # @example
132
+ # formatter = Familia::LogFormatter.new
133
+ # formatter.call("INFO", Time.now, nil, "Test message")
134
+ # # => "I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Test message\n"
135
+ #
136
+ def call(severity, datetime, _progname, msg)
137
+ # Check if we're in trace mode (TRACE uses same level as DEBUG but marks itself)
138
+ # FamiliaLogger sets Fiber[:familia_trace_mode] when trace() is called
139
+ severity_letter = if Fiber[:familia_trace_mode]
140
+ 'T'
141
+ else
142
+ SEVERITY_LETTERS.fetch(severity, severity[0])
143
+ end
144
+
145
+ utc_datetime = datetime.utc.strftime('%H:%M:%S.%3N')
146
+
147
+ "#{severity_letter}, #{utc_datetime} #{msg}\n"
148
+ end
149
+ end
150
+
151
+ # The Logging module provides logging capabilities for Familia.
152
+ #
153
+ # Familia uses a custom FamiliaLogger that extends the standard Ruby Logger
154
+ # with a TRACE level for detailed debugging output.
155
+ #
156
+ # == Log Levels (from most to least verbose):
157
+ # - TRACE: Extremely detailed debugging (controlled by FAMILIA_TRACE env var)
158
+ # - DEBUG: Detailed debugging information
159
+ # - INFO: General informational messages
160
+ # - WARN: Warning messages
161
+ # - ERROR: Error messages
162
+ # - FATAL: Fatal errors that cause termination
61
163
  #
62
164
  # == Usage:
63
- # To use the Logging module, you need to include the Familia::Refinements::LoggerTrace module
64
- # and use the `using` keyword to enable the refinement. This will add the TRACE
65
- # log level and the trace method to the Logger class.
66
- #
67
- # Example:
68
- # require 'logger'
69
- #
70
- # module Familia::Refinements::LoggerTrace
71
- # refine Logger do
72
- # TRACE = 0
73
- #
74
- # def trace(progname = nil, &block)
75
- # add(TRACE, nil, progname, &block)
76
- # end
77
- # end
78
- # end
79
- #
80
- # using Familia::Refinements::LoggerTrace
81
- #
82
- # logger = Logger.new(STDOUT)
83
- # logger.trace("This is a trace message")
84
- # logger.debug("This is a debug message")
85
- # logger.info("This is an info message")
86
- # logger.warn("This is a warning message")
87
- # logger.error("This is an error message")
88
- # logger.fatal("This is a fatal message")
89
- #
90
- # In this example, the Familia::Refinements::LoggerTrace module is defined with a refinement
91
- # for the Logger class. The TRACE constant and trace method are added to the Logger
92
- # class within the refinement. The `using` keyword is used to apply the refinement
93
- # in the scope where it's needed.
94
- #
95
- # == Conditions:
96
- # The trace method and TRACE log level are only available if the Familia::Refinements::LoggerTrace
97
- # module is used with the `using` keyword. Without this, the Logger class will not
98
- # have the trace method or the TRACE log level.
99
- #
100
- # == Minimum Ruby Version:
101
- # This module requires Ruby 2.0.0 or later to use refinements.
165
+ # # Use default logger
166
+ # Familia.info "Connection established"
167
+ # Familia.warn "Cache miss"
168
+ #
169
+ # # Set custom logger
170
+ # Familia.logger = Logger.new('familia.log')
171
+ #
172
+ # # Trace-level debugging (requires FAMILIA_TRACE=true)
173
+ # Familia.trace :LOAD, redis_client, "user:123", "from cache"
102
174
  #
103
175
  module Logging
104
- attr_reader :logger
105
-
106
- # Gives our logger the ability to use our trace method.
107
- using Familia::Refinements::LoggerTrace if Familia::Refinements::LoggerTrace::ENABLED
176
+ # Get the logger instance, initializing with defaults if not yet set
177
+ #
178
+ # @return [FamiliaLogger] the logger instance
179
+ #
180
+ # @example Set a custom logger
181
+ # Familia.logger = Logger.new('familia.log')
182
+ #
183
+ # @example Use the default logger
184
+ # Familia.logger.info "Connection established"
185
+ #
186
+ def logger
187
+ @logger ||= FamiliaLogger.new($stderr).tap do |log|
188
+ log.progname = name
189
+ log.formatter = LogFormatter.new
190
+ end
191
+ end
108
192
 
109
- def info(*msg)
110
- @logger.info(*msg)
193
+ # Set a custom logger instance.
194
+ #
195
+ # Allows replacing the default FamiliaLogger with any Logger-compatible
196
+ # object. Useful for integrating with application logging frameworks.
197
+ #
198
+ # @param new_logger [Logger] The logger to use
199
+ # @return [Logger] The logger that was set
200
+ #
201
+ # @example Use Rails logger
202
+ # Familia.logger = Rails.logger
203
+ #
204
+ # @example Custom file logger
205
+ # Familia.logger = Logger.new('familia.log').tap do |log|
206
+ # log.level = Logger::INFO
207
+ # end
208
+ #
209
+ def logger=(new_logger)
210
+ @logger = new_logger
111
211
  end
112
212
 
113
- def warn(*msg)
114
- @logger.warn(*msg)
213
+ # Log an informational message.
214
+ #
215
+ # @param msg [String] The message to log
216
+ # @return [true]
217
+ #
218
+ # @example
219
+ # Familia.info "Redis connection established"
220
+ #
221
+ def info(msg)
222
+ logger.info(msg)
115
223
  end
116
224
 
117
- def ld(*msg)
118
- return unless Familia.debug?
225
+ # Log a warning message.
226
+ #
227
+ # @param msg [String] The message to log
228
+ # @return [true]
229
+ #
230
+ # @example
231
+ # Familia.warn "Cache miss for key: user:123"
232
+ #
233
+ def warn(msg)
234
+ logger.warn(msg)
235
+ end
119
236
 
120
- @logger.debug(*msg)
237
+ # Log a debug message (only when Familia.debug? is true).
238
+ #
239
+ # Short for "log debug". Only outputs when FAMILIA_DEBUG environment
240
+ # variable is set to '1' or 'true'.
241
+ #
242
+ # @param msg [String] The message to log
243
+ # @return [true, nil] Returns true if logged, nil if debug disabled
244
+ #
245
+ # @example
246
+ # Familia.ld "Cache lookup for user:123"
247
+ # # Only outputs when FAMILIA_DEBUG=true
248
+ #
249
+ def ld(msg)
250
+ logger.debug(msg) if Familia.debug?
121
251
  end
122
252
 
123
- def le(*msg)
124
- @logger.error(*msg)
253
+ # Log an error message.
254
+ #
255
+ # Short for "log error".
256
+ #
257
+ # @param msg [String] The message to log
258
+ # @return [true]
259
+ #
260
+ # @example
261
+ # Familia.le "Failed to deserialize value: #{e.message}"
262
+ #
263
+ def le(msg)
264
+ logger.error(msg)
125
265
  end
126
266
 
127
- # Logs a trace message for debugging purposes if Familia.debug? is true.
267
+ # Logs a structured trace message for debugging Familia operations.
268
+ #
269
+ # This method only executes when both FAMILIA_TRACE and FAMILIA_DEBUG
270
+ # environment variables are enabled.
128
271
  #
129
272
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
130
273
  # :FROMREDIS, :LOAD, :EXISTS).
131
- # @param instance_id
132
- # @param ident [String] An identifier or key related to the operation being
133
- # traced.
134
- # @param extra_context [Array<String>, String, nil] Any extra details to include.
135
- #
136
- # @example Familia.trace :LOAD, Familia.dbclient(uri), objkey if Familia.debug?
274
+ # @param instance_id [Object] The object instance being traced (e.g., Redis client)
275
+ # @param ident [String] An identifier or key related to the operation being traced
276
+ # @param extra_context [String, nil] Any extra details to include
137
277
  #
138
278
  # @return [nil]
139
279
  #
140
- # @note This method only executes if Familia::Refinements::LoggerTrace::ENABLED is true.
141
- # @note The dbclient can be a Database object, Redis::Future (used in
142
- # pipelined and multi blocks), or nil (when the database connection isn't
143
- # relevant).
280
+ # @example
281
+ # Familia.trace :LOAD, redis_client, "user:123", "from cache"
282
+ # # Output: T, 10-05 20:43:09.843 pid:123 [456/789]: [LOAD] #<Redis> -> user:123 <-from cache
283
+ #
284
+ # @note Controlled by FAMILIA_TRACE environment variable (set to '1', 'true', or 'yes')
285
+ # @note The instance_id can be a Redis client, Redis::Future, or nil
144
286
  #
145
287
  def trace(label, instance_id = nil, ident = nil, extra_context = nil)
146
- return unless Familia::Refinements::LoggerTrace::ENABLED
288
+ return unless trace_enabled? && Familia.debug?
289
+
290
+ ident_str = ident.nil? ? '<nil>' : ident.to_s
291
+ logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
292
+ end
147
293
 
148
- # Let the other values show nothing when nil, but make it known for the focused value
149
- ident_str = (ident.nil? ? '<nil>' : ident).to_s
150
- @logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
294
+ private
295
+
296
+ # Check if trace logging is enabled via FAMILIA_TRACE environment variable.
297
+ #
298
+ # Trace logging is enabled when FAMILIA_TRACE is set to '1', 'true',
299
+ # or 'yes' (case-insensitive). Checks the environment variable on every
300
+ # call to support dynamic changes in test environments.
301
+ #
302
+ # @return [Boolean] true if trace logging is enabled
303
+ # @api private
304
+ #
305
+ def trace_enabled?
306
+ %w[1 true yes].include?(ENV.fetch('FAMILIA_TRACE', 'false').downcase)
151
307
  end
152
308
  end
153
309
  end
@@ -1,6 +1,5 @@
1
1
  # lib/familia/refinements.rb
2
2
 
3
3
  require_relative 'refinements/dear_json'
4
- require_relative 'refinements/logger_trace'
5
4
  require_relative 'refinements/stylize_words'
6
5
  require_relative 'refinements/time_literals'
@@ -11,7 +11,7 @@ module Familia
11
11
  @encryption_keys = nil
12
12
  @current_key_version = nil
13
13
  @encryption_personalization = 'FamilialMatters'.freeze
14
- @pipeline_mode = :warn
14
+ @pipelined_mode = :warn
15
15
 
16
16
  # Familia::Settings
17
17
  #
@@ -118,24 +118,24 @@ module Familia
118
118
  #
119
119
  # @example Setting pipeline mode
120
120
  # Familia.configure do |config|
121
- # config.pipeline_mode = :permissive
121
+ # config.pipelined_mode = :permissive
122
122
  # end
123
123
  #
124
- def pipeline_mode(val = nil)
124
+ def pipelined_mode(val = nil)
125
125
  if val
126
126
  unless [:strict, :warn, :permissive].include?(val)
127
127
  raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
128
128
  end
129
- @pipeline_mode = val
129
+ @pipelined_mode = val
130
130
  end
131
- @pipeline_mode || :warn # default to warn mode
131
+ @pipelined_mode || :warn # default to warn mode
132
132
  end
133
133
 
134
- def pipeline_mode=(val)
134
+ def pipelined_mode=(val)
135
135
  unless [:strict, :warn, :permissive].include?(val)
136
136
  raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
137
137
  end
138
- @pipeline_mode = val
138
+ @pipelined_mode = val
139
139
  end
140
140
 
141
141
  # Configure Familia settings
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre17'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre19'.freeze unless defined?(Familia::VERSION)
6
6
  end
data/lib/familia.rb CHANGED
@@ -39,7 +39,7 @@ module Familia
39
39
  using Refinements::StylizeWords
40
40
 
41
41
  class << self
42
- attr_accessor :debug # rubocop:disable ThreadSafety/ClassAndModuleAttributes
42
+ attr_accessor :debug
43
43
  attr_reader :members
44
44
 
45
45
  def included(member)
@@ -139,7 +139,7 @@ module Familia
139
139
  require_relative 'familia/connection'
140
140
  require_relative 'familia/settings'
141
141
  require_relative 'familia/utils'
142
- require_relative 'familia/distinguisher'
142
+ require_relative 'familia/identifier_extractor'
143
143
  require_relative 'familia/json_serializer'
144
144
 
145
145
  extend SecureIdentifier