class Lutaml::Model::Collection

def <<(item)

def <<(item)
  push(item)
end

def [](index)

def [](index)
  collection[index]
end

def []=(index, value)

def []=(index, value)
  collection[index] = value
  sort_items!
  build_index_caches!
end

def add_uniqueness_error(errors, field, duplicates, message)

def add_uniqueness_error(errors, field, duplicates, message)
  default_message = "#{field} values must be unique across collection, found duplicates: #{duplicates.to_a.join(', ')}"
  errors.add(:collection, message || default_message)
end

def apply_mappings(data, format, options = {})

def apply_mappings(data, format, options = {})
  super(data, format, options.merge(collection: true))
end

def apply_sort!

def apply_sort!
  field = self.class.sort_by_field
  if field.is_a?(Proc)
    collection.sort_by!(&field)
  else
    collection.sort_by! { |item| item.public_send(field) }
  end
end

def as(format, instance, options = {})

def as(format, instance, options = {})
  mappings = mappings_for(format)
  data = super
  if !collection_structured_format?(format) && mappings.no_root? && !mappings.root_mapping
    unwrap_unwrapped_data(data)
  else
    data
  end
end

def build_index_caches!

Build index caches for all configured indexes
def build_index_caches!
  return unless self.class.index_configured?
  return if collection.nil? || collection.empty?
  @index_caches = {}
  self.class.indexes.each do |name, field_or_proc|
    @index_caches[name] = {}
    collection.each do |item|
      key = if field_or_proc.is_a?(Proc)
              field_or_proc.call(item)
            else
              item.public_send(field_or_proc)
            end
      @index_caches[name][key] = item
    end
  end
end

def collection

def collection
  instance_variable_get(:"@#{self.class.instance_name}")
end

def collection=(collection)

def collection=(collection)
  instance_variable_set(:"@#{self.class.instance_name}", collection)
  sort_items!
  build_index_caches!
end

def collection_structured_format?(_format)

XML overrides to return true.
like XML. Key-value formats (JSON, YAML, TOML) return false (default).
Hook: returns true for formats that use structured (tree-based) serialization
def collection_structured_format?(_format)
  false
end

def collection_unwrapped_to(_format, _mappings, _instance, _options)

XML overrides to serialize each mapping separately.
Hook for unwrapped serialization (e.g., XML).
def collection_unwrapped_to(_format, _mappings, _instance, _options)
  raise NotImplementedError
end

def collection_unwrapped_to?(_format)

XML overrides to return true for :xml format.
Hook: returns true if this format handles unwrapped serialization specially.
def collection_unwrapped_to?(_format)
  false
end

def difference(other)

def difference(other)
  self.class.new(items - other.items)
end

def each(&)

def each(&)
  collection.each(&)
end

def empty?

def empty?
  collection&.empty?
end

def extract_field_value(instance, field)

def extract_field_value(instance, field)
  instance.is_a?(Serialize) ? instance.public_send(field) : nil
end

def fetch(key)

Raises:
  • (ArgumentError) - If multiple indexes are configured

Returns:
  • (Object, nil) - The item or nil if not found

Parameters:
  • key (Object) -- The key to look up
def fetch(key)
  unless self.class.indexes&.one?
    raise ArgumentError,
          "#fetch only works with single index. Use #find_by(field, key)"
  end
  field = self.class.indexes.keys.first
  find_by(field, key)
end

def find_by(field, key)

Returns:
  • (Object, nil) - The item or nil if not found

Parameters:
  • key (Object) -- The key to look up
  • field (Symbol) -- The index field name
def find_by(field, key)
  return nil unless @index_caches
  cache = @index_caches[field.to_sym]
  cache&.fetch(key, nil)
end

def find_duplicate_values(collection, field)

def find_duplicate_values(collection, field)
  seen_values = Set.new
  duplicates = Set.new
  collection.each do |instance|
    value = extract_field_value(instance, field)
    next if value.nil?
    if seen_values.include?(value)
      duplicates.add(value)
    else
      seen_values.add(value)
    end
  end
  duplicates
end

def first

def first
  collection.first
end

def from(format, data, options = {})

def from(format, data, options = {})
  mappings = mappings_for(format)
  if collection_structured_format?(format) && mappings.no_root?
    data = wrap_unwrapped_input(format, mappings, data)
  end
  super(format, data, options.merge(from_collection: true))
end

def index(name, by:)

Example: index :email, by: ->(item) { item.email.downcase }
Named index with optional proc for custom key extraction
def index(name, by:)
  @indexes ||= {}
  @indexes[name.to_sym] = by
end

def index_by(*fields)

Example: index_by :id, :email
Index by one or more fields for O(1) lookups
def index_by(*fields)
  @indexes ||= {}
  fields.each do |field|
    if field.is_a?(Proc)
      raise ArgumentError,
            "Proc indexes require a name. Use: index :name, by: ->(item) { ... }"
    end
    @indexes[field.to_sym] = field.to_sym
  end
