familia 2.0.0.pre14 → 2.0.0.pre16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +623 -19
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -7,25 +7,41 @@ require_relative 'horreum/core'
7
7
 
8
8
  module Familia
9
9
  #
10
- # Horreum: A module for managing Redis-based object storage and relationships
10
+ # Horreum: A Valkey/Redis-backed ORM base class providing field definitions and DataType relationships
11
11
  #
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
12
+ # Familia::Horreum serves as the foundation for creating Ruby objects that are persisted
13
+ # to and retrieved from Valkey/Redis. It provides a comprehensive field system, DataType
14
+ # relationships, and automated key generation for seamless object-relational mapping.
17
15
  #
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
16
+ # Core Features:
17
+ # * Field definition system with automatic getter/setter/fast writer generation
18
+ # * DataType relationships (sets, lists, hashes, sorted sets, counters, locks)
19
+ # * Flexible identifier strategies (symbols, procs, arrays)
20
+ # * Automatic Redis key generation and management
21
+ # * Feature system for modular functionality (expiration, safe_dump, relationships)
22
+ # * Thread-safe DataType instances with automatic freezing
23
+ # * Multiple initialization patterns for different use cases
24
+ #
25
+ # Architecture:
26
+ # * Inheriting classes automatically extend definition and management methods
27
+ # * Instance-level DataTypes are created per object with unique Redis keys
28
+ # * Class-level DataTypes are shared across all instances
29
+ # * All objects tracked in Familia.members for reloading and introspection
22
30
  #
23
31
  # Usage:
24
- # class MyModel < Familia::Horreum
32
+ # class User < Familia::Horreum
33
+ # identifier_field :email
25
34
  # field :name
26
- # field :email
35
+ # field :created_at
36
+ # set :tags
37
+ # list :activity_log
38
+ # feature :expiration
27
39
  # end
28
40
  #
41
+ # user = User.new(email: "john@example.com", name: "John")
42
+ # user.tags << "premium"
43
+ # user.save
44
+ #
29
45
  class Horreum
30
46
  include Familia::Base
31
47
  include Familia::Horreum::Core
@@ -33,68 +49,136 @@ module Familia
33
49
 
34
50
  using Familia::Refinements::TimeLiterals
35
51
 
36
- # Singleton Class Context
37
- #
38
- # The code within this block operates on the singleton class (also known as
39
- # eigenclass or metaclass) of the current class. This means:
52
+ # Class-Level Inheritance and Extension Management
40
53
  #
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.
54
+ # This singleton class block defines the metaclass behavior that governs how
55
+ # Familia::Horreum subclasses are configured and extended when they inherit.
44
56
  #
45
- # Use this context for:
46
- # * Defining class methods
47
- # * Setting class-level configurations
48
- # * Creating factory methods
49
- # * Establishing relationships with other classes
57
+ # Key Responsibilities:
58
+ # * Automatically extend subclasses with essential functionality modules
59
+ # * Register new classes in Familia.members for tracking and reloading
60
+ # * Provide class-level attribute accessors for configuration
61
+ # * Establish the inheritance chain that enables the Horreum ORM pattern
50
62
  #
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
63
+ # When a class inherits from Horreum, the inherited() hook automatically:
64
+ # * Extends DefinitionMethods (field, identifier_field, dbkey definitions)
65
+ # * Extends ManagementMethods (create, find, destroy operations)
66
+ # * Extends Connection (database client and connection management)
67
+ # * Extends Features (feature loading and configuration)
68
+ # * Registers the class in Familia.members for introspection
59
69
  #
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.
70
+ # Class-Level Attributes:
71
+ # * @parent - Parent object reference for nested relationships
72
+ # * @dbclient - Database connection override for this class
73
+ # * @dump_method/@load_method - Serialization method configuration
74
+ # * @has_related_fields - Flag indicating if DataType relationships are defined
64
75
  #
65
76
  class << self
66
77
  attr_accessor :parent
67
78
  # TODO: Where are we calling dbclient= from now with connection pool?
68
79
  attr_writer :dbclient, :dump_method, :load_method
69
- attr_reader :has_relations
80
+ attr_reader :has_related_fields
70
81
 
71
82
  # Extends ClassMethods to subclasses and tracks Familia members
72
83
  def inherited(member)
73
- Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
84
+ Familia.trace :HORREUM, nil, "Welcome #{member} to the family" if Familia.debug?
74
85
 
75
86
  # Class-level functionality extensions:
76
87
  member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
77
88
  member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
