familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  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 +624 -20
  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 +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -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/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,172 @@
1
+ # lib/familia/horreum/serialization.rb
2
+
3
+ module Familia
4
+ class Horreum
5
+ # Serialization - Instance-level methods for object serialization
6
+ # Handles conversion between Ruby objects and Valkey hash storage
7
+ module Serialization
8
+ # Converts the object's persistent fields to a hash for external use.
9
+ #
10
+ # Serializes persistent field values for external consumption (APIs, logs),
11
+ # excluding non-loggable fields like encrypted fields for security.
12
+ # Only non-nil values are included in the resulting hash.
13
+ #
14
+ # @return [Hash] Hash with field names as keys and serialized values
15
+ # safe for external exposure
16
+ #
17
+ # @example Converting an object to hash format for API response
18
+ # user = User.new(name: "John", email: "john@example.com", age: 30)
19
+ # user.to_h
20
+ # # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
21
+ # # encrypted fields are excluded for security
22
+ #
23
+ # @note Only loggable fields are included for security
24
+ # @note Only fields with non-nil values are included
25
+ #
26
+ def to_h
27
+ self.class.persistent_fields.each_with_object({}) do |field, hsh|
28
+ field_type = self.class.field_types[field]
29
+
30
+ # Security: Skip non-loggable fields (e.g., encrypted fields)
31
+ next unless field_type.loggable
32
+
33
+ method_name = field_type.method_name
34
+ val = send(method_name)
35
+ prepared = serialize_value(val)
36
+ Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
37
+
38
+ # Only include non-nil values in the hash for Valkey
39
+ # Use string key for database compatibility
40
+ hsh[field.to_s] = prepared unless prepared.nil?
41
+ end
42
+ end
43
+
44
+ # Converts the object's persistent fields to a hash for database storage.
45
+ #
46
+ # Serializes ALL persistent field values for database storage, including
47
+ # encrypted fields. This is used internally by commit_fields and other
48
+ # persistence operations.
49
+ #
50
+ # @return [Hash] Hash with field names as keys and serialized values
51
+ # ready for database storage
52
+ #
53
+ # @note Includes ALL persistent fields, including encrypted fields
54
+ # @note Only fields with non-nil values are included for storage efficiency
55
+ #
56
+ def to_h_for_storage
57
+ self.class.persistent_fields.each_with_object({}) do |field, hsh|
58
+ field_type = self.class.field_types[field]
59
+ method_name = field_type.method_name
60
+ val = send(method_name)
61
+ prepared = serialize_value(val)
62
+ Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
63
+
64
+ # Only include non-nil values in the hash for Valkey
65
+ # Use string key for database compatibility
66
+ hsh[field.to_s] = prepared unless prepared.nil?
67
+ end
68
+ end
69
+
70
+ # Converts the object's persistent fields to an array.
71
+ #
72
+ # Serializes all persistent field values in field definition order,
73
+ # preparing them for Valkey storage. Each value is processed through
74
+ # the serialization pipeline to ensure Valkey compatibility.
75
+ #
76
+ # @return [Array] Array of serialized field values in field order
77
+ #
78
+ # @example Converting an object to array format
79
+ # user = User.new(name: "John", email: "john@example.com", age: 30)
80
+ # user.to_a
81
+ # # => ["John", "john@example.com", "30"]
82
+ #
83
+ # @note Values are serialized using the same process as other persistence
84
+ # methods to maintain data consistency across operations.
85
+ #
86
+ def to_a
87
+ self.class.persistent_fields.filter_map do |field|
88
+ field_type = self.class.field_types[field]
89
+
90
+ # Security: Skip non-loggable fields (e.g., encrypted fields)
91
+ next unless field_type.loggable
92
+
93
+ method_name = field_type.method_name
94
+ val = send(method_name)
95
+ prepared = serialize_value(val)
96
+ Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
97
+ prepared
98
+ end
99
+ end
100
+
101
+ # Serializes a Ruby object for Valkey storage.
102
+ #
103
+ # Converts Ruby objects into the DB-compatible string representations using
104
+ # the Familia distinguisher for type coercion. Falls back to JSON serialization
105
+ # for complex types (Hash, Array) when the primary distinguisher returns nil.
106
+ #
107
+ # The serialization process:
108
+ # 1. Attempts conversion using Familia.distinguisher with relaxed type checking
109
+ # 2. For Hash/Array types that return nil, tries custom dump_method or Familia::JsonSerializer.dump
110
+ # 3. Logs warnings when serialization fails completely
111
+ #
112
+ # @param val [Object] The Ruby object to serialize for Valkey storage
113
+ #
114
+ # @return [String, nil] The serialized value ready for Valkey storage, or nil
115
+ # if serialization failed
116
+ #
117
+ # @example Serializing different data types
118
+ # serialize_value("hello") # => "hello"
119
+ # serialize_value(42) # => "42"
120
+ # serialize_value({name: "John"}) # => '{"name":"John"}'
121
+ # serialize_value([1, 2, 3]) # => "[1,2,3]"
122
+ #
123
+ # @note This method integrates with Familia's type system and supports
124
+ # custom serialization methods when available on the object
125
+ #
126
+ # @see Familia.distinguisher The primary serialization mechanism
127
+ #
128
+ def serialize_value(val)
129
+ # Security: Handle ConcealedString safely - extract encrypted data for storage
130
+ return val.encrypted_value if val.respond_to?(:encrypted_value)
131
+
132
+ prepared = Familia.distinguisher(val, strict_values: false)
133
+
134
+ # If the distinguisher returns nil, try using the dump_method but only
135
+ # use JSON serialization for complex types that need it.
136
+ if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
137
+ prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
138
+ end
139
+
140
+ # If both the distinguisher and dump_method return nil, log an error
141
+ Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
142
+
143
+ prepared
144
+ end
145
+
146
+ # Converts a Database string value back to its original Ruby type
147
+ #
148
+ # This method attempts to deserialize JSON strings back to their original
149
+ # Hash or Array types. Simple string values are returned as-is.
150
+ #
151
+ # @param val [String] The string value from Database to deserialize
152
+ # @param symbolize [Boolean] Whether to symbolize hash keys (default: true for compatibility)
153
+ # @return [Object] The deserialized value (Hash, Array, or original string)
154
+ #
155
+ def deserialize_value(val, symbolize: true)
156
+ return val if val.nil? || val == ''
157
+
158
+ # Try to parse as JSON first for complex types
159
+ begin
160
+ parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
161
+ # Only return parsed value if it's a complex type (Hash/Array)
162
+ # Simple values should remain as strings
163
+ return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
164
+ rescue Familia::SerializerError
165
+ # Not valid JSON, return as-is
166
+ end
167
+
168
+ val
169
+ end
170
+ end
171
+ end
172
+ end
@@ -7,7 +7,8 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
- # Settings - Module containing settings for Familia::Horreum (InstanceMethods)
10
+ # Settings - Instance-level configuration methods for Horreum models
11
+ # Provides per-instance settings like logical_database, dump_method, load_method, suffix
11
12
  #
