Module: Familia::Features::Relationships::CollectionOperations
- Defined in:
- lib/familia/features/relationships/collection_operations.rb
Overview
Shared collection operations for Participation module Provides common methods for working with Horreum-managed DataType collections Used by both ParticipantMethods and TargetMethods to reduce duplication
Instance Method Summary collapse
-
#add_to_collection(collection, item, type:, score: nil, target_class: nil, collection_name: nil) ⇒ Object
Add an item to a collection, handling type-specific operations.
-
#assert_compatible_cap!(existing, max_length, target_class, collection_name, type, dsl: nil) ⇒ Object
When a participation declaration requests a cap but the collection accessor already exists, the existing declaration wins (participation never overwrites it).
-
#bulk_add_to_collection(collection, items, type:, target_class: nil, collection_name: nil) ⇒ Object
Bulk add items to a collection using DataType methods.
-
#ensure_collection_field(target_class, collection_name, type, participant_class: nil, max_length: nil) ⇒ Object
Ensure a target class has the specified DataType field defined.
-
#member_of_collection?(collection, item) ⇒ Boolean
Check if an item is a member of a collection.
-
#remove_from_collection(collection, item, type: nil) ⇒ Object
Remove an item from a collection.
-
#validate_max_length_option!(type, max_length) ⇒ Object
Eager definition-time validation for a max_length: passed through participation.
Instance Method Details
#add_to_collection(collection, item, type:, score: nil, target_class: nil, collection_name: nil) ⇒ Object
Add an item to a collection, handling type-specific operations
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 131 def add_to_collection(collection, item, type:, score: nil, target_class: nil, collection_name: nil) case type when :sorted_set # Ensure score is never nil for sorted sets score ||= calculate_item_score(item, target_class, collection_name) collection.add(item, score) when :list # Lists use push/unshift operations collection.add(item) when :set # Sets use simple add collection.add(item) else raise ArgumentError, "Unknown collection type: #{type}" end end |
#assert_compatible_cap!(existing, max_length, target_class, collection_name, type, dsl: nil) ⇒ Object
When a participation declaration requests a cap but the collection accessor already exists, the existing declaration wins (participation never overwrites it). Agreement is fine — repeating the cap is harmless — but a mismatch would silently produce a collection with the wrong cap (or none), so it raises instead. No-op when the participation declaration did not request a cap: a pre-declared cap is kept as-is, preserving the pre-declaration pattern.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 96 def assert_compatible_cap!(existing, max_length, target_class, collection_name, type, dsl: nil) return if max_length.nil? dsl ||= type if existing.nil? raise ArgumentError, <<~ERROR max_length: #{max_length} cannot be applied: #{target_class} already defines ##{collection_name}, but not via a `#{dsl} :#{collection_name}` declaration, so participation has no collection declaration to cap. Participation never overwrites an existing method. Either remove max_length: from the participation declaration, or replace the existing ##{collection_name} with a `#{dsl} :#{collection_name}, max_length: #{max_length}` declaration. ERROR end declared_max = existing.opts&.fetch(:max_length, nil) return if declared_max == max_length raise ArgumentError, <<~ERROR max_length: #{max_length} conflicts with the existing :#{collection_name} declaration on #{target_class} (max_length: #{declared_max.inspect}). Participation does not overwrite a collection that is already declared. Either remove max_length: from the participation declaration, or give the `#{dsl} :#{collection_name}` declaration on #{target_class} the same cap. ERROR end |
#bulk_add_to_collection(collection, items, type:, target_class: nil, collection_name: nil) ⇒ Object
Bulk add items to a collection using DataType methods
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 169 def bulk_add_to_collection(collection, items, type:, target_class: nil, collection_name: nil) return if items.empty? case type when :sorted_set # Add items one by one for sorted sets to ensure proper scoring items.each do |item| score = calculate_item_score(item, target_class, collection_name) collection.add(item, score) end when :set, :list # For sets and lists, add items one by one using DataType methods items.each do |item| collection.add(item) end else raise ArgumentError, "Unknown collection type: #{type}" end end |
#ensure_collection_field(target_class, collection_name, type, participant_class: nil, max_length: nil) ⇒ Object
Ensure a target class has the specified DataType field defined
When +participant_class+ is provided, the collection is declared with +record_class:+ pointing at the participant class. This is a loading-only hint: it lets +each_record+ hydrate the stored participant identifiers via +load_multi+ (issue #297) WITHOUT changing how the collection deserializes reads. +members+/+member?+/+score+ keep the generic DataType semantics, so adding participation to a collection is transparent to existing readers.
This deliberately differs from +instances+ and +unique_index+, which use +class: + reference: true+ because they also want raw-string read semantics. Participation only needs the loading capability, so it uses the narrower +record_class:+ option and avoids any read-behavior change.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 39 def ensure_collection_field(target_class, collection_name, type, participant_class: nil, max_length: nil) validate_max_length_option!(type, max_length) if target_class.method_defined?(collection_name) existing = target_class.[collection_name.to_s.to_sym] assert_compatible_cap!(existing, max_length, target_class, collection_name, type) return end opts = {} opts[:record_class] = participant_class if participant_class opts[:max_length] = max_length if max_length target_class.send(type, collection_name, **opts) end |
#member_of_collection?(collection, item) ⇒ Boolean
Check if an item is a member of a collection
161 162 163 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 161 def member_of_collection?(collection, item) collection.member?(item) end |
#remove_from_collection(collection, item, type: nil) ⇒ Object
Remove an item from a collection
152 153 154 155 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 152 def remove_from_collection(collection, item, type: nil) # All collection types support remove/delete collection.remove(item) end |
#validate_max_length_option!(type, max_length) ⇒ Object
Eager definition-time validation for a max_length: passed through participation. The DataType itself validates in #initialize, but for instance-level relations that instance is created lazily on first accessor call — too late to point at the participates_in line that caused it. No-op when max_length is nil.
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/familia/features/relationships/collection_operations.rb', line 62 def validate_max_length_option!(type, max_length) return if max_length.nil? unless max_length.is_a?(Integer) && max_length.positive? raise ArgumentError, "max_length must be a positive Integer, got #{max_length.inspect}" end return if Familia::DataType.registered_type(type)&.supports_max_length? raise ArgumentError, "max_length: is not supported for type: #{type.inspect} collections " \ '(only :sorted_set and :list implement capping)' end |