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
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/datatype_standalone.rb
3
+
4
+ # Demonstration: Familia::StringKey for Session Storage with Atomic Transactions
5
+ #
6
+ # This example shows how to use Familia's DataType classes independently
7
+ # without inheriting from Familia::Horreum. It implements a Rack-compatible
8
+ # session store using Familia::StringKey for secure, TTL-managed storage.
9
+ #
10
+ # Key Familia Features Demonstrated:
11
+ # - Standalone DataType usage (no parent model required)
12
+ # - Atomic transactions for multi-operation consistency
13
+ # - TTL management for automatic expiration
14
+ # - JSON serialization for complex data structures
15
+ # - Direct Redis access through DataType objects
16
+
17
+ require 'rack/session/abstract/id'
18
+ require 'securerandom'
19
+
20
+ require 'base64'
21
+ require 'openssl'
22
+
23
+ # Load local development version of Familia (not the gem)
24
+ begin
25
+ require_relative '../lib/familia'
26
+ rescue LoadError
27
+ # Fall back to installed gem
28
+ require 'familia'
29
+ end
30
+
31
+ # SecureSessionStore - a rack-session compatible session store using Familia::StringKey
32
+ #
33
+ # Usage:
34
+ # ruby examples/datatype_standalone.rb
35
+ # # Or in your Rack app:
36
+ # use SecureSessionStore, secret: 'your-secret-key', expire_after: 3600
37
+ #
38
+ # @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/abstract/id.rb
39
+ # @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/encryptor.rb
40
+ #
41
+ class SecureSessionStore < Rack::Session::Abstract::PersistedSecure
42
+ unless defined?(DEFAULT_OPTIONS)
43
+ DEFAULT_OPTIONS = {
44
+ key: 'project.session',
45
+ expire_after: 86_400, # 24 hours default
46
+ namespace: 'session',
47
+ sidbits: 256, # Required by Rack::Session::Abstract::Persisted
48
+ dbclient: nil,
49
+ }.freeze
50
+ end
51
+
52
+ attr_reader :dbclient
53
+
54
+ def initialize(app, options = {})
55
+ # Require a secret for security
56
+ raise ArgumentError, 'Secret required for secure sessions' unless options[:secret]
57
+
58
+ # Merge options with defaults
59
+ options = DEFAULT_OPTIONS.merge(options)
60
+
61
+ # Configure Familia connection if redis_uri provided
62
+ @dbclient = options[:dbclient] || Familia.dbclient
63
+
64
+ super
65
+
66
+ @secret = options[:secret]
67
+ @expire_after = options[:expire_after]
68
+ @namespace = options[:namespace] || 'session'
69
+
70
+ # Derive different keys for different purposes
71
+ @hmac_key = derive_key('hmac')
72
+ @encryption_key = derive_key('encryption')
73
+ end
74
+
75
+ private
76
+
77
+ # Create a StringKey instance for a session ID
78
+ def get_stringkey(sid)
79
+ return nil if sid.to_s.empty?
80
+
81
+ key = Familia.join(@namespace, sid)
82
+ Familia::StringKey.new(key,
83
+ ttl: @expire_after,
84
+ default: nil)
85
+ end
86
+
87
+ def delete_session(_request, sid, _options)
88
+ # Extract string ID from SessionId object if needed
89
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
90
+
91
+ get_stringkey(sid_string)&.del
92
+
93
+ generate_sid
94
+ end
95
+
96
+ def valid_session_id?(sid)
97
+ return false if sid.to_s.empty?
98
+ return false unless sid.match?(/\A[a-f0-9]{64,}\z/)
99
+
100
+ # Additional security checks could go here
101
+ true
102
+ end
103
+
104
+ def valid_hmac?(data, hmac)
105
+ expected = compute_hmac(data)
106
+ return false unless hmac.is_a?(String) && expected.is_a?(String) && hmac.bytesize == expected.bytesize
107
+
108
+ Rack::Utils.secure_compare(expected, hmac)
109
+ end
110
+
111
+ def derive_key(purpose)
112
+ OpenSSL::HMAC.hexdigest('SHA256', @secret, "session-#{purpose}")
113
+ end
114
+
115
+ def compute_hmac(data)
116
+ OpenSSL::HMAC.hexdigest('SHA256', @hmac_key, data)
117
+ end
118
+
119
+ def find_session(_request, sid)
120
+ # Parent class already extracts sid from cookies
121
+ # sid may be a SessionId object or nil
122
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
123
+
124
+ # Only generate new sid if none provided or invalid
125
+ return [generate_sid, {}] unless sid_string && valid_session_id?(sid_string)
126
+
127
+ begin
128
+ stringkey = get_stringkey(sid_string)
129
+ stored_data = stringkey.value if stringkey
130
+
131
+ # If no data stored, return empty session
132
+ return [sid, {}] unless stored_data
133
+
134
+ # Verify HMAC before deserializing
135
+ data, hmac = stored_data.split('--', 2)
136
+
137
+ # If no HMAC or invalid format, create new session
138
+ unless hmac && valid_hmac?(data, hmac)
139
+ # Session tampered with - create new session
140
+ return [generate_sid, {}]
141
+ end
142
+
143
+ # Decode and parse the session data
144
+ session_data = Familia::JsonSerializer.parse(Base64.decode64(data))
145
+
146
+ [sid, session_data]
147
+ rescue Familia::PersistenceError => e
148
+ # Log error in development/debugging
149
+ Familia.ld "[Session] Error reading session #{sid_string}: #{e.message}"
150
+
151
+ # Return new session on any error
152
+ [generate_sid, {}]
153
+ end
154
+ end
155
+
156
+ def write_session(_request, sid, session_data, _options)
157
+ # Extract string ID from SessionId object if needed
158
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
159
+
160
+ # Serialize and sign the data
161
+ encoded = Base64.encode64(Familia::JsonSerializer.dump(session_data)).delete("\n")
162
+ hmac = compute_hmac(encoded)
163
+ signed_data = "#{encoded}--#{hmac}"
164
+
165
+ # Get or create StringKey for this session
166
+ stringkey = get_stringkey(sid_string)
167
+
168
+ # ATOMIC TRANSACTION: Ensures both operations succeed or both fail
169
+ #
170
+ # Before DataType transaction support (PR #160), these operations were not atomic:
171
+ # stringkey.set(signed_data)
172
+ # stringkey.update_expiration(expiration: @expire_after)
173
+ #
174
+ # With transaction support, we guarantee atomicity - critical for session storage
175
+ # where partial writes could lead to sessions without TTL (memory leaks) or
176
+ # expired sessions with stale data (security issues).
177
+ #
178
+ # RECOMMENDED PATTERN: Use DataType methods inside transaction blocks
179
+ # The transaction block automatically handles the atomic MULTI/EXEC wrapping.
180
+ # DataType methods handle key generation and provide clean, expressive syntax.
181
+ stringkey.transaction do
182
+ stringkey.set(signed_data)
183
+ stringkey.update_expiration(expiration: @expire_after) if @expire_after&.positive?
184
+ end
185
+
186
+ # ADVANCED: The block yields the Redis connection for low-level access when needed
187
+ # This is useful for operations that require direct Redis command access or
188
+ # when working with multiple DataTypes in a single transaction.
189
+ #
190
+ # stringkey.transaction do |conn|
191
+ # conn.set(stringkey.dbkey, signed_data)
192
+ # conn.expire(stringkey.dbkey, @expire_after) if @expire_after&.positive?
193
+ # end
194
+
195
+ # Return the original sid (may be SessionId object)
196
+ sid
197
+ rescue Familia::PersistenceError => e
198
+ # Log error in development/debugging
199
+ Familia.ld "[Session] Error writing session #{sid_string}: #{e.message}"
200
+
201
+ # Return false to indicate failure
202
+ false
203
+ end
204
+
205
+ # Clean up expired sessions (optional, can be called periodically)
206
+ def cleanup_expired_sessions
207
+ # This would typically be handled by Redis TTL automatically
208
+ # but you could implement manual cleanup if needed
209
+ end
210
+ end
211
+
212
+ # Demo application showing session store in action
213
+ class DemoApp
214
+ def initialize
215
+ @store = SecureSessionStore.new(
216
+ proc { |_env| [200, {}, ['Demo App']] },
217
+ secret: 'demo-secret-key-change-in-production',
218
+ expire_after: 300, # 5 minutes for demo
219
+ )
220
+ end
221
+
222
+ def call(env)
223
+ puts "\n=== Familia::StringKey Session Demo ==="
224
+
225
+ # Mock Rack environment
226
+ env['rack.session'] ||= {}
227
+ env['HTTP_COOKIE'] ||= ''
228
+
229
+ # Simulate session operations
230
+ session_id = SecureRandom.hex(32)
231
+ session_data = {
232
+ 'user_id' => '12345',
233
+ 'username' => 'demo_user',
234
+ 'login_time' => Time.now.to_i,
235
+ 'preferences' => { 'theme' => 'dark', 'lang' => 'en' },
236
+ }
237
+
238
+ puts 'Writing session data...'
239
+ result = @store.send(:write_session, nil, session_id, session_data, {})
240
+ puts " Result: #{result ? 'Success' : 'Failed'}"
241
+
242
+ puts "\nReading session data..."
243
+ found_id, found_data = @store.send(:find_session, nil, session_id)
244
+ puts " Session ID: #{found_id}"
245
+ puts " Data: #{found_data}"
246
+
247
+ puts "\nDeleting session..."
248
+ @store.send(:delete_session, nil, session_id, {})
249
+
250
+ puts "\nVerifying deletion..."
251
+ deleted_id, deleted_data = @store.send(:find_session, nil, session_id)
252
+ puts " Data after deletion: #{deleted_data}"
253
+ puts " New session ID: #{deleted_id == session_id ? 'Same' : 'Generated'}"
254
+
255
+ puts "\n✅ Demo complete!"
256
+ puts "\nKey Familia Features Used:"
257
+ puts '• Familia::StringKey for typed Redis storage'
258
+ puts '• Automatic TTL management'
259
+ puts '• Direct Redis operations (set, get, del)'
260
+ puts '• JSON serialization support'
261
+ puts '• No Horreum inheritance required'
262
+
263
+ [200, { 'Content-Type' => 'text/plain' }, ['Familia StringKey Demo - Check console output']]
264
+ end
265
+ end
266
+
267
+ # Run demo if executed directly
268
+ if __FILE__ == $0
269
+ # Ensure Redis is available
270
+ begin
271
+ Familia.dbclient.ping
272
+ rescue Familia::PersistenceError => e
273
+ puts "❌ Redis connection failed: #{e.message}"
274
+ puts ' Please ensure Redis is running on localhost:6379'
275
+ exit 1
276
+ end
277
+
278
+ # Run the demo
279
+ app = DemoApp.new
280
+ app.call({})
281
+ end
data/lib/familia/base.rb CHANGED
@@ -17,8 +17,6 @@ module Familia
17
17
 
