This document demonstrates the new collection-level validation functionality in Lutaml::Model::Collection.

Overview

Collections support two types of validations:

  1. Instance-level validations: Applied to each individual item in the collection (existing functionality)

  2. Collection-level validations: Applied to the collection as a whole (new functionality)

  3. Validation chaining: Collection-level validations can share state and coordinate with each other

Instance-level Validations (Existing)

These validations work on each individual item in the collection:

class PublicationCollection < Lutaml::Model::Collection
  instances(:publications, Publication) do
    validates :year, numericality: { greater_than: 1900 }
    validates :title, presence: true

    validate :must_have_author

    def must_have_author(publications)
      publications.each do |publication|
        next unless publication.author.nil?
        errors.add(:author, "`#{publication.title}` must have an author")
      end
    end
  end
end

Collection-level Validations (New)

These validations work on the entire collection:

1. Uniqueness Validation

Ensures that a field value is unique across all instances in the collection:

class UniquePublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication
  validates_uniqueness_of :id, message: "Publication IDs must be unique"
  validates_uniqueness_of :title  # Uses default message
end

# Usage
collection = UniquePublicationCollection.new([
  Publication.new(id: "1", title: "Title A"),
  Publication.new(id: "1", title: "Title B")  # Duplicate ID!
])

collection.validate!
# => Raises ValidationError: "Publication IDs must be unique"

2. Count Validations

Ensures the collection has the right number of items:

class SizedPublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication
  validates_min_count 2, message: "Must have at least 2 publications"
  validates_max_count 10, message: "Cannot have more than 10 publications"
end

# Usage
collection = SizedPublicationCollection.new([Publication.new(id: "1")])
collection.validate!
# => Raises ValidationError: "Must have at least 2 publications"

3. "All Must Have" Validation

Ensures all instances have specific required attributes:

class CompletePublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication
  validates_all_present :author, message: "All publications must have an author"
  validates_all_present :year
end

# Usage
collection = CompletePublicationCollection.new([
  Publication.new(id: "1", title: "Title A", author: "Author A"),
  Publication.new(id: "2", title: "Title B")  # Missing author!
])

collection.validate!
# => Raises ValidationError: "All publications must have an author"

4. Custom Collection Validations

Define custom validation logic that operates on the entire collection:

class CustomValidatedPublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication

  # Ensure publication years are sequential
  validate_collection do |publications, errors|
    return if publications.empty?

    years = publications.map(&:year).compact.sort
    (1...years.length).each do |i|
      unless years[i] == years[i-1] + 1
        errors.add(:collection, "Publication years must be sequential")
        break
      end
    end
  end

  # Ensure diversity of categories
  validate_collection do |publications, errors|
    categories = publications.map(&:category).compact
    if categories.uniq.length < 2
      errors.add(:collection, "Collection must have publications from at least 2 different categories")
    end
  end
end

Validation Chaining

Collection-level validations can share state through a context object, enabling sophisticated validation workflows:

The ValidationContext Object

Each validation receives a third parameter (context) that enables chaining:

class PublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication

  validates_uniqueness_of :id

  validate_collection do |collection, errors, ctx|
    # ctx stores results from previous validations
    # ctx[:duplicates_of_id] contains any duplicate IDs found

    if ctx[:duplicates_of_id]&.any?
      errors.add(:collection, "Cannot proceed with duplicate IDs")
    end
  end
end

Context Methods

The context object provides these methods:

  • ctx[:key] - Get stored value

  • ctx[:key] = value - Store a value for downstream validations

  • ctx.failed? - Check if any errors have been added

  • ctx.stopped? - Check if chain was stopped

  • ctx.stop! - Stop the validation chain

Built-in Context Values

After validation methods run, these values are automatically stored:

  • ctx[:duplicates_of_<field>] - Set of duplicate values for uniqueness validations

  • ctx[:missing_<field>_count] - Count of items missing a field for validates_all_present

Conditional Execution

Validations can be conditionally run based on context state:

class PublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication

  validates_uniqueness_of :id

  # Only run if uniqueness passed
  validate_collection(if_cond: ->(ctx) { !ctx[:duplicates_of_id]&.any? }) do |collection, errors, ctx|
    # This expensive validation only runs when IDs are unique
  end

  # Skip when condition is met
  validate_collection(unless_cond: ->(ctx) { ctx[:duplicates_of_id]&.any? }) do |collection, errors, ctx|
    # This also only runs when IDs are unique
  end