12
13
  module Settings
13
14
  attr_writer :dump_method, :load_method, :suffix
@@ -7,7 +7,8 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
- # Utils - Module containing utility methods for Familia::Horreum (InstanceMethods)
10
+ # Utils - Instance-level utility methods for Familia::Horreum
11
+ # Provides identifier handling, dbkey generation, and object inspection
11
12
  #
12
13
  module Utils
13
14
  # def uri
@@ -1,100 +1,193 @@
1
1
  # lib/familia/horreum.rb
2
2
 
3
- require_relative 'horreum/subclass/definition'
4
- require_relative 'horreum/subclass/management'
5
- require_relative 'horreum/shared/settings'
6
- require_relative 'horreum/core'
3
+ require_relative 'horreum/settings'
4
+ require_relative 'horreum/connection'
5
+ require_relative 'horreum/database_commands'
6
+ require_relative 'horreum/related_fields'
7
+ require_relative 'horreum/definition'
8
+ require_relative 'horreum/management'
9
+ require_relative 'horreum/persistence'
10
+ require_relative 'horreum/serialization'
11
+ require_relative 'horreum/utils'
7
12
 
8
13
  module Familia
9
14
  #
10
- # Horreum: A module for managing Redis-based object storage and relationships
15
+ # Horreum: A Valkey/Redis-backed ORM base class providing field definitions and DataType relationships
11
16
  #