18
18
  @features_available = nil
19
19
  @feature_definitions = nil
20
- @dump_method = :to_json
21
- @load_method = :from_json
22
20
 
23
21
  def self.included(base)
24
22
  # Ensure the including class gets its own feature registry
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/familia/connection/behavior.rb
4
+
5
+ module Familia
6
+ module Connection
7
+ # Shared connection behavior for both Horreum and DataType classes
8
+ #
9
+ # This module extracts common connection management functionality that was
10
+ # previously duplicated between Horreum::Connection and DataType::Connection.
11
+ # It provides:
12
+ #
13
+ # * URI normalization with logical_database support
14
+ # * Connection creation methods
15
+ # * Transaction and pipeline execution methods
16
+ # * Consistent connection API across object types
17
+ #
18
+ # Classes including this module must implement:
19
+ # * `dbclient(uri = nil)` - Connection resolution method
20
+ # * `build_connection_chain` (private) - Chain of Responsibility setup
21
+ #
22
+ # @example Basic usage in a class
23
+ # class MyDataStore
24
+ # include Familia::Connection::Behavior
25
+ #
26
+ # def dbclient(uri = nil)
27
+ # @connection_chain ||= build_connection_chain
28
+ # @connection_chain.handle(uri)
29
+ # end
30
+ #
31
+ # private
32
+ #
33
+ # def build_connection_chain
34
+ # # ... handler setup ...
35
+ # end
36
+ # end
37
+ #
38
+ module Behavior
39
+ def self.included(base)
40
+ base.class_eval do
41
+ attr_writer :dbclient
42
+ attr_reader :uri
43
+ end
44
+ end
45
+
46
+ # Normalizes various URI formats to a consistent URI object
47
+ #
48
+ # Handles multiple input types and considers the logical_database setting
49
+ # when uri is nil or Integer. This method is public so connection handlers
50
+ # can use it for consistent URI processing.
51
+ #
52
+ # @param uri [Integer, String, URI, nil] The URI to normalize
53
+ # @return [URI] Normalized URI object
54
+ # @raise [ArgumentError] If URI type is invalid
55
+ #
56
+ # @example Integer database number
57
+ # normalize_uri(2) # => URI with db=2 on default server
58
+ #
59
+ # @example String URI
60
+ # normalize_uri('redis://localhost:6379/1')
61
+ #
62
+ # @example nil with logical_database
63
+ # class MyModel
64
+ # include Familia::Connection::Behavior
65
+ # attr_accessor :logical_database
66
+ # end
67
+ # model = MyModel.new
68
+ # model.logical_database = 3
69
+ # model.normalize_uri(nil) # => URI with db=3
70
+ #
71
+ def normalize_uri(uri)
72
+ case uri
73
+ when Integer
74
+ new_uri = Familia.uri.dup
75
+ new_uri.db = uri
76
+ new_uri
77
+ when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
78
+ URI.parse(uri)
79
+ when URI
80
+ uri
81
+ when nil
82
+ # Use logical_database if available, otherwise fall back to Familia.uri
83
+ if respond_to?(:logical_database) && logical_database
84
+ new_uri = Familia.uri.dup
85
+ new_uri.db = logical_database
86
+ new_uri
87
+ else
88
+ Familia.uri
89
+ end
90
+ else
91
+ raise ArgumentError, "Invalid URI type: #{uri.class.name}"
92
+ end
93
+ end
94
+
95
+ # Creates a new Database connection instance
96
+ #
97
+ # This method always creates a fresh connection and does not use caching.
98
+ # Each call returns a new Redis client instance that you are responsible
99
+ # for managing and closing when done.
100
+ #
101
+ # @param uri [String, URI, Integer, nil] The URI of the Database server
102
+ # @return [Redis] A new Database client connection
103
+ #
104
+ # @example Creating a new connection
105
+ # client = create_dbclient('redis://localhost:6379/1')
106
+ # client.ping
107
+ # client.close
108
+ #
109
+ def create_dbclient(uri = nil)
110
+ parsed_uri = normalize_uri(uri)
111
+ Familia.create_dbclient(parsed_uri)
112
+ end
113
+
114
+ # Alias for create_dbclient (backward compatibility)
115
+ def connect(*)
116
+ create_dbclient(*)
117
+ end
118
+
119
+ # Sets the URI for this object's database connection
120
+ #
121
+ # @param uri [String, URI, Integer] The new URI
122
+ # @return [URI] The normalized URI
123
+ #
124
+ def uri=(uri)
125
+ @uri = normalize_uri(uri)
126
+ end
127
+
128
+ # Alias for uri (backward compatibility)
129
+ def url
130
+ uri
131
+ end
132
+
133
+ # Alias for uri= (backward compatibility)
134
+ def url=(uri)
135
+ self.uri = uri
136
+ end
137
+
138
+ # Executes a Redis transaction (MULTI/EXEC) using this object's connection context
139
+ #
140
+ # Provides atomic execution of multiple Redis commands with automatic connection
141
+ # management and operation mode enforcement. Uses the object's database and
142
+ # connection settings. Returns a MultiResult object for consistency.
143
+ #
144
+ # @yield [Redis] conn The Redis connection configured for transaction mode
145
+ # @return [MultiResult] Result object with success status and command results
146
+ #
147
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
148
+ #
149
+ # @example Basic transaction
150
+ # obj.transaction do |conn|
151
+ # conn.set('key1', 'value1')
152
+ # conn.set('key2', 'value2')
153
+ # conn.get('key1')
154
+ # end
155
+ #
156
+ # @example Reentrant behavior
157
+ # obj.transaction do |conn|
158
+ # conn.set('outer', 'value')
159
+ #
160
+ # # Nested transaction reuses same connection
161
+ # obj.transaction do |inner_conn|
162
+ # inner_conn.set('inner', 'value')
163
+ # end
164
+ # end
165
+ #
166
+ # @note Connection Inheritance:
167
+ # - Uses object's logical_database setting if configured
168
+ # - Inherits class-level database settings
169
+ # - Falls back to instance-level dbclient if set
170
+ # - Uses global connection chain as final fallback
171
+ #
172
+ # @note Transaction Context:
173
+ # - When called outside global transaction: Creates local MultiResult
174
+ # - When called inside global transaction: Yields to existing transaction
175
+ # - Maintains proper Fiber-local state for nested calls
176
+ #
177
+ # @see Familia.transaction For global transaction method
178
+ # @see MultiResult For details on the return value structure
179
+ #
180
+ def transaction(&)
181
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
182
+ Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
183
+ end
184
+
185
+ # Alias for transaction (alternate naming)
186
+ def multi(&)
187
+ transaction(&)
188
+ end
189
+
190
+ # Executes Redis commands in a pipeline using this object's connection context
191
+ #
192
+ # Batches multiple Redis commands together and sends them in a single network
193
+ # round-trip for improved performance. Uses the object's database and connection
194
+ # settings. Returns a MultiResult object for consistency.
195
+ #
196
+ # @yield [Redis] conn The Redis connection configured for pipelined mode
197
+ # @return [MultiResult] Result object with success status and command results
198
+ #
199
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
200
+ #
201
+ # @example Basic pipeline
202
+ # obj.pipelined do |conn|
203
+ # conn.set('key1', 'value1')
204
+ # conn.incr('counter')
205
+ # conn.get('key1')
206
+ # end
207
+ #
208
+ # @example Performance optimization
209
+ # # Instead of multiple round-trips:
210
+ # obj.save # Round-trip 1
211
+ # obj.increment_count # Round-trip 2
212
+ # obj.update_timestamp # Round-trip 3
213
+ #
214
+ # # Use pipeline for single round-trip:
215
+ # obj.pipelined do |conn|
216
+ # conn.hmset(obj.dbkey, obj.to_h)
217
+ # conn.hincrby(obj.dbkey, 'count', 1)
218
+ # conn.hset(obj.dbkey, 'updated_at', Time.now.to_i)
219
+ # end
220
+ #
221
+ # @note Connection Inheritance:
222
+ # - Uses object's logical_database setting if configured
223
+ # - Inherits class-level database settings
224
+ # - Falls back to instance-level dbclient if set
225
+ # - Uses global connection chain as final fallback
226
+ #
227
+ # @note Pipeline Context:
228
+ # - When called outside global pipeline: Creates local MultiResult
229
+ # - When called inside global pipeline: Yields to existing pipeline
230
+ # - Maintains proper Fiber-local state for nested calls
231
+ #
232
+ # @note Performance Considerations:
233
+ # - Best for multiple independent operations
234
+ # - Reduces network latency by batching commands
235
+ # - Commands execute independently (some may succeed, others fail)
236
+ #
237
+ # @see Familia.pipelined For global pipeline method
238
+ # @see MultiResult For details on the return value structure
239
+ # @see #transaction For atomic command execution
240
+ #
241
+ def pipelined(&block)
242
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
243
+ Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
244
+ end
245
+
246
+ # Alias for pipelined (alternate naming)
247
+ def pipeline(&block)
248
+ pipelined(&block)
249
+ end
250
+ end
251
+ end
252
+ end
@@ -219,5 +219,100 @@ module Familia
219
219
  dbclient