end

Early Exit

Stop the validation chain when a critical validation fails:

class PublicationCollection < Lutaml::Model::Collection
  instances :publications, Publication

  validate_collection do |collection, errors, ctx|
    # Critical validation
    if collection.empty?
      errors.add(:collection, "Cannot have empty collection")
      ctx.stop!  # Stop further validations
    end
  end

  validate_collection do |collection, errors, ctx|
    # This will NOT run if the first validation called ctx.stop!
  end
end

Complete Chaining Example

class BusinessRuleCollection < Lutaml::Model::Collection
  instances :publications, Publication

  # Step 1: Basic data integrity
  validates_uniqueness_of :id
  validates_all_present :author
  validates_min_count 1

  # Step 2: Cross-validation (only runs if above passed)
  validate_collection do |collection, errors, ctx|
    return if ctx[:duplicates_of_id]&.any?

    # Expensive cross-reference check
    # (safely skipped if duplicates exist)
  end

  # Step 3: Aggregate business rules
  validate_collection do |collection, errors, ctx|
    # Can reference results from any previous validation
    if ctx[:missing_author_count].to_i > 5
      errors.add(:collection, "Too many items missing author")
    end
  end
end

Combining Instance and Collection Validations

You can use both types of validations together:

class MixedValidationPublicationCollection < Lutaml::Model::Collection
  # Instance-level validations (applied to each item)
  instances(:publications, Publication) do
    validates :year, numericality: { greater_than: 1900 }
    validates :title, presence: true
  end

  # Collection-level validations (applied to the whole collection)
  validates_uniqueness_of :id
  validates_min_count 1
  validates_all_present :author

  # Custom collection validation
  validate_collection do |publications, errors|
    total_pages = publications.sum { |pub| pub.pages || 0 }
    if total_pages > 10000
      errors.add(:collection, "Total pages across all publications cannot exceed 10,000")
    end
  end
end

Available Collection Validation Methods

Built-in Validators

  • validates_uniqueness_of(field, message: nil) - Ensures field values are unique

  • validates_min_count(count, message: nil) - Ensures minimum number of items

  • validates_max_count(count, message: nil) - Ensures maximum number of items

  • validates_all_present(field, message: nil) - Ensures all items have the field

Custom Validators

  • validate_collection(&block) - Define custom validation logic

  • validate_collection(if_cond: →(ctx) { …​ }, &block) - Conditional validation

  • validate_collection(unless_cond: →(ctx) { …​ }, &block) - Skip on condition

The block can receive 1, 2, or 3 parameters:

  • |collection| - Just the collection items

  • |collection, errors| - Collection and errors object

  • |collection, errors, ctx| - Collection, errors, and context for chaining

Error Handling

Collection validations integrate with the existing error handling system:

collection = UniquePublicationCollection.new([duplicate_items])

# Check for errors without raising
errors = collection.validate
if errors.any?
  puts "Validation failed: #{errors.map(&:message).join(', ')}"
end

# Raise on validation failure
begin
  collection.validate!
rescue Lutaml::Model::ValidationError => e
  puts "Validation error: #{e.message}"
end

Use Cases

Collection-level validations are ideal for:

  1. Uniqueness constraints: Ensuring IDs, names, or other identifiers are unique

  2. Business rules: Enforcing domain-specific collection constraints

  3. Data integrity: Ensuring the collection as a whole makes sense

  4. Size constraints: Limiting or requiring minimum collection sizes

  5. Cross-item relationships: Validating relationships between items

  6. Aggregated properties: Validating sums, averages, or other aggregate values

Migration Guide

Existing collections will continue to work without changes. To add collection-level validations:

  1. Keep existing instance validations in the instances block

  2. Add collection validations using the new class methods or validate_collection

  3. Both types of validations will run when validate or validate! is called

  4. For chaining, add ctx parameter and use context methods

# Old code still works:
class OldCollection < Lutaml::Model::Collection
  instances :items, Item do
    validates :name, presence: true
  end
end

# Add collection-level validation:
class NewCollection < Lutaml::Model::Collection
  instances :items, Item do
    validates :name, presence: true
  end

  validates_uniqueness_of :id

  # Optional: Add chaining
  validate_collection do |items, errors, ctx|
    # ctx[:duplicates_of_id] is automatically available
  end
end