12
- # Key features:
13
- # * Provides instance-level access to a single hash in Redis
14
- # * Includes Familia for class/module level access to Database types and operations
15
- # * Uses 'hashkey' to define a Database hash referred to as "object"
16
- # * Applies a default expiry (5 years) to all keys
17
+ # Familia::Horreum serves as the foundation for creating Ruby objects that are persisted
18
+ # to and retrieved from Valkey/Redis. It provides a comprehensive field system, DataType
19
+ # relationships, and automated key generation for seamless object-relational mapping.
17
20
  #
18
- # Metaprogramming:
19
- # * The class << self block defines class-level behavior
20
- # * The `inherited` method extends ClassMethods to subclasses like
21
- # `MyModel` in the example below
21
+ # Core Features:
22
+ # * Field definition system with automatic getter/setter/fast writer generation
23
+ # * DataType relationships (sets, lists, hashes, sorted sets, counters, locks)
24
+ # * Flexible identifier strategies (symbols, procs, arrays)
25
+ # * Automatic Redis key generation and management
26
+ # * Feature system for modular functionality (expiration, safe_dump, relationships)
27
+ # * Thread-safe DataType instances with automatic freezing
28
+ # * Multiple initialization patterns for different use cases
29
+ #
30
+ # Architecture:
31
+ # * Inheriting classes automatically extend definition and management methods
32
+ # * Instance-level DataTypes are created per object with unique Redis keys
33
+ # * Class-level DataTypes are shared across all instances
34
+ # * All objects tracked in Familia.members for reloading and introspection
22
35
  #
23
36
  # Usage:
24
- # class MyModel < Familia::Horreum
37
+ # class User < Familia::Horreum
38
+ # identifier_field :email
25
39
  # field :name
26
- # field :email
40
+ # field :created_at
41
+ # set :tags
42
+ # list :activity_log
43
+ # feature :expiration
27
44
  # end
28
45
  #
46
+ # user = User.new(email: "john@example.com", name: "John")
47
+ # user.tags << "premium"
48
+ # user.save
49
+ #
29
50
  class Horreum
30
51
  include Familia::Base
31
- include Familia::Horreum::Core
52
+ include Familia::Horreum::Persistence
53
+ include Familia::Horreum::Serialization
54
+ include Familia::Horreum::Connection
55
+ include Familia::Horreum::DatabaseCommands
32
56
  include Familia::Horreum::Settings
57
+ include Familia::Horreum::Utils
33
58
 
34
59
  using Familia::Refinements::TimeLiterals
35
60
 
36
- # Singleton Class Context
61
+ # Class-Level Inheritance and Extension Management
37
62
  #
38
- # The code within this block operates on the singleton class (also known as
39
- # eigenclass or metaclass) of the current class. This means:
63
+ # This singleton class block defines the metaclass behavior that governs how
64
+ # Familia::Horreum subclasses are configured and extended when they inherit.
40
65
  #
41
- # 1. Methods defined here become class methods, not instance methods.
42
- # 2. Constants and variables set here belong to the class, not instances.
43
- # 3. This is the place to define class-level behavior and properties.
66
+ # Key Responsibilities:
67
+ # * Automatically extend subclasses with essential functionality modules
68
+ # * Register new classes in Familia.members for tracking and reloading
69
+ # * Provide class-level attribute accessors for configuration
70
+ # * Establish the inheritance chain that enables the Horreum ORM pattern
44
71
  #
45
- # Use this context for:
46
- # * Defining class methods
47
- # * Setting class-level configurations
48
- # * Creating factory methods
49
- # * Establishing relationships with other classes
72
+ # When a class inherits from Horreum, the inherited() hook automatically:
73
+ # * Extends DefinitionMethods (field, identifier_field, dbkey definitions)
74
+ # * Extends ManagementMethods (create, find, destroy operations)
75
+ # * Extends Connection (database client and connection management)
76
+ # * Extends Features (feature loading and configuration)
77
+ # * Registers the class in Familia.members for introspection
50
78
  #