220
220
  end
221
221
  end
222
+
223
+ # Handler for delegating connection resolution to parent object
224
+ #
225
+ # Used by DataType objects that are attached to a parent (Horreum instance or class).
226
+ # Delegates the connection resolution to the parent's dbclient method, which allows
227
+ # DataType objects to inherit connection settings, logical_database, and transaction
228
+ # context from their parent.
229
+ #
230
+ # This preserves the existing architectural pattern where DataType objects owned by
231
+ # Horreum models use the parent's connection chain. This is the primary behavior
232
+ # for DataType objects in typical usage.
233
+ #
234
+ # @example Instance-level DataType with parent
235
+ # user = User.new(userid: 'user_123')
236
+ # user.tags # DataType that delegates to user.dbclient
237
+ #
238
+ # @example Class-level DataType with parent
239
+ # User.global_users # DataType that delegates to User.dbclient
240
+ #
241
+ class ParentDelegationHandler < BaseConnectionHandler
242
+ @allows_transaction = true
243
+ @allows_pipelined = true
244
+
245
+ def initialize(data_type)
246
+ @data_type = data_type
247
+ end
248
+
249
+ def handle(uri)
250
+ return nil unless @data_type.parent
251
+
252
+ # Delegate to parent's connection chain
253
+ # Parent can be either a Horreum class or instance
254
+ parent_connection = @data_type.parent.dbclient(uri)
255
+
256
+ if parent_connection
257
+ Familia.trace :DBCLIENT_PARENT_DELEGATION, @data_type.dbkey,
258
+ "Using parent connection from #{@data_type.parent.class}"
259
+ end
260
+
261
+ parent_connection
262
+ end
263
+ end
264
+
265
+ # Handler for standalone DataType objects without a parent
266
+ #
267
+ # Provides connection resolution for DataType objects that are created independently
268
+ # rather than being attached to a Horreum model. Checks for instance-level @dbclient
269
+ # first, then falls back to creating a connection based on logical_database option
270
+ # or global Familia connection.
271
+ #
272
+ # This enables standalone DataType usage patterns like Rack::Session implementations
273
+ # where DataType objects need independent connection management and transaction support.
274
+ #
275
+ # @example Standalone DataType with custom connection
276
+ # leaderboard = Familia::SortedSet.new('game:leaderboard')
277
+ # leaderboard.dbclient = ConnectionPool.new { Redis.new }
278
+ #
279
+ # @example Standalone DataType with logical_database option
280
+ # cache = Familia::HashKey.new('app:cache', logical_database: 2)
281
+ #
282
+ class StandaloneConnectionHandler < BaseConnectionHandler
283
+ @allows_transaction = true
284
+ @allows_pipelined = true
285
+
286
+ def initialize(data_type)
287
+ @data_type = data_type
288
+ end
289
+
290
+ def handle(uri)
291
+ # If a specific URI is provided, always use it to get a connection.
292
+ if uri
293
+ connection = Familia.dbclient(uri)
294
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
295
+ "Created standalone connection for specific URI: #{uri}"
296
+ return connection
297
+ end
298
+
299
+ # Use instance @dbclient if explicitly set and no URI was passed
300
+ instance_dbclient = @data_type.instance_variable_get(:@dbclient)
301
+ if instance_dbclient
302
+ Familia.trace :DBCLIENT_DATATYPE_INSTANCE, @data_type.dbkey,
303
+ 'Using DataType instance @dbclient'
304
+ return instance_dbclient
305
+ end
306
+
307
+ # Fall back to creating connection based on opts or global
308
+ target_uri = @data_type.opts[:logical_database]
309
+ connection = Familia.dbclient(target_uri)
310
+
311
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
312
+ "Created standalone connection for #{target_uri || 'default'}"
313
+
314
+ connection
315
+ end
316
+ end
222
317
  end
223
318
  end