Module: Familia::Features::Relationships
- Defined in:
- lib/familia/features/relationships.rb,
lib/familia/features/relationships/indexing.rb,
lib/familia/features/relationships/participation.rb,
lib/familia/features/relationships/score_encoding.rb,
lib/familia/features/relationships/collection_operations.rb,
lib/familia/features/relationships/indexing_relationship.rb,
lib/familia/features/relationships/participation_membership.rb,
lib/familia/features/relationships/participation_relationship.rb,
lib/familia/features/relationships/indexing/rebuild_strategies.rb,
lib/familia/features/relationships/participation/target_methods.rb,
lib/familia/features/relationships/indexing/multi_index_generators.rb,
lib/familia/features/relationships/indexing/unique_index_generators.rb,
lib/familia/features/relationships/participation/participant_methods.rb,
lib/familia/features/relationships/participation/through_model_operations.rb
Overview
Unified Relationships feature for Familia v2
This feature merges the functionality of relatable_objects and relationships into a single, Valkey/Redis-native implementation that embraces the "where does this appear?" philosophy rather than "who owns this?".
Defined Under Namespace
Modules: CollectionOperations, Indexing, ModelClassMethods, ModelInstanceMethods, ParticipantMethods, Participation, ScoreEncoding, TargetMethods Classes: CascadeError, InvalidIdentifierError, InvalidScoreError, RelationshipError
Constant Summary collapse
- IndexingRelationship =
IndexingRelationship
Stores metadata about indexing relationships defined at class level. Used to configure code generation and runtime behavior for unique_index and multi_index declarations.
Similar to ParticipationRelationship but for attribute-based lookups rather than collection membership.
Terminology:
scope_class: The class that provides the uniqueness boundary for instance-scoped indexes. For example, inunique_index :badge_number, :badge_index, within: Company, the Company is the scope class.within: Preserves the original DSL parameter to explicitly distinguish class-level indexes (within: nil) from instance-scoped indexes (within: SomeClass). This avoids brittle class comparisons and prevents issues with inheritance scenarios.
Data.define( :field, # Symbol - field being indexed (e.g., :email, :department) :index_name, # Symbol - name of the index (e.g., :email_index, :dept_index) :scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:) :within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped) :cardinality, # Symbol - :unique (1:1) or :multi (1:many) :query, # Boolean - whether to generate query methods ) do # # Get the normalized config name for the scope class # # @return [String] The config name (e.g., "user", "company", "test_company") # def scope_class_config_name scope_class.config_name end end
- ParticipationMembership =
Note:
This represents what currently exists in Redis, not just configuration. See ParticipationRelationship for static configuration metadata.
ParticipationMembership
Represents runtime snapshot of a participant's membership in a target collection. Returned by current_participations to provide a type-safe, structured view of actual participation state.
Data.define( :target_class, # String - class name (e.g., "Customer") :target_id, # String - target instance identifier :collection_name, # Symbol - collection name (e.g., :domains) :type, # Symbol - collection type (:sorted_set, :set, :list) :score, # Float - optional, for sorted_set only :decoded_score, # Hash - optional, decoded score data :position # Integer - optional, for list only ) do # Check if this membership is a sorted set # @return [Boolean] def sorted_set? type == :sorted_set end # Check if this membership is a set # @return [Boolean] def set? type == :set end # Check if this membership is a list # @return [Boolean] def list? type == :list end # Get the target instance (requires loading from database) # @return [Familia::Horreum, nil] the loaded target instance def target_instance return nil unless target_class # Resolve class from string name # Only rescue NameError (class doesn't exist), not all exceptions klass = Object.const_get(target_class) klass.find_by_id(target_id) rescue NameError # Target class doesn't exist or isn't loaded nil end end
- ParticipationRelationship =
Note:
target_class is resolved once at definition time for performance. Use _original_target for debugging/introspection to see what was passed.
ParticipationRelationship
Stores metadata about participation relationships defined at class level. Used to configure code generation and runtime behavior for participates_in and class_participates_in declarations.
Data.define( :_original_target, # Original Symbol/String/Class as passed to participates_in :target_class, # Resolved Class object (e.g., User class, not :User symbol) :collection_name, # Symbol name of the collection (e.g., :members, :domains) :score, # Proc/Symbol/nil - score calculator for sorted sets :type, # Symbol - collection type (:sorted_set, :set, :list) :generate_participant_methods, # Boolean - whether to generate participant methods :through, # Symbol/Class/nil - through model class for join table pattern :method_prefix, # Symbol/nil - custom prefix for reverse method names (e.g., :team) ) do # Get a unique key for this participation relationship # Useful for comparisons and hash keys # # @return [String] unique identifier in format "TargetClass:collection_name" def unique_key Familia.join(target_class_base, collection_name) end # Get the base class name without namespace # Handles anonymous class wrappers like "#<Class:0x123>::SymbolResolutionCustomer" # # @return [String] base class name (e.g., "Customer") def target_class_base target_class.name.split('::').last end # Check if this relationship matches the given target and collection # Handles namespace-agnostic class comparison # # @param comparison_target [Class, String, Symbol] target to compare against # @param comparison_collection [Symbol, String] collection name to compare # @return [Boolean] true if both target and collection match def matches?(comparison_target, comparison_collection) # Normalize comparison target to base class name comparison_target = comparison_target.name if comparison_target.is_a?(Class) comparison_target_base = comparison_target.to_s.split('::').last target_class_base == comparison_target_base && collection_name == comparison_collection.to_sym end # Check if this relationship uses a through model # # @return [Boolean] true if through model is configured def through_model? !through.nil? end # Resolve the through class to an actual Class object # # @return [Class, nil] The resolved through class or nil def resolved_through_class return nil unless through through.is_a?(Class) ? through : Familia.resolve_class(through) end end
Class Method Summary collapse
-
.included(base) ⇒ Object
Feature initialization.
Class Method Details
.included(base) ⇒ Object
Feature initialization
88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/familia/features/relationships.rb', line 88 def self.included(base) Familia.debug "[#{base}] Relationships included" base.extend ModelClassMethods base.include ModelInstanceMethods # Include all relationship submodules and their class methods base.include ScoreEncoding base.include Participation base.extend Participation::ModelClassMethods base.include Indexing base.extend Indexing::ModelClassMethods end |