78
89
  member.extend(Familia::Horreum::Connection) # dbclient, connection management
79
- member.extend(Familia::Features) # feature() method for optional modules
90
+ member.extend(Familia::Features) # feature() method for optional modules
91
+
92
+ # Copy parent class configuration to child class
93
+ # This implements conventional ORM inheritance behavior where child classes
94
+ # automatically inherit all parent configuration without manual copying
95
+ parent_class = member.superclass
96
+ if parent_class.respond_to?(:identifier_field) && parent_class != Familia::Horreum
97
+ # Copy essential configuration instance variables from parent
98
+ if parent_class.identifier_field
99
+ member.instance_variable_set(:@identifier_field, parent_class.identifier_field)
100
+ end
101
+
102
+ # Copy field system configuration
103
+ member.instance_variable_set(:@fields, parent_class.fields.dup) if parent_class.fields&.any?
104
+
105
+ if parent_class.respond_to?(:field_types) && parent_class.field_types&.any?
106
+ # Copy field_types hash (FieldType instances are frozen/immutable and can be safely shared)
107
+ copied_field_types = parent_class.field_types.dup
108
+ member.instance_variable_set(:@field_types, copied_field_types)
109
+ # Re-install field methods on the child class using proper method name detection
110
+ parent_class.field_types.each_value do |field_type|
111
+ # Collect all method names that field_type.install will create
112
+ methods_to_check = [
113
+ field_type.method_name,
114
+ (field_type.method_name ? :"#{field_type.method_name}=" : nil),
115
+ field_type.fast_method_name,
116
+ ].compact
117
+
118
+ # Only install if none of the methods already exist
119
+ methods_exist = methods_to_check.any? do |method_name|
120
+ member.method_defined?(method_name) || member.private_method_defined?(method_name)
121
+ end
122
+
123
+ field_type.install(member) unless methods_exist
124
+ end
125
+ end
126
+
127
+ # Copy features configuration
128
+ if parent_class.respond_to?(:features_enabled) && parent_class.features_enabled&.any?
129
+ member.instance_variable_set(:@features_enabled, parent_class.features_enabled.dup)
130
+ end
131
+
132
+ # Copy other configuration using consistent instance variable access
133
+ if (prefix = parent_class.instance_variable_get(:@prefix))
134
+ member.instance_variable_set(:@prefix, prefix)
135
+ end
136
+ if (suffix = parent_class.instance_variable_get(:@suffix))
137
+ member.instance_variable_set(:@suffix, suffix)
138
+ end
139
+ if (logical_db = parent_class.instance_variable_get(:@logical_database))
140
+ member.instance_variable_set(:@logical_database, logical_db)
141
+ end
142
+ if (default_exp = parent_class.instance_variable_get(:@default_expiration))
143
+ member.instance_variable_set(:@default_expiration, default_exp)
144
+ end
145
+
146
+ # Copy DataType relationships
147
+ if parent_class.class_related_fields&.any?
148
+ member.instance_variable_set(:@class_related_fields, parent_class.class_related_fields.dup)
149
+ end
150
+ if parent_class.related_fields&.any?
151
+ member.instance_variable_set(:@related_fields, parent_class.related_fields.dup)
152
+ end
153
+ if parent_class.instance_variable_get(:@has_related_fields)
154
+ member.instance_variable_set(:@has_related_fields,
155
+ parent_class.instance_variable_get(:@has_related_fields))
156
+ end
157
+ end
80
158
 
81
159
  # Track all classes that inherit from Horreum
82
160
  Familia.members << member
161
+
162
+ # Set up automatic instance tracking using built-in class_sorted_set
163
+ member.class_sorted_set :instances
164
+
83
165
  super
84
166
  end
85
167
  end
86
168
 
169
+ attr_writer :dbclient
170
+
87
171
  # Instance initialization
88
- # This method sets up the object's state, including Redis-related data.
172
+ # This method sets up the object's state, including Valkey/Redis-related data.
89
173
  #
90
174
  # Usage:
91
175
  #
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)
176
+ # `Session.new("abc123", "user456")` # positional (brittle)
177
+ # `Session.new(sessid: "abc123", custid: "user456")` # hash (robust)
178
+ # `Session.new({sessid: "abc123", custid: "user456"})` # legacy hash (robust)
95
179
  #
96
180
  def initialize(*args, **kwargs)
97
- Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
181
+ Familia.trace :INITIALIZE, nil, "Initializing #{self.class}" if Familia.debug?
98
182
  initialize_relatives