51
- # Example:
52
- # class MyClass
53
- # class << self
54
- # def class_method
55
- # puts "This is a class method"
56
- # end
57
- # end
58
- # end
59
- #
60
- # MyClass.class_method # => "This is a class method"
61
- #
62
- # Note: Changes made here affect the class itself and all future instances,
63
- # but not existing instances of the class.
79
+ # Class-Level Attributes:
80
+ # * @parent - Parent object reference for nested relationships
81
+ # * @dbclient - Database connection override for this class
82
+ # * @dump_method/@load_method - Serialization method configuration
83
+ # * @has_related_fields - Flag indicating if DataType relationships are defined
64
84
  #
65
85
  class << self
66
86
  attr_accessor :parent
67
87
  # TODO: Where are we calling dbclient= from now with connection pool?
68
88
  attr_writer :dbclient, :dump_method, :load_method
69
- attr_reader :has_relations
89
+ attr_reader :has_related_fields
70
90
 
71
91
  # Extends ClassMethods to subclasses and tracks Familia members
72
92
  def inherited(member)
73
- Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
93
+ Familia.trace :HORREUM, nil, "Welcome #{member} to the family" if Familia.debug?
74
94
 
75
95
  # Class-level functionality extensions:
76
96
  member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
77
97
  member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
78
98
  member.extend(Familia::Horreum::Connection) # dbclient, connection management
79
- member.extend(Familia::Features) # feature() method for optional modules
99
+ member.extend(Familia::Features) # feature() method for optional modules
100
+
101
+ # Copy parent class configuration to child class
102
+ # This implements conventional ORM inheritance behavior where child classes
103
+ # automatically inherit all parent configuration without manual copying
104
+ parent_class = member.superclass
105
+ if parent_class.respond_to?(:identifier_field) && parent_class != Familia::Horreum
106
+ # Copy essential configuration instance variables from parent
107
+ if parent_class.identifier_field
108
+ member.instance_variable_set(:@identifier_field, parent_class.identifier_field)
109
+ end
110
+
111
+ # Copy field system configuration
112
+ member.instance_variable_set(:@fields, parent_class.fields.dup) if parent_class.fields&.any?
113
+
114
+ if parent_class.respond_to?(:field_types) && parent_class.field_types&.any?
115
+ # Copy field_types hash (FieldType instances are frozen/immutable and can be safely shared)
116
+ copied_field_types = parent_class.field_types.dup
117
+ member.instance_variable_set(:@field_types, copied_field_types)
118
+ # Re-install field methods on the child class using proper method name detection
119
+ parent_class.field_types.each_value do |field_type|
120
+ # Collect all method names that field_type.install will create
121
+ methods_to_check = [
122
+ field_type.method_name,
123
+ (field_type.method_name ? :"#{field_type.method_name}=" : nil),
124
+ field_type.fast_method_name,
125
+ ].compact
126
+
127
+ # Only install if none of the methods already exist
128
+ methods_exist = methods_to_check.any? do |method_name|
129
+ member.method_defined?(method_name) || member.private_method_defined?(method_name)
130
+ end
131
+
132
+ field_type.install(member) unless methods_exist
133
+ end
134
+ end
135
+
136
+ # Copy features configuration
137
+ if parent_class.respond_to?(:features_enabled) && parent_class.features_enabled&.any?
138
+ member.instance_variable_set(:@features_enabled, parent_class.features_enabled.dup)
139
+ end
140
+
141
+ # Copy other configuration using consistent instance variable access
142
+ if (prefix = parent_class.instance_variable_get(:@prefix))
143
+ member.instance_variable_set(:@prefix, prefix)
144
+ end
145
+ if (suffix = parent_class.instance_variable_get(:@suffix))
146
+ member.instance_variable_set(:@suffix, suffix)
147
+ end
148
+ if (logical_db = parent_class.instance_variable_get(:@logical_database))
149
+ member.instance_variable_set(:@logical_database, logical_db)
150
+ end
151
+ if (default_exp = parent_class.instance_variable_get(:@default_expiration))
152
+ member.instance_variable_set(:@default_expiration, default_exp)
153
+ end
154
+
155
+ # Copy DataType relationships
156
+ if parent_class.class_related_fields&.any?
157
+ member.instance_variable_set(:@class_related_fields, parent_class.class_related_fields.dup)
158
+ end
159
+ if parent_class.related_fields&.any?
160
+ member.instance_variable_set(:@related_fields, parent_class.related_fields.dup)
161
+ end
162
+ if parent_class.instance_variable_get(:@has_related_fields)
163
+ member.instance_variable_set(:@has_related_fields,
164
+ parent_class.instance_variable_get(:@has_related_fields))
165
+ end
166
+ end
80
167
 
