familia 2.0.0.pre15 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
data/lib/familia/horreum.rb
CHANGED
@@ -7,25 +7,41 @@ require_relative 'horreum/core'
|
|
7
7
|
|
8
8
|
module Familia
|
9
9
|
#
|
10
|
-
# Horreum: A
|
10
|
+
# Horreum: A Valkey/Redis-backed ORM base class providing field definitions and DataType relationships
|
11
11
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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
|
-
#
|
19
|
-
# *
|
20
|
-
# *
|
21
|
-
#
|
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
|
32
|
+
# class User < Familia::Horreum
|
33
|
+
# identifier_field :email
|
25
34
|
# field :name
|
26
|
-
# field :
|
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
|
-
#
|
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
|
-
#
|
42
|
-
#
|
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
|
-
#
|
46
|
-
# *
|
47
|
-
# *
|
48
|
-
# *
|
49
|
-
# *
|
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
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
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
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
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 :
|
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"
|
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)
|
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,
|
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
|
112
|
-
#
|
113
|
-
#
|
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
|
116
|
-
#
|
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
|
-
#
|
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
|
-
|
131
|
-
Familia.trace :INITIALIZE,
|
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,
|
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
|
-
|
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
|
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
|
-
|
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
|
-
#
|
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
|
-
# @
|
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
|
-
|
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
|
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
|
data/lib/familia/logging.rb
CHANGED
@@ -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 =
|
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
|
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
|
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,
|
145
|
+
def trace(label, instance_id = nil, ident = nil, extra_context = nil)
|
151
146
|
return unless Familia::Refinements::LoggerTrace::ENABLED
|
152
147
|
|
153
|
-
#
|
154
|
-
|
155
|
-
|
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
|
-
```
|