99
183
 
100
184
  # No longer auto-create a key field - the identifier method will
@@ -108,15 +192,16 @@ module Familia
108
192
 
109
193
  # Initialize object with arguments using one of four strategies:
110
194
  #
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.
195
+ # 1. **Identifier** (Recommended for lookups): A single argument is
196
+ # treated as the identifier. Robust and convenient for creating
197
+ # objects from an ID. e.g. `Customer.new("cust_123")`
114
198
  #
115
- # 2. **Keyword Arguments** (Recommended for creation): Order-independent field assignment
116
- # Example: Customer.new(name: "John", email: "john@example.com")
199
+ # 2. **Keyword Arguments** (Recommended for creation): Order-independent
200
+ # field assignment
201
+ # e.g. Customer.new(name: "John", email: "john@example.com")
117
202
  #
118
203
  # 3. **Positional Arguments** (Legacy): Field assignment by definition order
119
- # Example: Customer.new("cust_123", "John", "john@example.com")
204
+ # e.g. Customer.new("cust_123", "John", "john@example.com")
120
205
  #
121
206
  # 4. **No Arguments**: Object created with all fields as nil
122
207
  #
@@ -127,8 +212,8 @@ module Familia
127
212
  initialize_with_keyword_args(**kwargs)
128
213
  elsif args.any?
129
214
  initialize_with_positional_args(*args)
130
- else
131
- Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
215
+ elsif Familia.debug?
216
+ Familia.trace :INITIALIZE, nil, "#{self.class} initialized with no arguments"
132
217
  # Default values are intentionally NOT set here
133
218
  end
134
219
 
@@ -143,7 +228,7 @@ module Familia
143
228
  end
144
229
 
145
230
  # Sets up related Database objects for the instance
146
- # This method is crucial for establishing Redis-based relationships
231
+ # This method is crucial for establishing Valkey/Redis-based relationships
147
232
  #
148
233
  # This needs to be called in the initialize method.
149
234
  #
@@ -159,7 +244,7 @@ module Familia
159
244
  self.class.related_fields.each_pair do |name, data_type_definition|
160
245
  klass = data_type_definition.klass
161
246
  opts = data_type_definition.opts
162
- Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
247
+ Familia.trace :INITIALIZE_RELATIVES, nil, "#{name} => #{klass} #{opts.keys}" if Familia.debug?
163
248
 
164
249
  # As a subclass of Familia::Horreum, we add ourselves as the parent
165
250
  # automatically. This is what determines the dbkey for DataType
@@ -169,7 +254,8 @@ module Familia
169
254
  # then the dbkey for this DataType instance will be
170
255
  # `customer:customer_id:name`.
171
256
  #
172
- opts[:parent] = self # unless opts.key(:parent)
257
+ # Store reference to the instance for lazy ParentDefinition creation
258
+ opts[:parent] = self
173
259
 
174
260
  suffix_override = opts.fetch(:suffix, name)
175
261
 
@@ -188,48 +274,6 @@ module Familia
188
274
  end
189
275
  end
190
276
 
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
206
- 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
-
233
277
  def initialize_with_keyword_args_deserialize_value(**fields)
234
278
  # Deserialize Database string values back to their original types
235
279
  deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
@@ -270,33 +314,26 @@ module Familia
270
314
  end
271
315
 
272
316
  # Return nil for unpopulated identifiers (like unsaved ActiveRecord objects)
273
- # Only raise errors when the identifier is actually needed for Redis operations
317
+ # Only raise errors when the identifier is actually needed for db operations
274
318
  return nil if unique_id.nil? || unique_id.to_s.empty?
275
319
 
276
320
  unique_id
277
321
  end
278
322
 
279
- attr_writer :dbclient
280
-
281
- # Summon the mystical Database connection from the depths of instance or class.
282
- #
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.
323
+ # Returns the Database connection for the instance using Chain of Responsibility pattern.
286
324
  #
287
- # @return [Redis] A shimmering Database connection, ready for your bidding.
325
+ # This method uses a chain of handlers to resolve connections in priority order:
326
+ # 1. FiberTransactionHandler - Fiber[:familia_transaction] (active transaction)
327
+ # 2. CachedConnectionHandler - Accesses self.dbclient
328
+ # 3. CachedConnectionHandler - Accesses self.class.dbclient
329
+ # 4. GlobalFallbackHandler - Familia.dbclient(uri || logical_database) (global fallback)
288
330
  #
