Class: Lutaml::Model::Collection

Inherits:
Serializable show all
Includes:
Enumerable
Defined in:
lib/lutaml/model/collection.rb

Direct Known Subclasses

SomeCollection

Constant Summary collapse

INHERITED_ATTRIBUTES =
%i[
  instance_type
  instance_name
  sort_by_field
  sort_direction
  indexes
  collection_validations
  organization
].freeze
ALLOWED_OPTIONS =
%i[polymorphic].freeze

Constants included from Serialize

Serialize::DEFAULT_VALUE_MAP, Serialize::INTERNAL_ATTRIBUTES, Serialize::LAZY_EMPTY_COLLECTION

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from Serialize

#lutaml_parent, #lutaml_root

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Serialize

#attr_value, #attribute_exist?, #extract_register_id, included, #init_deserialization_state, #key_exist?, #key_value, #method_missing, #prepare_instance_format_options, #pretty_print_instance_variables, register_format_mapping_method, register_from_format_method, register_to_format_method, #respond_to_missing?, #to_yaml_hash, #using_default?, #using_default_for, #validate_attribute!, #validate_root_mapping!, #value_map, #value_set_for

Methods included from Liquefiable

included, #to_liquid

Methods included from Validation

#format_element_sequences, #order_names, #validate!, #validate_helper, #validate_sequence!

Methods included from ComparableModel

#already_compared?, #attributes_hash, #calculate_hash, #comparison_key, #eql?, #hash, included, #same_class?

Methods included from Serialize::Builder

#mixed_content?

Constructor Details

#initialize(items = [], lutaml_register: Lutaml::Model::Config.default_register) ⇒ Collection

Returns a new instance of Collection.



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/lutaml/model/collection.rb', line 434