end

def index_configured?

def index_configured?
  @indexes && !@indexes.empty?
end

def inherited(subclass)

def inherited(subclass)
  super
  INHERITED_ATTRIBUTES.each do |var|
    subclass.instance_variable_set(
      :"@#{var}",
      instance_variable_get(:"@#{var}"),
    )
  end
end

def initialize(items = [],

def initialize(items = [],
_register: Lutaml::Model::Config.default_register)
  super()
  @lutaml_register = lutaml_register
  items = [items].compact unless items.is_a?(Array)
  type = Lutaml::Model::GlobalContext.resolve_type(
    self.class.instance_type, @lutaml_register
  )
  self.collection = items.map do |item|
    if item.is_a?(type) || item.is_a?(Lutaml::Model::Serializable)
      item
    elsif type <= Lutaml::Model::Type::Value
      type.cast(item)
    else
      type.new(item)
    end
  end
  sort_items!
  build_index_caches!
end

def instances(name, type, options = {}, &block)

def instances(name, type, options = {}, &block)
  if (invalid_opts = options.keys - ALLOWED_OPTIONS).any?
    raise Lutaml::Model::InvalidAttributeOptionsError.new(name,
                                                          invalid_opts)
  end
  attribute(name, type, collection: true, validations: block, **options)
  @instance_type = Lutaml::Model::Attribute.cast_type!(type)
  @instance_name = name
  define_method(:"#{name}=") do |collection|
    self.collection = collection
  end
end

def intersection(other)

def intersection(other)
  self.class.new(items & other.items)
end

def last

def last
  collection.last
end

def of(format, data, options = {})

def of(format, data, options = {})
  mappings = mappings_for(format)
  if !collection_structured_format?(format) && mappings.no_root? && !mappings.root_mapping
    data = { mappings.find_by_to!(instance_name).name => data }
  end
  super(format, data, options.merge(from_collection: true))
end

def order_defined?

def order_defined?
  self.class.sort_configured?
end

def organizes(name, group_class)

Parameters:
  • group_class (Class) -- the GroupClass type
  • name (Symbol) -- attribute name on the Collection
def organizes(name, group_class)
  attribute(name, group_class, collection: true)
  @organization = Organization.new(name, group_class)
end

def push(item)

def push(item)
  collection.push(item)
  sort_items!
  build_index_caches!
end

def size

def size
  collection.size
end

def sort(by:, order: :asc)

def sort(by:, order: :asc)
  @sort_by_field = by.is_a?(Proc) ? by : by.to_sym
  @sort_direction = order
  check_sort_configs!
end

def sort_configured?

def sort_configured?
  !!@sort_by_field
end

def sort_items!

def sort_items!
  return if collection.nil?
  return unless order_defined?
  return if collection.one?
  apply_sort!
  collection.reverse! if self.class.sort_direction == :desc
end

def to(format, instance, options = {})

def to(format, instance, options = {})
  mappings = mappings_for(format)
  if mappings.no_root? && collection_unwrapped_to?(format)
    collection_unwrapped_to(format, mappings, instance, options)
  else
    super(format, instance, options.merge(collection: true))
  end
end

def to_format(format, options = {})

def to_format(format, options = {})
  super(format, options.merge(collection: true))
end

def union(other)

def union(other)
  self.class.new((items + other.items).uniq)
end

def unwrap_unwrapped_data(data)

def unwrap_unwrapped_data(data)
  # Convert KeyValueElement to Hash if needed
  hash = data.is_a?(Hash) ? data : data.to_hash
  # Handle "__root__" wrapper for key-value formats (created by transformation)
  hash = hash["__root__"] if hash.key?("__root__")
  result = Utils.fetch_str_or_sym(hash, instance_name)
  # Extract values from nested hashes with empty string keys
  # (created by transformation for simple models with single attribute)
  if result.is_a?(Array)
    result = result.map do |item|
      if item.is_a?(Hash) && item.key?("") && item.size == 1
        item[""]
      else
        item
      end
    end
  end
  result
end

def validate(register: Lutaml::Model::Config.default_register)

Other tags:
    Example: With chaining (context) -
    Example: Standard usage -

Returns:
  • (Array) - List of validation errors
def validate(register: Lutaml::Model::Config.default_register)
  errors = []
  # Run standard instance-level validations first (inherited from Serializable)
  errors.concat(super)
  # Run collection-level validations with context for chaining
  errors.concat(validate_collection_rules)
  errors
end

def validate_collection(if_cond: nil, unless_cond: nil, &block)

Other tags:
    Example: Sharing results between validations -
    Example: Conditional validation with :if_cond option -
    Example: Conditional execution based on context state -
    Example: Basic usage -

Parameters:
  • block (Proc) -- Block receiving (collection, errors, context)
  • unless_cond (Proc) -- Block receiving (context) - validation skips if it returns true
  • block (Proc) -- Block receiving (collection, errors, context)
  • if_cond (Proc) -- Block receiving (context) - validation runs if it returns true
  • block (Proc) -- Block receiving (collection, errors, context)

Overloads:
  • validate_collection(unless_cond:, &block)
  • validate_collection(if_cond:, &block)
  • validate_collection(&block)
def validate_collection(if_cond: nil, unless_cond: nil, &block)
  @collection_validations ||= []
  return unless block
  options = { if_cond: if_cond, unless_cond: unless_cond }
  @collection_validations << [block, options]
end

def validate_collection_rules

def validate_collection_rules
  return [] unless self.class.collection_validations
  errors = Errors.new
  collection_items = collection || []
  context = ValidationContext.new
  self.class.collection_validations.each do |validation_block, options|
    # Check stop condition
    break if context.stopped?
    # Evaluate conditional options
    next if options[:if_cond] && !options[:if_cond].call(context)
    next if options[:unless_cond]&.call(context)
    # Build block arguments based on arity for backwards compatibility
    # Old blocks: |collection, errors|
    # New blocks:  |collection, errors, context|
    begin
      case validation_block.arity
      when 1
        validation_block.call(collection_items)
      when 2
        validation_block.call(collection_items, errors)
      else
        validation_block.call(collection_items, errors, context)
      end
    rescue LocalJumpError
      # ctx.stop! was called via `return` - this is a valid pattern
      context.stop!
    end
    # Update context with current errors
    context.add_errors(errors.messages)
  end
  errors.messages.map { |msg| ValidationFailedError.new([msg]) }
end

def validates_all_present(field, message: nil)

Other tags:
    Example: Checking context in subsequent validation -
    Example: Basic presence validation -

Parameters:
  • message (String, nil) -- Custom error message
  • field (Symbol) -- The attribute name that must be present on all items
def validates_all_present(field, message: nil)
  validate_collection(if_cond: ->(ctx) {
    !ctx.stopped?
  }) do |collection, errors, ctx|
    missing_items = collection.select do |instance|
      value = instance.is_a?(Serialize) ? instance.public_send(field) : nil
      Utils.blank?(value)
    end
    # Store count in context for downstream validations
    ctx[:"missing_#{field}_count"] = missing_items.size
    unless missing_items.empty?
      default_message = "all items must have #{field}, but #{missing_items.size} items are missing it"
      errors.add(:collection, message || default_message)
      ctx.add_errors(errors.messages)
    end
  end
end

def validates_max_count(count, message: nil)

Other tags:
    Example: Basic maximum count -

Parameters:
  • message (String, nil) -- Custom error message
  • count (Integer) -- Maximum number of items allowed
def validates_max_count(count, message: nil)
  validate_collection(if_cond: ->(ctx) {
    !ctx.stopped?
  }) do |collection, errors, ctx|
    if collection.size > count
      default_message = "collection must have at most #{count} items, but has #{collection.size}"
      errors.add(:collection, message || default_message)
      ctx.add_errors(errors.messages)
    end
  end
end

def validates_min_count(count, message: nil)

Other tags:
    Example: Basic minimum count -

Parameters:
  • message (String, nil) -- Custom error message
  • count (Integer) -- Minimum number of items required
def validates_min_count(count, message: nil)
  validate_collection(if_cond: ->(ctx) {
    !ctx.stopped?
  }) do |collection, errors, ctx|
    if collection.size < count
      default_message = "collection must have at least #{count} items, but has #{collection.size}"
      errors.add(:collection, message || default_message)
      ctx.add_errors(errors.messages)
    end
  end
end

def validates_uniqueness_of(field, message: nil)

Other tags:
    Example: Using context in subsequent validations -
    Example: With custom message -
    Example: Basic uniqueness -

Parameters:
  • message (String, nil) -- Custom error message
  • field (Symbol) -- The attribute name to check for uniqueness
def validates_uniqueness_of(field, message: nil)
  validate_collection(if_cond: ->(ctx) {
    !ctx.stopped?
  }) do |collection, errors, ctx|
    duplicates = find_duplicate_values(collection, field)
    # Store duplicates in context for potential use by other validations
    ctx[:"duplicates_of_#{field}"] = duplicates
    if duplicates.any?
      add_uniqueness_error(errors, field, duplicates, message)
      ctx.add_errors(errors.messages)
    end
  end
end

def wrap_unwrapped_input(_format, _mappings, data)

XML overrides to wrap raw data in a fake root tag.
Hook for wrapping unwrapped input (e.g., XML).
def wrap_unwrapped_input(_format, _mappings, data)
  data
end