81
168
  # Track all classes that inherit from Horreum
82
169
  Familia.members << member
170
+
171
+ # Set up automatic instance tracking using built-in class_sorted_set
172
+ member.class_sorted_set :instances
173
+
83
174
  super
84
175
  end
85
176
  end
86
177
 
178
+ attr_writer :dbclient
179
+
87
180
  # Instance initialization
88
- # This method sets up the object's state, including Redis-related data.
181
+ # This method sets up the object's state, including Valkey/Redis-related data.
89
182
  #
90
183
  # Usage:
91
184
  #
92
- # Session.new("abc123", "user456") # positional (brittle)
93
- # Session.new(sessid: "abc123", custid: "user456") # hash (robust)
94
- # Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
185
+ # `Session.new("abc123", "user456")` # positional (brittle)
186
+ # `Session.new(sessid: "abc123", custid: "user456")` # hash (robust)
187
+ # `Session.new({sessid: "abc123", custid: "user456"})` # legacy hash (robust)
95
188
  #
96
189
  def initialize(*args, **kwargs)
97
- Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
190
+ Familia.trace :INITIALIZE, nil, "Initializing #{self.class}" if Familia.debug?
98
191
  initialize_relatives
99
192
 
100
193
  # No longer auto-create a key field - the identifier method will
@@ -108,15 +201,16 @@ module Familia
108
201
 
109
202
  # Initialize object with arguments using one of four strategies:
110
203
  #
111
- # 1. **Identifier** (Recommended for lookups): A single argument is treated as the identifier.
112
- # Example: Customer.new("cust_123")
113
- # - Robust and convenient for creating objects from an ID.
204
+ # 1. **Identifier** (Recommended for lookups): A single argument is
205
+ # treated as the identifier. Robust and convenient for creating
206
+ # objects from an ID. e.g. `Customer.new("cust_123")`
114
207
  #
115
- # 2. **Keyword Arguments** (Recommended for creation): Order-independent field assignment
116
- # Example: Customer.new(name: "John", email: "john@example.com")
208
+ # 2. **Keyword Arguments** (Recommended for creation): Order-independent
209
+ # field assignment
210
+ # e.g. Customer.new(name: "John", email: "john@example.com")
117
211
  #
118
212
  # 3. **Positional Arguments** (Legacy): Field assignment by definition order
119
- # Example: Customer.new("cust_123", "John", "john@example.com")
213
+ # e.g. Customer.new("cust_123", "John", "john@example.com")
120
214
  #
121
215
  # 4. **No Arguments**: Object created with all fields as nil
122
216
  #
@@ -127,8 +221,8 @@ module Familia
127
221
  initialize_with_keyword_args(**kwargs)
128
222
  elsif args.any?
129
223
  initialize_with_positional_args(*args)
130
- else
131
- Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
224
+ elsif Familia.debug?
225
+ Familia.trace :INITIALIZE, nil, "#{self.class} initialized with no arguments"
132
226
  # Default values are intentionally NOT set here
133
227
  end
134
228
 
@@ -138,16 +232,27 @@ module Familia
138
232
  init
139
233
  end
140
234
 
235
+ # Override this method in subclasses for custom initialization logic.
236
+ # This is called AFTER fields are set and relatives are initialized.
237
+ #
238
+ # DO NOT override initialize() - use this init() hook instead.
239
+ #
240
+ # Example:
241
+ # def init(name = nil)
242
+ # @name = name || SecureRandom.hex(4)
243
+ # end
141
244
  def init(*args, **kwargs)