def initialize(items = [],
lutaml_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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Lutaml::Model::Serialize

Class Attribute Details

.collection_validationsObject (readonly)

Returns the value of attribute collection_validations.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def collection_validations
  @collection_validations
end

.indexesObject (readonly)

Returns the value of attribute indexes.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def indexes
  @indexes
end

.instance_nameObject (readonly)

Returns the value of attribute instance_name.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def instance_name
  @instance_name
end

.instance_typeObject (readonly)

Returns the value of attribute instance_type.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def instance_type
  @instance_type
end

.organizationObject (readonly)

Returns the value of attribute organization.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def organization
  @organization
end

.sort_by_fieldObject (readonly)

Returns the value of attribute sort_by_field.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def sort_by_field
  @sort_by_field
end

.sort_directionObject (readonly)

Returns the value of attribute sort_direction.



78
79
80
# File 'lib/lutaml/model/collection.rb', line 78

def sort_direction
  @sort_direction
end

Instance Attribute Details

#index_cachesHash? (readonly)

Returns Hash of { field_name => { key => item } } or nil.

Returns:

  • (Hash, nil)

    Hash of { field_name => { key => item } } or nil



540
541
542
# File 'lib/lutaml/model/collection.rb', line 540

def index_caches
  @index_caches
end

#lutaml_registerObject (readonly)

Returns the value of attribute lutaml_register.



432
433
434
# File 'lib/lutaml/model/collection.rb', line 432

def lutaml_register
  @lutaml_register
end

Class Method Details

.apply_mappings(data, format, options = {}) ⇒ Object



376
377
378
# File 'lib/lutaml/model/collection.rb', line 376

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

.as(format, instance, options = {}) ⇒ Object



320
321
322
323
324
325
326
327
328
329
# File 'lib/lutaml/model/collection.rb', line 320

def as(format, instance, options = {})
  mappings = mappings_for(format)
  data = super

  if !collection_structured_format?(format) && mappings.no_root? && !mappings.root_mapping
    unwrap_no_root_data(data)
  else
    data
  end
end

.collection_no_root_to(_format, _mappings, _instance, _options) ⇒ Object

Hook for structured-format no_root serialization (e.g., XML). XML overrides to serialize each mapping separately.

Raises:

  • (NotImplementedError)


366
367
368
# File 'lib/lutaml/model/collection.rb', line 366

def collection_no_root_to(_format, _mappings, _instance, _options)
  raise NotImplementedError
end

.collection_no_root_to?(_format) ⇒ Boolean

Hook: returns true if this format handles no_root serialization specially. XML overrides to return true for :xml format.

Returns:

  • (Boolean)


360
361
362
# File 'lib/lutaml/model/collection.rb', line 360

def collection_no_root_to?(_format)
  false
end

.collection_structured_format?(_format) ⇒ Boolean

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

Returns:

  • (Boolean)


354
355
356
# File 'lib/lutaml/model/collection.rb', line 354

def collection_structured_format?(_format)
  false
end

.from(format, data, options = {}) ⇒ Object



331
332
333
334
335
336
337
338
339
# File 'lib/lutaml/model/collection.rb', line 331

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

  if collection_structured_format?(format) && mappings.no_root?
    data = wrap_no_root_input(format, mappings, data)
  end

  super(format, data, options.merge(from_collection: true))
end

.index(name, by:) ⇒ Object

Named index with optional proc for custom key extraction Example: index :email, by: ->(item) { item.email.downcase }



139
140
141
142
# File 'lib/lutaml/model/collection.rb', line 139

def index(name, by:)
  @indexes ||= {}
  @indexes[name.to_sym] = by
end

.index_by(*fields) ⇒ Object

Index by one or more fields for O(1) lookups Example: index_by :id, :email



126
127
128
129
130
131
132
133
134
135
# File 'lib/lutaml/model/collection.rb', line 126

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

.index_configured?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/lutaml/model/collection.rb', line 144

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

.inherited(subclass) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'lib/lutaml/model/collection.rb', line 67

def inherited(subclass)
  super

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

.instances(name, type, options = {}, &block) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/lutaml/model/collection.rb', line 86

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

.of(format, data, options = {}) ⇒ Object



341
342
343
344
345
346
347
348
349
# File 'lib/lutaml/model/collection.rb', line 341

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

.organizes(name, group_class) ⇒ Object

Declare that this Collection produces organized instances of a GroupClass.

Parameters:

  • name (Symbol)

    attribute name on the Collection

  • group_class (Class)

    the GroupClass type



106
107
108
109
# File 'lib/lutaml/model/collection.rb', line 106

def organizes(name, group_class)
  attribute(name, group_class, collection: true)
  @organization = Organization.new(name, group_class)
end

.sort(by:, order: :asc) ⇒ Object Also known as: ordered



111
112
113
114
115
116
# File 'lib/lutaml/model/collection.rb', line 111

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

  check_sort_configs!
end

.sort_configured?Boolean

Returns:

  • (Boolean)


120
121
122
# File 'lib/lutaml/model/collection.rb', line 120

def sort_configured?
  !!@sort_by_field
end

.to(format, instance, options = {}) ⇒ Object



310
311
312
313
314
315
316
317
318
# File 'lib/lutaml/model/collection.rb', line 310

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

  if mappings.no_root? && collection_no_root_to?(format)
    collection_no_root_to(format, mappings, instance, options)
  else
    super(format, instance, options.merge(collection: true))
  end
end

.validate_collection(&block) ⇒ Object .validate_collection(if_cond:, &block) ⇒ Object .validate_collection(unless_cond:, &block) ⇒ Object

Define collection-level validations

Examples:

Basic usage

validate_collection do |collection, errors, ctx|
  # Custom validation logic
end

Conditional execution based on context state

validate_collection do |collection, errors, ctx|
  # Only run if uniqueness validation passed
  return if ctx[:duplicates_found]
  # Expensive cross-reference check...
end

Conditional validation with :if_cond option

validate_collection(if_cond: ->(ctx) { !ctx[:skip_expensive_checks] }) do |collection, errors|
  # This only runs when ctx[:skip_expensive_checks] is falsy
end

Sharing results between validations

validates_uniqueness_of :id  # Stores duplicates in ctx[:duplicates_of_id]

validate_collection do |collection, errors, ctx|
  if ctx[:duplicates_of_id]&.any?
    errors.add(:collection, "Found duplicates that prevent cross-validation")
  end
end

Overloads:

  • .validate_collection(&block) ⇒ Object

    Define a custom validation block

    Parameters:

    • block (Proc)

      Block receiving (collection, errors, context)

  • .validate_collection(if_cond:, &block) ⇒ Object

    Define a conditional validation that only runs if the condition is met

    Parameters:

    • if_cond (Proc)

      Block receiving (context) - validation runs if it returns true

    • block (Proc)

      Block receiving (collection, errors, context)

  • .validate_collection(unless_cond:, &block) ⇒ Object

    Define a conditional validation that runs unless the condition is met

    Parameters:

    • unless_cond (Proc)

      Block receiving (context) - validation skips if it returns true

    • block (Proc)

      Block receiving (collection, errors, context)



190
191
192
193
194
195
196
# File 'lib/lutaml/model/collection.rb', line 190

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

.validates_all_present(field, message: nil) ⇒ Object

Validate that all instances have a specific attribute

Examples:

Basic presence validation

validates_all_present :author, message: "All items must have an author"

Checking context in subsequent validation

validates_all_present :email  # Sets ctx[:missing_email_count] if items are missing email

validate_collection do |collection, errors, ctx|
  if ctx[:missing_email_count].to_i > 5
    errors.add(:collection, "Too many items missing email addresses")
  end
end

Parameters:

  • field (Symbol)

    The attribute name that must be present on all items

  • message (String, nil) (defaults to: nil)

    Custom error message



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/lutaml/model/collection.rb', line 290

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.respond_to?(field) ? 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

.validates_max_count(count, message: nil) ⇒ Object

Validate maximum count requirement

Examples:

Basic maximum count

validates_max_count 100, message: "Cannot exceed 100 items"

Parameters:

  • count (Integer)

    Maximum number of items allowed

  • message (String, nil) (defaults to: nil)

    Custom error message



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/lutaml/model/collection.rb', line 261

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

.validates_min_count(count, message: nil) ⇒ Object

Validate minimum count requirement

Examples:

Basic minimum count

validates_min_count 1, message: "At least one item is required"

Parameters:

  • count (Integer)

    Minimum number of items required

  • message (String, nil) (defaults to: nil)

    Custom error message



241
242
243
244
245
246
247
248
249
250
251
# File 'lib/lutaml/model/collection.rb', line 241

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

.validates_uniqueness_of(field, message: nil) ⇒ Object

Validate uniqueness of a field across all instances in the collection

Examples:

Basic uniqueness

validates_uniqueness_of :id

With custom message

validates_uniqueness_of :email, message: "Email addresses must be unique"

Using context in subsequent validations

validates_uniqueness_of :id  # Stores duplicate values in ctx[:duplicates_of_id]

validate_collection do |collection, errors, ctx|
  return if ctx[:duplicates_of_id].nil? || ctx[:duplicates_of_id].empty?
  # Handle the duplicate IDs...
end

Parameters:

  • field (Symbol)

    The attribute name to check for uniqueness

  • message (String, nil) (defaults to: nil)

    Custom error message



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/lutaml/model/collection.rb', line 217

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

.wrap_no_root_input(_format, _mappings, data) ⇒ Object

Hook for structured-format no_root input wrapping (e.g., XML). XML overrides to wrap raw data in a fake root tag.



372
373
374
# File 'lib/lutaml/model/collection.rb', line 372

def wrap_no_root_input(_format, _mappings, data)
  data
end

Instance Method Details

#<<(item) ⇒ Object



500
501
502
# File 'lib/lutaml/model/collection.rb', line 500

def <<(item)
  push(item)
end

#[](index) ⇒ Object



510
511
512
# File 'lib/lutaml/model/collection.rb', line 510

def [](index)
  collection[index]
end

#[]=(index, value) ⇒ Object



514
515
516
517
518
# File 'lib/lutaml/model/collection.rb', line 514

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

#apply_sort!Object



588
589
590
591
592
593
594
595
596
# File 'lib/lutaml/model/collection.rb', line 588

def apply_sort!
  field = self.class.sort_by_field

  if field.is_a?(Proc)
    collection.sort_by!(&field)
  else
    collection.sort_by! { |item| item.send(field) }
  end
end

#build_index_caches!Object

Build index caches for all configured indexes



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/lutaml/model/collection.rb', line 543

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.send(field_or_proc)
            end
      @index_caches[name][key] = item
    end
  end
end

#collectionObject



462
463
464
# File 'lib/lutaml/model/collection.rb', line 462

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

#collection=(collection) ⇒ Object



466
467
468
469
470
# File 'lib/lutaml/model/collection.rb', line 466

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

#difference(other) ⇒ Object



480
481
482
# File 'lib/lutaml/model/collection.rb', line 480

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

#eachObject



484
485
486
# File 'lib/lutaml/model/collection.rb', line 484

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

#empty?Boolean

Returns:

  • (Boolean)


520
521
522
# File 'lib/lutaml/model/collection.rb', line 520

def empty?
  collection&.empty?
end

#fetch(key) ⇒ Object?

Fetch an item by key (only for single-index collections)

Parameters:

  • key (Object)

    The key to look up

Returns:

  • (Object, nil)

    The item or nil if not found

Raises:

  • (ArgumentError)

    If multiple indexes are configured



578
579
580
581
582
583
584
585
586
# File 'lib/lutaml/model/collection.rb', line 578

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

#find_by(field, key) ⇒ Object?

Find an item by index field and key

Parameters:

  • field (Symbol)

    The index field name

  • key (Object)

    The key to look up

Returns:

  • (Object, nil)

    The item or nil if not found



567
568
569
570
571
572
# File 'lib/lutaml/model/collection.rb', line 567

def find_by(field, key)
  return nil unless @index_caches

  cache = @index_caches[field.to_sym]
  cache&.fetch(key, nil)
end

#firstObject



492
493
494
# File 'lib/lutaml/model/collection.rb', line 492

def first
  collection.first
end

#intersection(other) ⇒ Object



476
477
478
# File 'lib/lutaml/model/collection.rb', line 476

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

#lastObject



496
497
498
# File 'lib/lutaml/model/collection.rb', line 496

def last
  collection.last
end

#order_defined?Boolean

Returns:

  • (Boolean)


524
525
526
# File 'lib/lutaml/model/collection.rb', line 524

def order_defined?
  self.class.sort_configured?
end

#push(item) ⇒ Object



504
505
506
507
508
# File 'lib/lutaml/model/collection.rb', line 504

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

#sizeObject



488
489
490
# File 'lib/lutaml/model/collection.rb', line 488

def size
  collection.size
end

#sort_items!Object



528
529
530
531
532
533
534
535
# File 'lib/lutaml/model/collection.rb', line 528

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

#to_format(format, options = {}) ⇒ Object



458
459
460
# File 'lib/lutaml/model/collection.rb', line 458

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

#union(other) ⇒ Object



472
473
474
# File 'lib/lutaml/model/collection.rb', line 472

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

#validate(register: Lutaml::Model::Config.default_register) ⇒ Array<Lutaml::Model::ValidationFailedError>

Override validate to support both instance and collection-level validations

Collection-level validations run in order and can share state through a context object. Validations can stop the chain early by calling ctx.stop! or by checking ctx to see results from previous validations.

Examples:

Standard usage

collection.validate  # Returns array of errors, doesn't raise
collection.validate! # Raises if any errors found

With chaining (context)

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

  validates_uniqueness_of :id  # Stores ctx[:duplicates_of_id]

  validate_collection do |collection, errors, ctx|
    # Check if uniqueness validation found duplicates
    return if ctx[:duplicates_of_id].nil? || ctx[:duplicates_of_id].empty?

    # Skip expensive validation if duplicates exist
    errors.add(:collection, "Cannot run expensive checks with duplicate IDs")
  end
end

Returns:



625
626
627
628
629
630
631
632
633
634
635
# File 'lib/lutaml/model/collection.rb', line 625

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