289
- # @example Finding your Database way
290
- # puts object.dbclient
291
- # # => #<Redis client v5.4.1 for redis://localhost:6379/0>
331
+ # @return [Redis] the Database connection instance.
292
332
  #
293
- def dbclient
294
- Fiber[:familia_transaction] || @dbclient || self.class.dbclient
295
- # conn.select(self.class.logical_database)
296
- end
333
+
297
334
 
298
335
  def generate_id
299
- @objid ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
336
+ @objid ||= Familia.generate_id
300
337
  end
301
338
 
302
339
  # The principle is: **If Familia objects have `to_s`, then they should work
@@ -309,5 +346,50 @@ module Familia
309
346
 
310
347
  identifier.to_s
311
348
  end
349
+
350
+ private
351
+
352
+ # Initializes the object with positional arguments.
353
+ # Maps each argument to a corresponding field in the order they are defined.
354
+ #
355
+ # @param args [Array] List of values to be assigned to fields
356
+ # @return [Array<Symbol>] List of field names that were successfully updated
357
+ # (i.e., had non-nil values assigned)
358
+ # @private
359
+ def initialize_with_positional_args(*args)
360
+ Familia.trace :INITIALIZE_ARGS, nil, args if Familia.debug?
361
+ self.class.fields.zip(args).filter_map do |field, value|
362
+ if value
363
+ send(:"#{field}=", value)
364
+ field.to_sym
365
+ end
366
+ end
367
+ end
368
+
369
+ # Initializes the object with keyword arguments.
370
+ # Assigns values to fields based on the provided hash of field names and values.
371
+ # Handles both symbol and string keys to accommodate different sources of data.
372
+ #
373
+ # @param fields [Hash] Hash of field names (as symbols or strings) and their values
374
+ # @return [Array<Symbol>] List of field names that were successfully updated
375
+ # (i.e., had non-nil values assigned)
376
+ # @private
377
+ def initialize_with_keyword_args(**fields)
378
+ Familia.trace :INITIALIZE_KWARGS, nil, fields.keys if Familia.debug?
379
+ self.class.fields.filter_map do |field|
380
+ # Database will give us field names as strings back, but internally
381
+ # we use symbols. So we check for both.
382
+ value = fields[field.to_sym] || fields[field.to_s]
383
+ if value
384
+ # Use the mapped method name, not the field name
385
+ method_name = self.class.field_method_map[field] || field
386
+ send(:"#{method_name}=", value)
387
+ field.to_sym
388
+ end
389
+ end
390
+ end
391
+
392
+ # Builds the instance-level connection chain with handlers in priority order
393
+
312
394
  end
313
395
  end
@@ -13,7 +13,6 @@ module Familia
13
13
  # parsed = Familia::JsonSerializer.parse(json, symbolize_names: true)
14
14
  #
15
15
  module JsonSerializer
16
-
17
16
  class << self
18
17
  # Parse JSON string into Ruby objects
19
18
  #
@@ -10,6 +10,7 @@ module Familia
10
10
  severity_letter = severity[0] # Get the first letter of the severity
11
11
  pid = Process.pid
12
12
  thread_id = Thread.current.object_id
13
+ fiber_id = Fiber.current.object_id
13
14
  full_path, line = caller(5..5).first.split(':')[0..1]
14
15
  parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
15
16
  relative_path = full_path.sub(parent_path.to_s, 'familia')
@@ -19,9 +20,9 @@ module Familia
19
20
  # the default. The thread local variable is set in the trace
20
21
  # method in the Familia::Refinements::LoggerTrace module. The name of the
21
22
  # variable `severity_letter` is arbitrary and could be anything.
22
- severity_letter = Thread.current[:severity_letter] || severity_letter
23
+ severity_letter = Fiber[:severity_letter] || severity_letter
23
24
 
24
- "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg} [#{relative_path}:#{line}]\n"
25
+ "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}/#{fiber_id}: #{msg} [#{relative_path}:#{line}]\n"
25
26
  end
26
27
 
27
28
  # The Logging module provides a set of methods and constants for logging messages
@@ -103,9 +104,7 @@ module Familia
103
104
  attr_reader :logger
104
105
 
105
106
  # Gives our logger the ability to use our trace method.
106
- if Familia::Refinements::LoggerTrace::ENABLED
107
- using Familia::Refinements::LoggerTrace
108
- end
107
+ using Familia::Refinements::LoggerTrace if Familia::Refinements::LoggerTrace::ENABLED
109
108
 
110
109
  def info(*msg)