142
245
  # Default no-op
143
246
  end
144
247
 
145
248
  # Sets up related Database objects for the instance
146
- # This method is crucial for establishing Redis-based relationships
249
+ # This method is crucial for establishing Valkey/Redis-based relationships
147
250
  #
148
251
  # This needs to be called in the initialize method.
149
252
  #
150
253
  def initialize_relatives
254
+ # Store initialization flag on singleton class to avoid polluting instance variables
255
+ return if singleton_class.instance_variable_defined?(:"@relatives_initialized")
151
256
  # Generate instances of each DataType. These need to be
152
257
  # unique for each instance of this class so they can piggyback
153
258
  # on the specifc index of this instance.
@@ -159,7 +264,7 @@ module Familia
159
264
  self.class.related_fields.each_pair do |name, data_type_definition|
160
265
  klass = data_type_definition.klass
161
266
  opts = data_type_definition.opts
162
- Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
267
+ Familia.trace :INITIALIZE_RELATIVES, nil, "#{name} => #{klass} #{opts.keys}" if Familia.debug?
163
268
 
164
269
  # As a subclass of Familia::Horreum, we add ourselves as the parent
165
270
  # automatically. This is what determines the dbkey for DataType
@@ -169,7 +274,8 @@ module Familia
169
274
  # then the dbkey for this DataType instance will be
170
275
  # `customer:customer_id:name`.
171
276
  #
172
- opts[:parent] = self # unless opts.key(:parent)
277
+ # Store reference to the instance for lazy ParentDefinition creation
278
+ opts[:parent] = self
173
279
 
174
280
  suffix_override = opts.fetch(:suffix, name)
175
281
 
@@ -186,49 +292,10 @@ module Familia
186
292
  # e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
187
293
  instance_variable_set :"@#{name}", related_object
188
294
  end
189
- end
190
295
 
191
- # Initializes the object with positional arguments.
192
- # Maps each argument to a corresponding field in the order they are defined.
193
- #
194
- # @param args [Array] List of values to be assigned to fields
195
- # @return [Array<Symbol>] List of field names that were successfully updated
196
- # (i.e., had non-nil values assigned)
197
- # @private
198
- def initialize_with_positional_args(*args)
199
- Familia.trace :INITIALIZE_ARGS, dbclient, args, caller(1..1) if Familia.debug?
200
- self.class.fields.zip(args).filter_map do |field, value|
201
- if value
202
- send(:"#{field}=", value)
203
- field.to_sym
204
- end
205
- end
296
+ # Mark relatives as initialized on singleton class to avoid polluting instance variables
297
+ singleton_class.instance_variable_set(:"@relatives_initialized", true)
206
298
  end
207
- private :initialize_with_positional_args
208
-
209
- # Initializes the object with keyword arguments.
210
- # Assigns values to fields based on the provided hash of field names and values.
211
- # Handles both symbol and string keys to accommodate different sources of data.
212
- #
213
- # @param fields [Hash] Hash of field names (as symbols or strings) and their values
214
- # @return [Array<Symbol>] List of field names that were successfully updated
215
- # (i.e., had non-nil values assigned)
216
- # @private
217
- def initialize_with_keyword_args(**fields)
218
- Familia.trace :INITIALIZE_KWARGS, dbclient, fields.keys, caller(1..1) if Familia.debug?
219
- self.class.fields.filter_map do |field|
220
- # Database will give us field names as strings back, but internally
221
- # we use symbols. So we check for both.
222
- value = fields[field.to_sym] || fields[field.to_s]
223
- if value
224
- # Use the mapped method name, not the field name
225
- method_name = self.class.field_method_map[field] || field
226
- send(:"#{method_name}=", value)
227
- field.to_sym
228
- end
229
- end
230
- end
231
- private :initialize_with_keyword_args
232
299
 
233
300
  def initialize_with_keyword_args_deserialize_value(**fields)
234
301
  # Deserialize Database string values back to their original types
