Module: Familia::Features::Relationships::Participation::ModelInstanceMethods
- Defined in:
- lib/familia/features/relationships/participation.rb
Overview
Instance methods available on objects that participate in collections.
These methods provide the core functionality for participation management, including score calculation, membership tracking, and participation queries.
Instance Method Summary collapse
-
#calculate_participation_score(target_class, collection_name) ⇒ Float
Calculate the appropriate score for a participation relationship based on configured scoring strategy.
- #current_participations ⇒ Object
-
#participating_ids_for_target(target_class, collection_names = nil) ⇒ Array<Hash>, Array<String>
Get comprehensive information about all collections this object participates in.
-
#participating_in_target?(target_class, collection_names = nil) ⇒ Boolean
Check if this instance participates in any target of a specific class.
-
#track_participation_in(collection_key) ⇒ Object
Add participation tracking to the reverse index.
-
#untrack_participation_in(collection_key) ⇒ Object
Remove participation tracking from the reverse index.
Instance Method Details
#calculate_participation_score(target_class, collection_name) ⇒ Float
Calculate the appropriate score for a participation relationship based on configured scoring strategy.
This method serves as the single source of truth for participation scoring across the entire relationship lifecycle. It supports multiple scoring strategies and provides robust fallback behavior for edge cases and error conditions.
The calculated score determines the object's position within sorted collections and can be dynamically recalculated as object state changes, enabling responsive collection ordering based on real-time business logic.
=== Scoring Strategies
[Symbol] Field name or method name - calls +send(symbol)+ on the instance
- +:priority_level+ - Uses value of priority_level field
- +:created_at+ - Uses timestamp for chronological ordering
- +:calculate_importance+ - Calls custom method for complex logic
[Proc] Dynamic calculation executed in instance context using +instance_exec+
- +-> { skill_level * experience_years }+ - Combines multiple fields
- +-> { active? ? 100 : 0 }+ - Conditional scoring based on state
- +-> { Rails.cache.fetch("score:#id") { expensive_calculation } }+ - Cached computations
[Numeric] Static score applied uniformly to all instances
- +50.0+ - All instances get same floating-point score
- +100+ - All instances get same integer score (converted to float)
[nil] Uses +current_score+ method as fallback if available
=== Performance Considerations
- Score calculations are performed on-demand during collection operations
- Proc-based calculations should be efficient as they may be called frequently
- Consider caching expensive calculations within the Proc itself
- Static numeric scores have no performance overhead
=== Thread Safety
Score calculations should be idempotent and thread-safe since they may be called concurrently during collection updates. Avoid modifying instance state within scoring Procs.
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/familia/features/relationships/participation.rb', line 458 def calculate_participation_score(target_class, collection_name) # Find the participation configuration using the new matches? method participation_config = self.class.participation_relationships.find do |details| details.matches?(target_class, collection_name) end return current_score unless participation_config score_calculator = participation_config.score # Get the raw result based on calculator type result = case score_calculator when Symbol # Field name or method name respond_to?(score_calculator) ? send(score_calculator) : nil when Proc # Execute proc in context of this instance instance_exec(&score_calculator) when Numeric # Static numeric value return score_calculator.to_f else # Unrecognized type return current_score end # Convert result to appropriate score with unified logic convert_to_score(result) end |
#current_participations ⇒ Object
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 |
# File 'lib/familia/features/relationships/participation.rb', line 653 def current_participations return [] unless self.class.respond_to?(:participation_relationships) # Use the reverse index as the single source of truth collection_keys = participations.members return [] if collection_keys.empty? memberships = [] # Check membership in each tracked collection using DataType methods collection_keys.each do |collection_key| # Parse the collection key to extract target info # Expected format: "targetclass:targetid:collectionname" target_class_config, target_id, collection_name_from_key = collection_key.split(Familia.delim, 3) next unless target_class_config && target_id && collection_name_from_key # Find the matching participation configuration # Note: target_class_config from key uses prefix (may differ from config_name) config = self.class.participation_relationships.find do |cfg| cfg.target_class.prefix.to_s == target_class_config && cfg.collection_name.to_s == collection_name_from_key end next unless config # Find the target instance and check membership using Horreum DataTypes # config.target_class is already a resolved Class object begin target_instance = config.target_class.find_by_id(target_id) next unless target_instance # Use Horreum's DataType accessor to get the collection collection = target_instance.send(config.collection_name) # Check membership using DataType methods and build ParticipationMembership score = nil decoded_score = nil position = nil case config.type when :sorted_set # Pass self (Familia object) not identifier (string) for correct serialization # See GitHub issue #212: serialize_value handles objects vs strings differently score = collection.score(self) next unless score decoded_score = decode_score(score) if respond_to?(:decode_score) when :set # Pass self (Familia object) not identifier (string) for correct serialization is_member = collection.member?(self) next unless is_member when :list # List uses to_a which returns deserialized values, so identifier is correct here position = collection.to_a.index(identifier) next unless position end # Create ParticipationMembership instance # Use target_class_base to get clean class name without namespace membership = ParticipationMembership.new( target_class: config.target_class_base, target_id: target_id, collection_name: config.collection_name, type: config.type, score: score, decoded_score: decoded_score, position: position ) memberships << membership rescue StandardError => e Familia.debug "[#{collection_key}] Error checking membership: #{e.}" next end end memberships end |
#participating_ids_for_target(target_class, collection_names = nil) ⇒ Array<Hash>, Array<String>
Get comprehensive information about all collections this object participates in.
This method leverages the reverse index to efficiently retrieve membership details across all collections without requiring expensive scans. For each membership, it provides collection metadata, membership details, and type-specific information like scores or positions.
The method handles missing target objects gracefully and validates membership using the actual DataType collections to ensure accuracy.
=== Return Format
Returns an array of hashes, each containing:
- +:target_class+ - Name of the class owning the collection
- +:target_id+ - Identifier of the specific target instance
- +:collection_name+ - Name of the collection within the target
- +:type+ - Collection type (:sorted_set, :set, :list)
Additional fields based on collection type:
- +:score+ - Current score (sorted_set only)
- +:decoded_score+ - Human-readable score if decode_score method exists
- +:position+ - Zero-based position in the list (list only)
Get all IDs where this instance participates for a specific target class
This is a shallow check - it extracts IDs from the participation index without verifying that the target Redis keys actually exist. Use this for fast ID enumeration; use *_instances methods if you need existence verification.
Optimized to iterate through keys once and use Set for efficient uniqueness, reducing string operations and object allocations.
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 |
# File 'lib/familia/features/relationships/participation.rb', line 602 def participating_ids_for_target(target_class, collection_names = nil) # Use centralized key_prefix method for consistent key generation target_prefix = target_class.key_prefix ids = Set.new participations.members.each do |key| next unless key.start_with?(target_prefix) parts = key.split(Familia.delim, 3) # Split into ["targetclass", "id", "collection"] id = parts[1] # If filtering by collection names, check before adding if collection_names && !collection_names.empty? collection = parts[2] ids << id if collection_names.include?(collection) else ids << id end end ids.to_a end |
#participating_in_target?(target_class, collection_names = nil) ⇒ Boolean
Check if this instance participates in any target of a specific class
This is a shallow check - it only verifies that participation entries exist in the participation index. It does NOT verify that the target Redis keys actually exist. Use this for fast membership checks.
Optimized to stop scanning as soon as a match is found.
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 |
# File 'lib/familia/features/relationships/participation.rb', line 636 def participating_in_target?(target_class, collection_names = nil) # Use centralized key_prefix method for consistent key generation target_prefix = target_class.key_prefix participations.members.any? do |key| next false unless key.start_with?(target_prefix) # If filtering by specific collections, check the collection name if collection_names && !collection_names.empty? collection = key.split(Familia.delim, 3)[2] collection_names.include?(collection) else true end end end |
#track_participation_in(collection_key) ⇒ Object
Add participation tracking to the reverse index.
This method maintains the reverse index that tracks which collections this object participates in. The reverse index enables efficient lookup of all memberships via +current_participations+ without requiring expensive scans.
The collection key follows the pattern: +"targetclass:targetid:collectionname"+
502 503 504 505 |
# File 'lib/familia/features/relationships/participation.rb', line 502 def track_participation_in(collection_key) # Use Horreum's DataType field instead of manual key construction participations.add(collection_key) end |
#untrack_participation_in(collection_key) ⇒ Object
Remove participation tracking from the reverse index.
This method removes the collection key from the reverse index when the object is removed from a collection. This keeps the reverse index accurate and prevents stale references from appearing in +current_participations+ results.
519 520 521 522 |
# File 'lib/familia/features/relationships/participation.rb', line 519 def untrack_participation_in(collection_key) # Use Horreum's DataType field instead of manual key construction participations.remove(collection_key) end |