111
110
  @logger.info(*msg)
@@ -129,16 +128,12 @@ module Familia
129
128
  #
130
129
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
131
130
  # :FROMREDIS, :LOAD, :EXISTS).
132
- # @param dbclient [Redis, Redis::Future, nil] The Database instance or
133
- # Future being used.
131
+ # @param instance_id
134
132
  # @param ident [String] An identifier or key related to the operation being
135
133
  # traced.
136
- # @param context [Array<String>, String, nil] The calling context, typically
137
- # obtained from `caller` or `caller.first`. Default is nil.
134
+ # @param extra_context [Array<String>, String, nil] Any extra details to include.
138
135
  #
139
- # @example
140
- # Familia.trace :LOAD, Familia.dbclient(uri), objkey, caller(1..1) if
141
- # Familia.debug?
136
+ # @example Familia.trace :LOAD, Familia.dbclient(uri), objkey if Familia.debug?
142
137
  #
143
138
  # @return [nil]
144
139
  #
@@ -147,110 +142,12 @@ module Familia
147
142
  # pipelined and multi blocks), or nil (when the database connection isn't
148
143
  # relevant).
149
144
  #
150
- def trace(label, dbclient, ident, context = nil)
145
+ def trace(label, instance_id = nil, ident = nil, extra_context = nil)
151
146
  return unless Familia::Refinements::LoggerTrace::ENABLED
152
147
 
153
- # Usually dbclient is a Database object, but it could be
154
- # a Redis::Future which is what is used inside of pipelined
155
- # and multi blocks. In some contexts it's nil where the
156
- # database connection isn't relevant.
157
- instance_id = if dbclient
158
- case dbclient
159
- when Redis
160
- dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
161
- when Redis::Future
162
- 'Redis::Future'
163
- else
164
- dbclient.class.name
165
- end
166
- end
167
-
168
- codeline = if context
169
- context = [context].flatten
170
- context.reject! { |line| line =~ %r{lib/familia} }
171
- context.first
172
- end
173
-
174
- @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
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)
175
151
  end
176
152
  end
177
153
  end
178
-
179
-
180
- __END__
181
-
182
-
183
- ### Example 1: Basic Logging
184
- ```ruby
185
- require 'logger'
186
-
187
- logger = Logger.new($stdout)
188
- logger.info("This is an info message")
189
- logger.warn("This is a warning message")
190
- logger.error("This is an error message")
191
- ```
192
-
193
- ### Example 2: Setting Log Level
194
- ```ruby
195
- require 'logger'
196
-
197
- logger = Logger.new($stdout)
198
- logger.level = Logger::WARN
199
-
200
- logger.debug("This is a debug message") # Will not be logged
201
- logger.info("This is an info message") # Will not be logged
202
- logger.warn("This is a warning message")
203
- logger.error("This is an error message")
204
- ```
205
-
206
- ### Example 3: Customizing Log Format
207
- ```ruby
208
- require 'logger'
209
-
210
- logger = Logger.new($stdout)
211
- logger.formatter = proc do |severity, datetime, progname, msg|
212
- "#{datetime}: #{severity} - #{msg}\n"
213
- end
214
-
215
- logger.info("This is an info message")
216
- logger.warn("This is a warning message")
217
- logger.error("This is an error message")
218
- ```
219
-
220
- ### Example 4: Logging with a Program Name
221
- ```ruby
222
- require 'logger'
223
-
224
- logger = Logger.new($stdout)
225
- logger.progname = 'Familia'
226
-
227
- logger.info("This is an info message")
228
- logger.warn("This is a warning message")
229
- logger.error("This is an error message")
230
- ```
231
-
232
- ### Example 5: Logging with a Block
233
- ```ruby
234
- require 'logger'
235
-
236
- # Calling any of the methods above with a block
237
- # (affects only the one entry).
238
- # Doing so can have two benefits:
239
- #
240
- # - Context: the block can evaluate the entire program context
241
- # and create a context-dependent message.
242
- # - Performance: the block is not evaluated unless the log level
243
- # permits the entry actually to be written:
244
- #
245
- # logger.error { my_slow_message_generator }
246
- #
247
- # Contrast this with the string form, where the string is
248
- # always evaluated, regardless of the log level:
249
- #
250
- # logger.error("#{my_slow_message_generator}")
251
- logger = Logger.new($stdout)
252
-
253
- logger.info { "This is an info message" }
254
- logger.warn { "This is a warning message" }
255
- logger.error { "This is an error message" }
256
- ```