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

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.

Examples:

Field-based scoring

class Task < Familia::Horreum
  field :priority  # 1=low, 5=high
  participates_in Project, :tasks, score: :priority
end

task.priority = 5
score = task.calculate_participation_score(Project, :tasks)  # => 5.0

Complex business logic with multiple factors

class Employee < Familia::Horreum
  field :hire_date
  field :performance_rating
  field :salary

  participates_in Department, :members, score: -> {
    tenure_months = (Time.now - hire_date) / 1.month
    base_score = tenure_months * 10
    performance_bonus = performance_rating * 100
    salary_factor = salary / 1000.0

    (base_score + performance_bonus + salary_factor).round(2)
  }
end

# Score reflects seniority, performance, and compensation
employee.performance_rating = 4.5
employee.salary = 85000
score = employee.calculate_participation_score(Department, :members)  # => 1375.0

Parameters:

  • target_class (Class, Symbol, String)

    The target class containing the collection

  • collection_name (Symbol)

    The collection name within the target class

Returns:

  • (Float)

    Calculated score for sorted set positioning, falls back to current_score

See Also:

Since Version:

  • 1.0.0



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/familia/features/relationships/participation.rb', line 404

def calculate_participation_score(target_class, collection_name)
  # Find the participation configuration with robust type comparison
  participation_config = self.class.participation_relationships.find do |details|
    # Normalize both sides for comparison to handle Class, Symbol, and String types
    config_target = details.target_class
    config_target = config_target.name if config_target.is_a?(Class)
    config_target = config_target.to_s

    comparison_target = target_class
    comparison_target = comparison_target.name if comparison_target.is_a?(Class)
    comparison_target = comparison_target.to_s

    config_target == comparison_target && details.collection_name == 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_participationsArray<Hash>

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)

Examples:

Employee participating in multiple collections

class Employee < Familia::Horreum
  field :name
  participates_in Department, :members, score: :hire_date
  participates_in Team, :contributors, score: :skill_level, type: :set
  participates_in Project, :assignees, score: :priority, type: :list
end

employee.add_to_department_members(engineering)
employee.add_to_team_contributors(frontend_team)
employee.add_to_project_assignees(mobile_project)

# Query all memberships
memberships = employee.current_participations
# => [
#   {
#     target_class: "Department",
#     target_id: "engineering",
#     collection_name: :members,
#     type: :sorted_set,
#     score: 1640995200.0,
#     decoded_score: "2022-01-01 00:00:00 UTC"
#   },
#   {
#     target_class: "Team",
#     target_id: "frontend",
#     collection_name: :contributors,
#     type: :set
#   },
#   {
#     target_class: "Project",
#     target_id: "mobile",
#     collection_name: :assignees,
#     type: :list,
#     position: 2
#   }
# ]

Returns:

  • (Array<Hash>)

    Array of membership details with collection metadata

See Also:

Since Version:

  • 1.0.0



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'lib/familia/features/relationships/participation.rb', line 545

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"
    key_parts = collection_key.split(':')
    next unless key_parts.length >= 3

    target_class_config = key_parts[0]
    target_id = key_parts[1]
    collection_name_from_key = key_parts[2]

    # Find the matching participation configuration
    # Note: target_class_config from key is snake_case
    config = self.class.participation_relationships.find do |cfg|
      cfg.target_class_config_name == 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
    begin
      target_class = Familia.resolve_class(config.target_class)
      target_instance = 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
      membership_data = {
        target_class: config.target_class.familia_name,
        target_id: target_id,
        collection_name: config.collection_name,
        type: config.type,
      }

      case config.type
      when :sorted_set
        score = collection.score(identifier)
        next unless score

        membership_data[:score] = score
        membership_data[:decoded_score] = decode_score(score) if respond_to?(:decode_score)
      when :set
        is_member = collection.member?(identifier)
        next unless is_member
      when :list
        position = collection.to_a.index(identifier)
        next unless position

        membership_data[:position] = position
      end

      memberships << membership_data
    rescue StandardError => e
      Familia.ld "[#{collection_key}] Error checking membership: #{e.message}"
      next
    end
  end

  memberships
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"+

Examples:

domain.track_participation_in("customer:123:domains")

Parameters:

  • collection_key (String)

    Unique identifier for the collection (format: "class:id:collection")

See Also:

Since Version:

  • 1.0.0



457
458
459
460
# File 'lib/familia/features/relationships/participation.rb', line 457

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.

Examples:

domain.untrack_participation_in("customer:123:domains")

Parameters:

  • collection_key (String)

    Collection identifier to remove from tracking

See Also:

Since Version:

  • 1.0.0



474
475
476
477
# File 'lib/familia/features/relationships/participation.rb', line 474

def untrack_participation_in(collection_key)
  # Use Horreum's DataType field instead of manual key construction
  participations.remove(collection_key)
end