This document demonstrates the new collection-level validation functionality in Lutaml::Model::Collection.
Overview
Collections support two types of validations:
-
Instance-level validations: Applied to each individual item in the collection (existing functionality)
-
Collection-level validations: Applied to the collection as a whole (new functionality)
-
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
endCollection-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
endValidation 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
endContext 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 forvalidates_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
endEarly 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
endComplete 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
endCombining 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
endAvailable 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}"
endUse Cases
Collection-level validations are ideal for:
-
Uniqueness constraints: Ensuring IDs, names, or other identifiers are unique
-
Business rules: Enforcing domain-specific collection constraints
-
Data integrity: Ensuring the collection as a whole makes sense
-
Size constraints: Limiting or requiring minimum collection sizes
-
Cross-item relationships: Validating relationships between items
-
Aggregated properties: Validating sums, averages, or other aggregate values
Migration Guide
Existing collections will continue to work without changes. To add collection-level validations:
-
Keep existing instance validations in the
instancesblock -
Add collection validations using the new class methods or
validate_collection -
Both types of validations will run when
validateorvalidate!is called -
For chaining, add
ctxparameter 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