@@ -267,36 +334,28 @@ module Familia
267
334
  send(definition)
268
335
  when Proc
269
336
  definition.call(self)
270
- end
337
+ end
271
338
 
272
339
  # Return nil for unpopulated identifiers (like unsaved ActiveRecord objects)
273
- # Only raise errors when the identifier is actually needed for Redis operations
340
+ # Only raise errors when the identifier is actually needed for db operations
274
341
  return nil if unique_id.nil? || unique_id.to_s.empty?
275
342
 
276
343
  unique_id
277
344
  end
278
345
 
279
- attr_writer :dbclient
280
-
281
- # Summon the mystical Database connection from the depths of instance or class.
346
+ # Returns the Database connection for the instance using Chain of Responsibility pattern.
282
347
  #
283
- # This method is like a magical divining rod, always pointing to the nearest
284
- # source of Database goodness. It first checks if we have a personal Redis
285
- # connection (@dbclient), and if not, it borrows the class's connection.
348
+ # This method uses a chain of handlers to resolve connections in priority order:
349
+ # 1. FiberTransactionHandler - Fiber[:familia_transaction] (active transaction)
350
+ # 2. CachedConnectionHandler - Accesses self.dbclient
351
+ # 3. CachedConnectionHandler - Accesses self.class.dbclient
352
+ # 4. GlobalFallbackHandler - Familia.dbclient(uri || logical_database) (global fallback)
286
353
  #
287
- # @return [Redis] A shimmering Database connection, ready for your bidding.
354
+ # @return [Redis] the Database connection instance.
288
355
  #
289
- # @example Finding your Database way
290
- # puts object.dbclient
291
- # # => #<Redis client v5.4.1 for redis://localhost:6379/0>
292
- #
293
- def dbclient
294
- Fiber[:familia_transaction] || @dbclient || self.class.dbclient
295
- # conn.select(self.class.logical_database)
296
- end
297
356
 
298
357
  def generate_id
299
- @objid ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
358
+ @objid ||= Familia.generate_id
300
359
  end
301
360
 
302
361
  # The principle is: **If Familia objects have `to_s`, then they should work
@@ -309,5 +368,49 @@ module Familia
309
368
 
310
369
  identifier.to_s
311
370
  end
371
+
372
+ private
373
+
374
+ # Initializes the object with positional arguments.
375
+ # Maps each argument to a corresponding field in the order they are defined.
376
+ #
377
+ # @param args [Array] List of values to be assigned to fields
378
+ # @return [Array<Symbol>] List of field names that were successfully updated
379
+ # (i.e., had non-nil values assigned)
380
+ # @private
381
+ def initialize_with_positional_args(*args)
382
+ Familia.trace :INITIALIZE_ARGS, nil, args if Familia.debug?
383
+ self.class.fields.zip(args).filter_map do |field, value|
384
+ if value
385
+ send(:"#{field}=", value)
386
+ field.to_sym
387
+ end
388
+ end
389
+ end
390
+
391
+ # Initializes the object with keyword arguments.
392
+ # Assigns values to fields based on the provided hash of field names and values.
393
+ # Handles both symbol and string keys to accommodate different sources of data.
394
+ #
395
+ # @param fields [Hash] Hash of field names (as symbols or strings) and their values
396
+ # @return [Array<Symbol>] List of field names that were successfully updated
397
+ # (i.e., had non-nil values assigned)
398
+ # @private
399
+ def initialize_with_keyword_args(**fields)
400
+ Familia.trace :INITIALIZE_KWARGS, nil, fields.keys if Familia.debug?
401
+ self.class.fields.filter_map do |field|
402
+ # Database will give us field names as strings back, but internally
403
+ # we use symbols. So we check for both.
404
+ value = fields[field.to_sym] || fields[field.to_s]
405
+ if value
406
+ # Use the mapped method name, not the field name
407
+ method_name = self.class.field_method_map[field] || field
408
+ send(:"#{method_name}=", value)
409
+ field.to_sym
410
+ end
411
+ end
412
+ end
413
+
414
+ # Builds the instance-level connection chain with handlers in priority order
312
415
  end
313
416
  end