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
@@ -0,0 +1,656 @@
|
|
1
|
+
# lib/familia/features/relationships/participation.rb
|
2
|
+
|
3
|
+
require_relative 'participation_relationship'
|
4
|
+
require_relative 'collection_operations'
|
5
|
+
require_relative 'participation/participant_methods'
|
6
|
+
require_relative 'participation/target_methods'
|
7
|
+
|
8
|
+
module Familia
|
9
|
+
module Features
|
10
|
+
module Relationships
|
11
|
+
# Participation module for bidirectional business relationships using Valkey/Redis collections.
|
12
|
+
# Provides semantic, scored relationships with automatic reverse tracking.
|
13
|
+
#
|
14
|
+
# Unlike Indexing (which is for attribute lookups), Participation manages
|
15
|
+
# relationships where membership has meaning, scores have semantic value,
|
16
|
+
# and bidirectional tracking is essential
|
17
|
+
#
|
18
|
+
# === Architecture Overview ===
|
19
|
+
# This module is organized into clear, separate concerns:
|
20
|
+
#
|
21
|
+
# 1. CollectionOperations: Shared helpers for all collection manipulation
|
22
|
+
# 2. ParticipantMethods: Methods added to the class calling participates_in
|
23
|
+
# 3. TargetMethods: Methods added to the target class specified in participates_in
|
24
|
+
#
|
25
|
+
# This separation makes it crystal clear what methods are added to which class.
|
26
|
+
#
|
27
|
+
# @example Basic participation with temporal scoring
|
28
|
+
# class Domain < Familia::Horreum
|
29
|
+
# feature :relationships
|
30
|
+
# field :created_at
|
31
|
+
# participates_in Customer, :domains, score: :created_at
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # TARGET (Customer) gets collection management:
|
35
|
+
# customer.domains # → Familia::SortedSet (by created_at)
|
36
|
+
# customer.add_domain(domain) # → adds with created_at score
|
37
|
+
# customer.remove_domain(domain) # → removes + cleans reverse index
|
38
|
+
# customer.add_domains([d1, d2, d3]) # → efficient bulk addition
|
39
|
+
#
|
40
|
+
# # PARTICIPANT (Domain) gets membership methods:
|
41
|
+
# domain.in_customer_domains?(customer) # → true/false
|
42
|
+
# domain.add_to_customer_domains(customer) # → self-addition
|
43
|
+
# domain.remove_from_customer_domains(customer) # → self-removal
|
44
|
+
# domain.participations # → reverse index tracking
|
45
|
+
#
|
46
|
+
# @example Class-level participation (all instances auto-tracked)
|
47
|
+
# class User < Familia::Horreum
|
48
|
+
# feature :relationships
|
49
|
+
# field :created_at
|
50
|
+
# class_participates_in :all_users, score: :created_at
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# User.all_users # → Familia::SortedSet (class-level)
|
54
|
+
# user.in_class_all_users? # → true if auto-added
|
55
|
+
# user.add_to_class_all_users # → explicit addition
|
56
|
+
#
|
57
|
+
# @example Semantic scores with permission encoding
|
58
|
+
# class Domain < Familia::Horreum
|
59
|
+
# feature :relationships
|
60
|
+
# field :created_at
|
61
|
+
# field :permission_bits
|
62
|
+
#
|
63
|
+
# participates_in Customer, :domains,
|
64
|
+
# score: -> { permission_encode(created_at, permission_bits) }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# customer.domains_with_permission(:read) # → filtered by score
|
68
|
+
#
|
69
|
+
# Key Differences from Indexing:
|
70
|
+
# - Participation: Bidirectional relationships with semantic scores
|
71
|
+
# - Indexing: Unidirectional lookups without relationship semantics
|
72
|
+
# - Participation: Collection name in key (customer:123:domains)
|
73
|
+
# - Indexing: Field value in key (company:123:dept_index:engineering)
|
74
|
+
#
|
75
|
+
# When to Use Participation:
|
76
|
+
# - Modeling business relationships (Customer owns Domains)
|
77
|
+
# - Scores have meaning (priority, permissions, join_date)
|
78
|
+
# - Need bidirectional tracking ("what collections does this belong to?")
|
79
|
+
# - Relationship lifecycle matters (cascade cleanup, reverse tracking)
|
80
|
+
#
|
81
|
+
module Participation
|
82
|
+
using Familia::Refinements::StylizeWords
|
83
|
+
|
84
|
+
# Hook called when module is included in a class.
|
85
|
+
#
|
86
|
+
# Extends the host class with ModelClassMethods for relationship definitions
|
87
|
+
# and includes ModelInstanceMethods for instance-level operations.
|
88
|
+
#
|
89
|
+
# @param base [Class] The class including this module
|
90
|
+
def self.included(base)
|
91
|
+
base.extend ModelClassMethods
|
92
|
+
base.include ModelInstanceMethods
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
# Class methods for defining participation relationships.
|
97
|
+
#
|
98
|
+
# These methods are available on any class that includes the Participation module,
|
99
|
+
# allowing definition of both instance-level and class-level participation relationships.
|
100
|
+
module ModelClassMethods
|
101
|
+
# Define a class-level participation collection where all instances automatically participate.
|
102
|
+
#
|
103
|
+
# Class-level participation creates a global collection containing all instances of the class,
|
104
|
+
# with automatic management of membership based on object lifecycle events. This is useful
|
105
|
+
# for maintaining global indexes, leaderboards, or categorical groupings.
|
106
|
+
#
|
107
|
+
# The collection is created at the class level (e.g., User.all_users) rather than on
|
108
|
+
# individual instances, providing a centralized view of all objects matching the criteria.
|
109
|
+
#
|
110
|
+
# === Generated Methods
|
111
|
+
#
|
112
|
+
# ==== On the Class (Target Methods)
|
113
|
+
# - +ClassName.collection_name+ - Access the collection DataType
|
114
|
+
# - +ClassName.add_to_collection_name(instance)+ - Add instance to collection
|
115
|
+
# - +ClassName.remove_from_collection_name(instance)+ - Remove instance from collection
|
116
|
+
#
|
117
|
+
# ==== On Instances (Participant Methods, if bidirectional)
|
118
|
+
# - +instance.in_class_collection_name?+ - Check membership in class collection
|
119
|
+
# - +instance.add_to_class_collection_name+ - Add self to class collection
|
120
|
+
# - +instance.remove_from_class_collection_name+ - Remove self from class collection
|
121
|
+
#
|
122
|
+
# @param collection_name [Symbol] Name of the class-level collection (e.g., +:all_users+, +:active_members+)
|
123
|
+
# @param score [Symbol, Proc, Numeric, nil] Scoring strategy for sorted collections:
|
124
|
+
# - +Symbol+: Field name or method name (e.g., +:priority_level+, +:created_at+)
|
125
|
+
# - +Proc+: Dynamic calculation in instance context (e.g., +-> { status == 'premium' ? 100 : 0 }+)
|
126
|
+
# - +Numeric+: Static score for all instances (e.g., +50.0+)
|
127
|
+
# - +nil+: Use +current_score+ method fallback
|
128
|
+
# - +:remove+: Remove from collection on destruction (default)
|
129
|
+
# - +:ignore+: Leave in collection when destroyed
|
130
|
+
# @param type [Symbol] Valkey/Redis collection type:
|
131
|
+
# - +:sorted_set+: Ordered by score (default)
|
132
|
+
# - +:set+: Unordered unique membership
|
133
|
+
# - +:list+: Ordered sequence allowing duplicates
|
134
|
+
# @param bidirectional [Boolean] Whether to generate convenience methods on instances (default: +true+)
|
135
|
+
#
|
136
|
+
# @example Simple priority-based global collection
|
137
|
+
# class User < Familia::Horreum
|
138
|
+
# field :priority_level
|
139
|
+
# class_participates_in :all_users, score: :priority_level
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# User.all_users.first # Highest priority user
|
143
|
+
# user.in_class_all_users? # true if user is in collection
|
144
|
+
#
|
145
|
+
# @example Dynamic scoring based on status
|
146
|
+
# class Customer < Familia::Horreum
|
147
|
+
# field :status
|
148
|
+
# field :last_purchase
|
149
|
+
#
|
150
|
+
# class_participates_in :active_customers, score: -> {
|
151
|
+
# status == 'active' ? last_purchase.to_i : 0
|
152
|
+
# }
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# Customer.active_customers.to_a # All active customers, sorted by last purchase
|
156
|
+
#
|
157
|
+
# @see #participates_in for instance-level participation relationships
|
158
|
+
# @since 1.0.0
|
159
|
+
def class_participates_in(collection_name, score: nil,
|
160
|
+
type: :sorted_set, bidirectional: true)
|
161
|
+
# Store metadata for this participation relationship
|
162
|
+
participation_relationships << ParticipationRelationship.new(
|
163
|
+
target_class: self,
|
164
|
+
collection_name: collection_name,
|
165
|
+
score: score,
|
166
|
+
|
167
|
+
type: type,
|
168
|
+
bidirectional: bidirectional,
|
169
|
+
)
|
170
|
+
|
171
|
+
# STEP 1: Add collection management methods to the class itself
|
172
|
+
# e.g., User.all_users, User.add_to_all_users(user)
|
173
|
+
TargetMethods::Builder.build_class_level(self, collection_name, type)
|
174
|
+
|
175
|
+
# STEP 2: Add participation methods to instances (if bidirectional)
|
176
|
+
# e.g., user.in_class_all_users?, user.add_to_class_all_users
|
177
|
+
return unless bidirectional
|
178
|
+
|
179
|
+
ParticipantMethods::Builder.build(self, 'class', collection_name, type)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Define an instance-level participation relationship between two classes.
|
183
|
+
#
|
184
|
+
# This method creates a bidirectional relationship where instances of the calling class
|
185
|
+
# (participants) can join collections owned by instances of the target class. This enables
|
186
|
+
# flexible multi-membership scenarios where objects can belong to multiple collections
|
187
|
+
# simultaneously with different scoring and management strategies.
|
188
|
+
#
|
189
|
+
# The relationship automatically handles reverse index tracking, allowing efficient
|
190
|
+
# lookup of all collections a participant belongs to via the +current_participations+ method.
|
191
|
+
#
|
192
|
+
# === Generated Methods
|
193
|
+
#
|
194
|
+
# ==== On Target Class (Collection Owner)
|
195
|
+
# - +target.collection_name+ - Access the collection DataType
|
196
|
+
# - +target.add_participant_class_name(participant)+ - Add participant to collection
|
197
|
+
# - +target.remove_participant_class_name(participant)+ - Remove participant from collection
|
198
|
+
# - +target.add_participant_class_names([participants])+ - Bulk add multiple participants
|
199
|
+
#
|
200
|
+
# ==== On Participant Class (if bidirectional)
|
201
|
+
# - +participant.in_target_collection_name?(target)+ - Check membership in target's collection
|
202
|
+
# - +participant.add_to_target_collection_name(target)+ - Add self to target's collection
|
203
|
+
# - +participant.remove_from_target_collection_name(target)+ - Remove self from target's collection
|
204
|
+
#
|
205
|
+
# === Reverse Index Tracking
|
206
|
+
#
|
207
|
+
# Automatically creates a +:participations+ set field on the participant class to track
|
208
|
+
# all collections the instance belongs to. This enables efficient membership queries
|
209
|
+
# and cleanup operations without scanning all possible collections.
|
210
|
+
#
|
211
|
+
# @param target_class [Class, Symbol, String] The class that owns the collection. Can be:
|
212
|
+
# - +Class+ object (e.g., +Customer+)
|
213
|
+
# - +Symbol+ referencing class name (e.g., +:customer+, +:Customer+)
|
214
|
+
# - +String+ class name (e.g., +"Customer"+)
|
215
|
+
# @param collection_name [Symbol] Name of the collection on the target class (e.g., +:domains+, +:members+)
|
216
|
+
# @param score [Symbol, Proc, Numeric, nil] Scoring strategy for sorted collections:
|
217
|
+
# - +Symbol+: Field name or method name (e.g., +:priority+, +:created_at+)
|
218
|
+
# - +Proc+: Dynamic calculation executed in participant instance context
|
219
|
+
# - +Numeric+: Static score applied to all participants
|
220
|
+
# - +nil+: Use +current_score+ method as fallback
|
221
|
+
# - +:remove+: Remove from all collections on destruction (default)
|
222
|
+
# - +:ignore+: Leave in collections when destroyed
|
223
|
+
# @param type [Symbol] Valkey/Redis collection type:
|
224
|
+
# - +:sorted_set+: Ordered by score, allows duplicates with different scores (default)
|
225
|
+
# - +:set+: Unordered unique membership
|
226
|
+
# - +:list+: Ordered sequence, allows duplicates
|
227
|
+
# @param bidirectional [Boolean] Whether to generate convenience methods on participant class (default: +true+)
|
228
|
+
#
|
229
|
+
# @example Basic domain-customer relationship
|
230
|
+
# class Domain < Familia::Horreum
|
231
|
+
# field :name
|
232
|
+
# field :created_at
|
233
|
+
#
|
234
|
+
# participates_in Customer, :domains, score: :created_at
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# # Usage:
|
238
|
+
# domain.add_to_customer_domains(customer) # Add domain to customer's collection
|
239
|
+
# customer.domains.first # Most recent domain
|
240
|
+
# domain.in_customer_domains?(customer) # true
|
241
|
+
# domain.current_participations # All collections domain belongs to
|
242
|
+
#
|
243
|
+
# @example Multi-collection participation with different types
|
244
|
+
# class Employee < Familia::Horreum
|
245
|
+
# field :hire_date
|
246
|
+
# field :skill_level
|
247
|
+
#
|
248
|
+
# # Sorted by hire date in department
|
249
|
+
# participates_in Department, :members, score: :hire_date
|
250
|
+
#
|
251
|
+
# # Simple set membership in teams
|
252
|
+
# participates_in Team, :contributors, score: :skill_level, type: :set
|
253
|
+
#
|
254
|
+
# # Complex scoring for project assignments
|
255
|
+
# participates_in Project, :assignees, score: -> {
|
256
|
+
# base_score = skill_level * 100
|
257
|
+
# seniority = (Time.now - hire_date) / 1.year
|
258
|
+
# base_score + seniority * 10
|
259
|
+
# }
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# # Employee can belong to department, multiple teams, and projects
|
263
|
+
# employee.add_to_department_members(engineering_dept)
|
264
|
+
# employee.add_to_team_contributors(frontend_team)
|
265
|
+
# employee.add_to_project_assignees(mobile_app_project)
|
266
|
+
#
|
267
|
+
# @see #class_participates_in for class-level participation
|
268
|
+
# @see ModelInstanceMethods#current_participations for membership queries
|
269
|
+
# @see ModelInstanceMethods#calculate_participation_score for scoring details
|
270
|
+
# @since 1.0.0
|
271
|
+
def participates_in(target_class, collection_name, score: nil,
|
272
|
+
type: :sorted_set, bidirectional: true)
|
273
|
+
# Handle class target using Familia.resolve_class
|
274
|
+
resolved_class = Familia.resolve_class(target_class)
|
275
|
+
|
276
|
+
# Store metadata for this participation relationship
|
277
|
+
participation_relationships << ParticipationRelationship.new(
|
278
|
+
target_class: target_class, # as passed to `participates_in`
|
279
|
+
collection_name: collection_name,
|
280
|
+
score: score,
|
281
|
+
|
282
|
+
type: type,
|
283
|
+
bidirectional: bidirectional,
|
284
|
+
)
|
285
|
+
|
286
|
+
# Resolve target class if it's a symbol/string
|
287
|
+
actual_target_class = if target_class.is_a?(Class)
|
288
|
+
target_class
|
289
|
+
else
|
290
|
+
Familia.member_by_config_name(target_class)
|
291
|
+
end
|
292
|
+
|
293
|
+
# STEP 0: Add participations tracking field to PARTICIPANT class (Domain)
|
294
|
+
# This creates the proper key: "domain:123:participations" (not "domain:123:object:participations")
|
295
|
+
set :participations unless method_defined?(:participations)
|
296
|
+
|
297
|
+
# STEP 1: Add collection management methods to TARGET class (Customer)
|
298
|
+
# Customer gets: domains, add_domain, remove_domain, etc.
|
299
|
+
TargetMethods::Builder.build(actual_target_class, collection_name, type)
|
300
|
+
|
301
|
+
# STEP 2: Add participation methods to PARTICIPANT class (Domain) - only if bidirectional
|
302
|
+
# Domain gets: in_customer_domains?, add_to_customer_domains, etc.
|
303
|
+
return unless bidirectional
|
304
|
+
|
305
|
+
ParticipantMethods::Builder.build(self, resolved_class.familia_name, collection_name, type)
|
306
|
+
end
|
307
|
+
|
308
|
+
# Get all participation relationships defined for this class.
|
309
|
+
#
|
310
|
+
# Returns an array of ParticipationRelationship objects containing metadata
|
311
|
+
# about each participation relationship, including target class, collection name,
|
312
|
+
# scoring strategy, and configuration options.
|
313
|
+
#
|
314
|
+
# @return [Array<ParticipationRelationship>] Array of relationship configurations
|
315
|
+
# @since 1.0.0
|
316
|
+
def participation_relationships
|
317
|
+
@participation_relationships ||= []
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Instance methods available on objects that participate in collections.
|
322
|
+
#
|
323
|
+
# These methods provide the core functionality for participation management,
|
324
|
+
# including score calculation, membership tracking, and participation queries.
|
325
|
+
module ModelInstanceMethods
|
326
|
+
# Calculate the appropriate score for a participation relationship based on configured scoring strategy.
|
327
|
+
#
|
328
|
+
# This method serves as the single source of truth for participation scoring across the entire
|
329
|
+
# relationship lifecycle. It supports multiple scoring strategies and provides robust fallback
|
330
|
+
# behavior for edge cases and error conditions.
|
331
|
+
#
|
332
|
+
# The calculated score determines the object's position within sorted collections and can be
|
333
|
+
# dynamically recalculated as object state changes, enabling responsive collection ordering
|
334
|
+
# based on real-time business logic.
|
335
|
+
#
|
336
|
+
# === Scoring Strategies
|
337
|
+
#
|
338
|
+
# [Symbol] Field name or method name - calls +send(symbol)+ on the instance
|
339
|
+
# * +:priority_level+ - Uses value of priority_level field
|
340
|
+
# * +:created_at+ - Uses timestamp for chronological ordering
|
341
|
+
# * +:calculate_importance+ - Calls custom method for complex logic
|
342
|
+
#
|
343
|
+
# [Proc] Dynamic calculation executed in instance context using +instance_exec+
|
344
|
+
# * +-> { skill_level * experience_years }+ - Combines multiple fields
|
345
|
+
# * +-> { active? ? 100 : 0 }+ - Conditional scoring based on state
|
346
|
+
# * +-> { Rails.cache.fetch("score:#{id}") { expensive_calculation } }+ - Cached computations
|
347
|
+
#
|
348
|
+
# [Numeric] Static score applied uniformly to all instances
|
349
|
+
# * +50.0+ - All instances get same floating-point score
|
350
|
+
# * +100+ - All instances get same integer score (converted to float)
|
351
|
+
#
|
352
|
+
# [nil] Uses +current_score+ method as fallback if available
|
353
|
+
#
|
354
|
+
# === Performance Considerations
|
355
|
+
#
|
356
|
+
# - Score calculations are performed on-demand during collection operations
|
357
|
+
# - Proc-based calculations should be efficient as they may be called frequently
|
358
|
+
# - Consider caching expensive calculations within the Proc itself
|
359
|
+
# - Static numeric scores have no performance overhead
|
360
|
+
#
|
361
|
+
# === Thread Safety
|
362
|
+
#
|
363
|
+
# Score calculations should be idempotent and thread-safe since they may be
|
364
|
+
# called concurrently during collection updates. Avoid modifying instance state
|
365
|
+
# within scoring Procs.
|
366
|
+
#
|
367
|
+
# @param target_class [Class, Symbol, String] The target class containing the collection
|
368
|
+
# @param collection_name [Symbol] The collection name within the target class
|
369
|
+
# @return [Float] Calculated score for sorted set positioning, falls back to current_score
|
370
|
+
#
|
371
|
+
# @example Field-based scoring
|
372
|
+
# class Task < Familia::Horreum
|
373
|
+
# field :priority # 1=low, 5=high
|
374
|
+
# participates_in Project, :tasks, score: :priority
|
375
|
+
# end
|
376
|
+
#
|
377
|
+
# task.priority = 5
|
378
|
+
# score = task.calculate_participation_score(Project, :tasks) # => 5.0
|
379
|
+
#
|
380
|
+
# @example Complex business logic with multiple factors
|
381
|
+
# class Employee < Familia::Horreum
|
382
|
+
# field :hire_date
|
383
|
+
# field :performance_rating
|
384
|
+
# field :salary
|
385
|
+
#
|
386
|
+
# participates_in Department, :members, score: -> {
|
387
|
+
# tenure_months = (Time.now - hire_date) / 1.month
|
388
|
+
# base_score = tenure_months * 10
|
389
|
+
# performance_bonus = performance_rating * 100
|
390
|
+
# salary_factor = salary / 1000.0
|
391
|
+
#
|
392
|
+
# (base_score + performance_bonus + salary_factor).round(2)
|
393
|
+
# }
|
394
|
+
# end
|
395
|
+
#
|
396
|
+
# # Score reflects seniority, performance, and compensation
|
397
|
+
# employee.performance_rating = 4.5
|
398
|
+
# employee.salary = 85000
|
399
|
+
# score = employee.calculate_participation_score(Department, :members) # => 1375.0
|
400
|
+
#
|
401
|
+
# @see #participates_in for relationship configuration
|
402
|
+
# @see #track_participation_in for reverse index management
|
403
|
+
# @since 1.0.0
|
404
|
+
def calculate_participation_score(target_class, collection_name)
|
405
|
+
# Find the participation configuration with robust type comparison
|
406
|
+
participation_config = self.class.participation_relationships.find do |details|
|
407
|
+
# Normalize both sides for comparison to handle Class, Symbol, and String types
|
408
|
+
config_target = details.target_class
|
409
|
+
config_target = config_target.name if config_target.is_a?(Class)
|
410
|
+
config_target = config_target.to_s
|
411
|
+
|
412
|
+
comparison_target = target_class
|
413
|
+
comparison_target = comparison_target.name if comparison_target.is_a?(Class)
|
414
|
+
comparison_target = comparison_target.to_s
|
415
|
+
|
416
|
+
config_target == comparison_target && details.collection_name == collection_name
|
417
|
+
end
|
418
|
+
|
419
|
+
return current_score unless participation_config
|
420
|
+
|
421
|
+
score_calculator = participation_config.score
|
422
|
+
|
423
|
+
# Get the raw result based on calculator type
|
424
|
+
result = case score_calculator
|
425
|
+
when Symbol
|
426
|
+
# Field name or method name
|
427
|
+
respond_to?(score_calculator) ? send(score_calculator) : nil
|
428
|
+
when Proc
|
429
|
+
# Execute proc in context of this instance
|
430
|
+
instance_exec(&score_calculator)
|
431
|
+
when Numeric
|
432
|
+
# Static numeric value
|
433
|
+
return score_calculator.to_f
|
434
|
+
else
|
435
|
+
# Unrecognized type
|
436
|
+
return current_score
|
437
|
+
end
|
438
|
+
|
439
|
+
# Convert result to appropriate score with unified logic
|
440
|
+
convert_to_score(result)
|
441
|
+
end
|
442
|
+
|
443
|
+
# Add participation tracking to the reverse index.
|
444
|
+
#
|
445
|
+
# This method maintains the reverse index that tracks which collections this object
|
446
|
+
# participates in. The reverse index enables efficient lookup of all memberships
|
447
|
+
# via +current_participations+ without requiring expensive scans.
|
448
|
+
#
|
449
|
+
# The collection key follows the pattern: +"targetclass:targetid:collectionname"+
|
450
|
+
#
|
451
|
+
# @param collection_key [String] Unique identifier for the collection (format: "class:id:collection")
|
452
|
+
# @example
|
453
|
+
# domain.track_participation_in("customer:123:domains")
|
454
|
+
# @see #untrack_participation_in for removal
|
455
|
+
# @see #current_participations for membership queries
|
456
|
+
# @since 1.0.0
|
457
|
+
def track_participation_in(collection_key)
|
458
|
+
# Use Horreum's DataType field instead of manual key construction
|
459
|
+
participations.add(collection_key)
|
460
|
+
end
|
461
|
+
|
462
|
+
# Remove participation tracking from the reverse index.
|
463
|
+
#
|
464
|
+
# This method removes the collection key from the reverse index when the object
|
465
|
+
# is removed from a collection. This keeps the reverse index accurate and prevents
|
466
|
+
# stale references from appearing in +current_participations+ results.
|
467
|
+
#
|
468
|
+
# @param collection_key [String] Collection identifier to remove from tracking
|
469
|
+
# @example
|
470
|
+
# domain.untrack_participation_in("customer:123:domains")
|
471
|
+
# @see #track_participation_in for addition
|
472
|
+
# @see #current_participations for membership queries
|
473
|
+
# @since 1.0.0
|
474
|
+
def untrack_participation_in(collection_key)
|
475
|
+
# Use Horreum's DataType field instead of manual key construction
|
476
|
+
participations.remove(collection_key)
|
477
|
+
end
|
478
|
+
|
479
|
+
# Get comprehensive information about all collections this object participates in.
|
480
|
+
#
|
481
|
+
# This method leverages the reverse index to efficiently retrieve membership details
|
482
|
+
# across all collections without requiring expensive scans. For each membership,
|
483
|
+
# it provides collection metadata, membership details, and type-specific information
|
484
|
+
# like scores or positions.
|
485
|
+
#
|
486
|
+
# The method handles missing target objects gracefully and validates membership
|
487
|
+
# using the actual DataType collections to ensure accuracy.
|
488
|
+
#
|
489
|
+
# === Return Format
|
490
|
+
#
|
491
|
+
# Returns an array of hashes, each containing:
|
492
|
+
# - +:target_class+ - Name of the class owning the collection
|
493
|
+
# - +:target_id+ - Identifier of the specific target instance
|
494
|
+
# - +:collection_name+ - Name of the collection within the target
|
495
|
+
# - +:type+ - Collection type (:sorted_set, :set, :list)
|
496
|
+
#
|
497
|
+
# Additional fields based on collection type:
|
498
|
+
# - +:score+ - Current score (sorted_set only)
|
499
|
+
# - +:decoded_score+ - Human-readable score if decode_score method exists
|
500
|
+
# - +:position+ - Zero-based position in the list (list only)
|
501
|
+
#
|
502
|
+
# @return [Array<Hash>] Array of membership details with collection metadata
|
503
|
+
#
|
504
|
+
# @example Employee participating in multiple collections
|
505
|
+
# class Employee < Familia::Horreum
|
506
|
+
# field :name
|
507
|
+
# participates_in Department, :members, score: :hire_date
|
508
|
+
# participates_in Team, :contributors, score: :skill_level, type: :set
|
509
|
+
# participates_in Project, :assignees, score: :priority, type: :list
|
510
|
+
# end
|
511
|
+
#
|
512
|
+
# employee.add_to_department_members(engineering)
|
513
|
+
# employee.add_to_team_contributors(frontend_team)
|
514
|
+
# employee.add_to_project_assignees(mobile_project)
|
515
|
+
#
|
516
|
+
# # Query all memberships
|
517
|
+
# memberships = employee.current_participations
|
518
|
+
# # => [
|
519
|
+
# # {
|
520
|
+
# # target_class: "Department",
|
521
|
+
# # target_id: "engineering",
|
522
|
+
# # collection_name: :members,
|
523
|
+
# # type: :sorted_set,
|
524
|
+
# # score: 1640995200.0,
|
525
|
+
# # decoded_score: "2022-01-01 00:00:00 UTC"
|
526
|
+
# # },
|
527
|
+
# # {
|
528
|
+
# # target_class: "Team",
|
529
|
+
# # target_id: "frontend",
|
530
|
+
# # collection_name: :contributors,
|
531
|
+
# # type: :set
|
532
|
+
# # },
|
533
|
+
# # {
|
534
|
+
# # target_class: "Project",
|
535
|
+
# # target_id: "mobile",
|
536
|
+
# # collection_name: :assignees,
|
537
|
+
# # type: :list,
|
538
|
+
# # position: 2
|
539
|
+
# # }
|
540
|
+
# # ]
|
541
|
+
#
|
542
|
+
# @see #track_participation_in for reverse index management
|
543
|
+
# @see #calculate_participation_score for scoring details
|
544
|
+
# @since 1.0.0
|
545
|
+
def current_participations
|
546
|
+
return [] unless self.class.respond_to?(:participation_relationships)
|
547
|
+
|
548
|
+
# Use the reverse index as the single source of truth
|
549
|
+
collection_keys = participations.members
|
550
|
+
return [] if collection_keys.empty?
|
551
|
+
|
552
|
+
memberships = []
|
553
|
+
|
554
|
+
# Check membership in each tracked collection using DataType methods
|
555
|
+
collection_keys.each do |collection_key|
|
556
|
+
# Parse the collection key to extract target info
|
557
|
+
# Expected format: "targetclass:targetid:collectionname"
|
558
|
+
key_parts = collection_key.split(':')
|
559
|
+
next unless key_parts.length >= 3
|
560
|
+
|
561
|
+
target_class_config = key_parts[0]
|
562
|
+
target_id = key_parts[1]
|
563
|
+
collection_name_from_key = key_parts[2]
|
564
|
+
|
565
|
+
# Find the matching participation configuration
|
566
|
+
# Note: target_class_config from key is snake_case
|
567
|
+
config = self.class.participation_relationships.find do |cfg|
|
568
|
+
cfg.target_class_config_name == target_class_config &&
|
569
|
+
cfg.collection_name.to_s == collection_name_from_key
|
570
|
+
end
|
571
|
+
|
572
|
+
next unless config
|
573
|
+
|
574
|
+
# Find the target instance and check membership using Horreum DataTypes
|
575
|
+
begin
|
576
|
+
target_class = Familia.resolve_class(config.target_class)
|
577
|
+
target_instance = target_class.find_by_id(target_id)
|
578
|
+
next unless target_instance
|
579
|
+
|
580
|
+
# Use Horreum's DataType accessor to get the collection
|
581
|
+
collection = target_instance.send(config.collection_name)
|
582
|
+
|
583
|
+
# Check membership using DataType methods
|
584
|
+
membership_data = {
|
585
|
+
target_class: config.target_class.familia_name,
|
586
|
+
target_id: target_id,
|
587
|
+
collection_name: config.collection_name,
|
588
|
+
type: config.type,
|
589
|
+
}
|
590
|
+
|
591
|
+
case config.type
|
592
|
+
when :sorted_set
|
593
|
+
score = collection.score(identifier)
|
594
|
+
next unless score
|
595
|
+
|
596
|
+
membership_data[:score] = score
|
597
|
+
membership_data[:decoded_score] = decode_score(score) if respond_to?(:decode_score)
|
598
|
+
when :set
|
599
|
+
is_member = collection.member?(identifier)
|
600
|
+
next unless is_member
|
601
|
+
when :list
|
602
|
+
position = collection.to_a.index(identifier)
|
603
|
+
next unless position
|
604
|
+
|
605
|
+
membership_data[:position] = position
|
606
|
+
end
|
607
|
+
|
608
|
+
memberships << membership_data
|
609
|
+
rescue StandardError => e
|
610
|
+
Familia.ld "[#{collection_key}] Error checking membership: #{e.message}"
|
611
|
+
next
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
memberships
|
616
|
+
end
|
617
|
+
|
618
|
+
private
|
619
|
+
|
620
|
+
# Convert a raw value to an appropriate participation score.
|
621
|
+
#
|
622
|
+
# This private method handles the final conversion step for participation scores,
|
623
|
+
# providing robust type coercion and fallback behavior for edge cases. It's called
|
624
|
+
# by +calculate_participation_score+ after the scoring strategy has produced a raw value.
|
625
|
+
#
|
626
|
+
# The method never raises exceptions, always returning a valid Float value
|
627
|
+
# suitable for use in Valkey/Redis sorted sets. Invalid or missing values
|
628
|
+
# gracefully fall back to the +current_score+ method.
|
629
|
+
#
|
630
|
+
# === Conversion Strategy
|
631
|
+
#
|
632
|
+
# [Numeric types] Convert using +to_f+ for floating-point precision
|
633
|
+
# [Integer-like] Use +encode_score+ if available, otherwise convert to float
|
634
|
+
# [nil values] Fall back to +current_score+ method
|
635
|
+
# [Other types] Fall back to +current_score+ method
|
636
|
+
#
|
637
|
+
# @param value [Object] The raw value to convert to a participation score
|
638
|
+
# @return [Float] Converted score suitable for sorted set operations
|
639
|
+
# @api private
|
640
|
+
# @since 1.0.0
|
641
|
+
def convert_to_score(value)
|
642
|
+
return current_score if value.nil?
|
643
|
+
|
644
|
+
if value.respond_to?(:to_f)
|
645
|
+
value.to_f
|
646
|
+
elsif value.respond_to?(:to_i)
|
647
|
+
encode_score(value, 0)
|
648
|
+
else
|
649
|
+
current_score
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# lib/familia/features/relationships/participation_relationship.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
#
|
7
|
+
# ParticipationRelationship
|
8
|
+
#
|
9
|
+
# Stores metadata about participation relationships defined at class level.
|
10
|
+
# Used to configure code generation and runtime behavior for participates_in
|
11
|
+
# and class_participates_in declarations.
|
12
|
+
#
|
13
|
+
ParticipationRelationship = Data.define(
|
14
|
+
:target_class, # Class object that owns the collection
|
15
|
+
:collection_name, # Symbol name of the collection (e.g., :members, :domains)
|
16
|
+
:score, # Proc/Symbol/nil - score calculator for sorted sets
|
17
|
+
:type, # Symbol - collection type (:sorted_set, :set, :list)
|
18
|
+
:bidirectional, # Boolean - whether to generate reverse methods
|
19
|
+
) do
|
20
|
+
#
|
21
|
+
# Get the normalized config name for the target class
|
22
|
+
#
|
23
|
+
# @return [String] The config name (e.g., "user", "perf_test_customer")
|
24
|
+
#
|
25
|
+
def target_class_config_name
|
26
|
+
target_class.config_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|