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,160 @@
|
|
1
|
+
# lib/familia/features/relationships/participation/participant_methods.rb
|
2
|
+
|
3
|
+
require_relative '../collection_operations'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
module Relationships
|
8
|
+
# Methods added to PARTICIPANT classes (the ones calling participates_in)
|
9
|
+
# These methods allow participant instances to manage their membership in target collections
|
10
|
+
#
|
11
|
+
# Example: When Domain calls `participates_in Customer, :domains`
|
12
|
+
# Domain instances get methods to check/manage their presence in Customer collections
|
13
|
+
module ParticipantMethods
|
14
|
+
using Familia::Refinements::StylizeWords
|
15
|
+
extend CollectionOperations
|
16
|
+
|
17
|
+
# Visual Guide for methods added to PARTICIPANT instances:
|
18
|
+
# =========================================================
|
19
|
+
# When Domain calls: participates_in Customer, :domains
|
20
|
+
#
|
21
|
+
# Domain instances (PARTICIPANT) get these methods:
|
22
|
+
# ├── in_customer_domains?(customer) # Check if I'm in this customer's domains
|
23
|
+
# ├── add_to_customer_domains(customer, score) # Add myself to customer's domains
|
24
|
+
# ├── remove_from_customer_domains(customer) # Remove myself from customer's domains
|
25
|
+
# ├── score_in_customer_domains(customer) # Get my score (sorted_set only)
|
26
|
+
# ├── update_score_in_customer_domains(customer) # Update my score (sorted_set only)
|
27
|
+
# └── position_in_customer_domains(customer) # Get my position (list only)
|
28
|
+
|
29
|
+
module Builder
|
30
|
+
extend CollectionOperations
|
31
|
+
|
32
|
+
# Build all participant methods for a participation relationship
|
33
|
+
# @param participant_class [Class] The class receiving these methods (e.g., Domain)
|
34
|
+
# @param target_class_name [String] Name of the target class (e.g., "Customer")
|
35
|
+
# @param collection_name [Symbol] Name of the collection (e.g., :domains)
|
36
|
+
# @param type [Symbol] Collection type (:sorted_set, :set, :list)
|
37
|
+
def self.build(participant_class, target_class_name, collection_name, type)
|
38
|
+
# Convert to snake_case once for consistency (target_class_name is PascalCase)
|
39
|
+
target_name = target_class_name.to_s.snake_case
|
40
|
+
|
41
|
+
# Core participant methods
|
42
|
+
build_membership_check(participant_class, target_name, collection_name, type)
|
43
|
+
build_add_to_target(participant_class, target_name, collection_name, type)
|
44
|
+
build_remove_from_target(participant_class, target_name, collection_name, type)
|
45
|
+
|
46
|
+
# Type-specific methods
|
47
|
+
case type
|
48
|
+
when :sorted_set
|
49
|
+
build_score_methods(participant_class, target_name, collection_name)
|
50
|
+
when :list
|
51
|
+
build_position_method(participant_class, target_name, collection_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Build method to check membership in target's collection
|
56
|
+
# Creates: domain.in_customer_domains?(customer)
|
57
|
+
def self.build_membership_check(participant_class, target_name, collection_name, _type)
|
58
|
+
method_name = "in_#{target_name}_#{collection_name}?"
|
59
|
+
|
60
|
+
participant_class.define_method(method_name) do |target_instance|
|
61
|
+
return false unless target_instance&.identifier
|
62
|
+
|
63
|
+
# Use Horreum's DataType accessor instead of manual creation
|
64
|
+
collection = target_instance.send(collection_name)
|
65
|
+
ParticipantMethods::Builder.member_of_collection?(collection, self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Build method to add self to target's collection
|
70
|
+
# Creates: domain.add_to_customer_domains(customer, score)
|
71
|
+
def self.build_add_to_target(participant_class, target_name, collection_name, type)
|
72
|
+
method_name = "add_to_#{target_name}_#{collection_name}"
|
73
|
+
|
74
|
+
participant_class.define_method(method_name) do |target_instance, score = nil|
|
75
|
+
return unless target_instance&.identifier
|
76
|
+
|
77
|
+
# Use Horreum's DataType accessor instead of manual creation
|
78
|
+
collection = target_instance.send(collection_name)
|
79
|
+
|
80
|
+
# Calculate score if needed and not provided
|
81
|
+
if type == :sorted_set && score.nil?
|
82
|
+
score = calculate_participation_score(target_instance.class, collection_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Use transaction for atomicity between collection add and reverse index tracking
|
86
|
+
# All operations use Horreum's DataType methods (not direct Redis calls)
|
87
|
+
target_instance.transaction do |_tx|
|
88
|
+
# Add to collection using DataType method (ZADD/SADD/RPUSH)
|
89
|
+
ParticipantMethods::Builder.add_to_collection(
|
90
|
+
collection,
|
91
|
+
self,
|
92
|
+
score: score,
|
93
|
+
type: type,
|
94
|
+
target_class: target_instance.class,
|
95
|
+
collection_name: collection_name,
|
96
|
+
)
|
97
|
+
|
98
|
+
# Track participation for efficient cleanup using DataType method (SADD)
|
99
|
+
track_participation_in(collection.dbkey) if respond_to?(:track_participation_in)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Build method to remove self from target's collection
|
105
|
+
# Creates: domain.remove_from_customer_domains(customer)
|
106
|
+
def self.build_remove_from_target(participant_class, target_name, collection_name, type)
|
107
|
+
method_name = "remove_from_#{target_name}_#{collection_name}"
|
108
|
+
|
109
|
+
participant_class.define_method(method_name) do |target_instance|
|
110
|
+
return unless target_instance&.identifier
|
111
|
+
|
112
|
+
# Use Horreum's DataType accessor instead of manual creation
|
113
|
+
collection = target_instance.send(collection_name)
|
114
|
+
|
115
|
+
# Use transaction for atomicity between collection remove and reverse index untracking
|
116
|
+
# All operations use Horreum's DataType methods (not direct Redis calls)
|
117
|
+
target_instance.transaction do |_tx|
|
118
|
+
# Remove from collection using DataType method (ZREM/SREM/LREM)
|
119
|
+
ParticipantMethods::Builder.remove_from_collection(collection, self, type: type)
|
120
|
+
|
121
|
+
# Remove from participation tracking using DataType method (SREM)
|
122
|
+
untrack_participation_in(collection.dbkey) if respond_to?(:untrack_participation_in)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Build score-related methods for sorted sets
|
128
|
+
# Creates: domain.score_in_customer_domains(customer)
|
129
|
+
# domain.update_score_in_customer_domains(customer, new_score)
|
130
|
+
def self.build_score_methods(participant_class, target_name, collection_name)
|
131
|
+
# Get score method
|
132
|
+
score_method = "score_in_#{target_name}_#{collection_name}"
|
133
|
+
participant_class.define_method(score_method) do |target_instance|
|
134
|
+
return nil unless target_instance&.identifier
|
135
|
+
|
136
|
+
# Use Horreum's DataType accessor instead of manual key construction
|
137
|
+
collection = target_instance.send(collection_name)
|
138
|
+
collection.score(identifier)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Build position method for lists
|
143
|
+
# Creates: domain.position_in_customer_domains(customer)
|
144
|
+
def self.build_position_method(participant_class, target_name, collection_name)
|
145
|
+
method_name = "position_in_#{target_name}_#{collection_name}"
|
146
|
+
|
147
|
+
participant_class.define_method(method_name) do |target_instance|
|
148
|
+
return nil unless target_instance&.identifier
|
149
|
+
|
150
|
+
# Use Horreum's DataType accessor instead of manual key construction
|
151
|
+
collection = target_instance.send(collection_name)
|
152
|
+
# Use DataType method to find position (index in list)
|
153
|
+
collection.to_a.index(identifier)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# lib/familia/features/relationships/participation/target_methods.rb
|
2
|
+
|
3
|
+
require_relative '../collection_operations'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
module Relationships
|
8
|
+
# Methods added to TARGET classes (the ones specified in participates_in)
|
9
|
+
# These methods allow target instances to manage their collections of participants
|
10
|
+
#
|
11
|
+
# Example: When Domain calls `participates_in Customer, :domains`
|
12
|
+
# Customer instances get methods to manage their domains collection
|
13
|
+
module TargetMethods
|
14
|
+
using Familia::Refinements::StylizeWords
|
15
|
+
extend CollectionOperations
|
16
|
+
|
17
|
+
# Visual Guide for methods added to TARGET instances:
|
18
|
+
# ====================================================
|
19
|
+
# When Domain calls: participates_in Customer, :domains
|
20
|
+
#
|
21
|
+
# Customer instances (TARGET) get these methods:
|
22
|
+
# ├── domains # Get the domains collection
|
23
|
+
# ├── add_domain(domain, score) # Add a domain to my collection
|
24
|
+
# ├── remove_domain(domain) # Remove a domain from my collection
|
25
|
+
# ├── add_domains([...]) # Bulk add domains
|
26
|
+
# └── domains_with_permission(level) # Query with score filtering (sorted_set only)
|
27
|
+
|
28
|
+
module Builder
|
29
|
+
extend CollectionOperations
|
30
|
+
|
31
|
+
# Build all target methods for a participation relationship
|
32
|
+
# @param target_class [Class] The class receiving these methods (e.g., Customer)
|
33
|
+
# @param collection_name [Symbol] Name of the collection (e.g., :domains)
|
34
|
+
# @param type [Symbol] Collection type (:sorted_set, :set, :list)
|
35
|
+
def self.build(target_class, collection_name, type)
|
36
|
+
# FIRST: Ensure the DataType field is defined on the target class
|
37
|
+
TargetMethods::Builder.ensure_collection_field(target_class, collection_name, type)
|
38
|
+
|
39
|
+
# Core target methods
|
40
|
+
build_collection_getter(target_class, collection_name, type)
|
41
|
+
build_add_item(target_class, collection_name, type)
|
42
|
+
build_remove_item(target_class, collection_name, type)
|
43
|
+
build_bulk_add(target_class, collection_name, type)
|
44
|
+
|
45
|
+
# Type-specific methods
|
46
|
+
return unless type == :sorted_set
|
47
|
+
|
48
|
+
build_permission_query(target_class, collection_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Build class-level collection methods (for class_participates_in)
|
52
|
+
# @param target_class [Class] The class receiving these methods
|
53
|
+
# @param collection_name [Symbol] Name of the collection
|
54
|
+
# @param type [Symbol] Collection type
|
55
|
+
def self.build_class_level(target_class, collection_name, type)
|
56
|
+
# FIRST: Ensure the class-level DataType field is defined
|
57
|
+
target_class.send("class_#{type}", collection_name)
|
58
|
+
|
59
|
+
# Class-level collection getter (e.g., User.all_users)
|
60
|
+
build_class_collection_getter(target_class, collection_name, type)
|
61
|
+
build_class_add_method(target_class, collection_name, type)
|
62
|
+
build_class_remove_method(target_class, collection_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Build method to get the collection
|
66
|
+
# Creates: customer.domains
|
67
|
+
def self.build_collection_getter(target_class, collection_name, type)
|
68
|
+
# No need to define the method - Horreum automatically creates it
|
69
|
+
# when we call ensure_collection_field above. This method is
|
70
|
+
# kept for backwards compatibility but now does nothing.
|
71
|
+
# The field definition (sorted_set :domains) creates the accessor automatically.
|
72
|
+
end
|
73
|
+
|
74
|
+
# Build method to add an item to the collection
|
75
|
+
# Creates: customer.add_domain(domain, score)
|
76
|
+
def self.build_add_item(target_class, collection_name, type)
|
77
|
+
singular_name = collection_name.to_s.singularize
|
78
|
+
method_name = "add_#{singular_name}"
|
79
|
+
|
80
|
+
target_class.define_method(method_name) do |item, score = nil|
|
81
|
+
collection = send(collection_name)
|
82
|
+
|
83
|
+
# Calculate score if needed and not provided
|
84
|
+
if type == :sorted_set && score.nil? && item.respond_to?(:calculate_participation_score)
|
85
|
+
score = item.calculate_participation_score(self.class, collection_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Use transaction for atomicity between collection add and reverse index tracking
|
89
|
+
# All operations use Horreum's DataType methods (not direct Redis calls)
|
90
|
+
transaction do |_tx|
|
91
|
+
# Add to collection using DataType method (ZADD/SADD/RPUSH)
|
92
|
+
TargetMethods::Builder.add_to_collection(
|
93
|
+
collection,
|
94
|
+
item,
|
95
|
+
score: score,
|
96
|
+
type: type,
|
97
|
+
target_class: self.class,
|
98
|
+
collection_name: collection_name,
|
99
|
+
)
|
100
|
+
|
101
|
+
# Track participation in reverse index using DataType method (SADD)
|
102
|
+
item.track_participation_in(collection.dbkey) if item.respond_to?(:track_participation_in)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Build method to remove an item from the collection
|
108
|
+
# Creates: customer.remove_domain(domain)
|
109
|
+
def self.build_remove_item(target_class, collection_name, type)
|
110
|
+
singular_name = collection_name.to_s.singularize
|
111
|
+
method_name = "remove_#{singular_name}"
|
112
|
+
|
113
|
+
target_class.define_method(method_name) do |item|
|
114
|
+
collection = send(collection_name)
|
115
|
+
|
116
|
+
# Use transaction for atomicity between collection remove and reverse index untracking
|
117
|
+
# All operations use Horreum's DataType methods (not direct Redis calls)
|
118
|
+
transaction do |_tx|
|
119
|
+
# Remove from collection using DataType method (ZREM/SREM/LREM)
|
120
|
+
TargetMethods::Builder.remove_from_collection(collection, item, type: type)
|
121
|
+
|
122
|
+
# Remove from participation tracking using DataType method (SREM)
|
123
|
+
item.untrack_participation_in(collection.dbkey) if item.respond_to?(:untrack_participation_in)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Build method for bulk adding items
|
129
|
+
# Creates: customer.add_domains([domain1, domain2, ...])
|
130
|
+
def self.build_bulk_add(target_class, collection_name, type)
|
131
|
+
method_name = "add_#{collection_name}"
|
132
|
+
|
133
|
+
target_class.define_method(method_name) do |items|
|
134
|
+
return if items.empty?
|
135
|
+
|
136
|
+
collection = send(collection_name)
|
137
|
+
|
138
|
+
# Use transaction for atomicity across all bulk additions and reverse index tracking
|
139
|
+
# All operations use Horreum's DataType methods (not direct Redis calls)
|
140
|
+
transaction do |_tx|
|
141
|
+
# Bulk add to collection using DataType methods (multiple ZADD/SADD/RPUSH)
|
142
|
+
TargetMethods::Builder.bulk_add_to_collection(collection, items, type: type, target_class: self.class,
|
143
|
+
collection_name: collection_name)
|
144
|
+
|
145
|
+
# Track all participations using DataType methods (multiple SADD)
|
146
|
+
items.each do |item|
|
147
|
+
item.track_participation_in(collection.dbkey) if item.respond_to?(:track_participation_in)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Build permission query for sorted sets
|
154
|
+
# Creates: customer.domains_with_permission(min_level)
|
155
|
+
def self.build_permission_query(target_class, collection_name)
|
156
|
+
method_name = "#{collection_name}_with_permission"
|
157
|
+
|
158
|
+
target_class.define_method(method_name) do |min_permission = :read|
|
159
|
+
collection = send(collection_name)
|
160
|
+
|
161
|
+
# Assumes ScoreEncoding module is available
|
162
|
+
if defined?(ScoreEncoding)
|
163
|
+
permission_score = ScoreEncoding.permission_encode(0, min_permission)
|
164
|
+
collection.zrangebyscore(permission_score, '+inf', with_scores: true)
|
165
|
+
else
|
166
|
+
# Fallback to all members if ScoreEncoding not available
|
167
|
+
collection.members(with_scores: true)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Build class-level collection getter
|
173
|
+
# Creates: User.all_users (class method)
|
174
|
+
def self.build_class_collection_getter(target_class, collection_name, type)
|
175
|
+
# No need to define the method - Horreum automatically creates it
|
176
|
+
# when we call class_#{type} above. This method is kept for
|
177
|
+
# backwards compatibility but now does nothing.
|
178
|
+
# The field definition (class_sorted_set :all_users) creates the accessor automatically.
|
179
|
+
end
|
180
|
+
|
181
|
+
# Build class-level add method
|
182
|
+
# Creates: User.add_to_all_users(user, score)
|
183
|
+
def self.build_class_add_method(target_class, collection_name, type)
|
184
|
+
method_name = "add_to_#{collection_name}"
|
185
|
+
|
186
|
+
target_class.define_singleton_method(method_name) do |item, score = nil|
|
187
|
+
collection = send(collection_name.to_s)
|
188
|
+
|
189
|
+
# Calculate score if needed
|
190
|
+
if type == :sorted_set && score.nil?
|
191
|
+
score = if item.respond_to?(:calculate_participation_score)
|
192
|
+
item.calculate_participation_score('class', collection_name)
|
193
|
+
elsif item.respond_to?(:current_score)
|
194
|
+
item.current_score
|
195
|
+
else
|
196
|
+
Familia.now.to_f
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
TargetMethods::Builder.add_to_collection(
|
201
|
+
collection,
|
202
|
+
item,
|
203
|
+
score: score,
|
204
|
+
type: type,
|
205
|
+
target_class: self.class,
|
206
|
+
collection_name: collection_name,
|
207
|
+
)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Build class-level remove method
|
212
|
+
# Creates: User.remove_from_all_users(user)
|
213
|
+
def self.build_class_remove_method(target_class, collection_name)
|
214
|
+
method_name = "remove_from_#{collection_name}"
|
215
|
+
|
216
|
+
target_class.define_singleton_method(method_name) do |item|
|
217
|
+
collection = send(collection_name.to_s)
|
218
|
+
TargetMethods::Builder.remove_from_collection(collection, item)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|