Class: Lutaml::Model::Collection
- Inherits:
-
Serializable
- Object
- Serializable
- Lutaml::Model::Collection
- Includes:
- Enumerable
- Defined in:
- lib/lutaml/model/collection.rb
Direct Known Subclasses
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::GENERATOR_STATE_KEY, Serialize::INTERNAL_ATTRIBUTES, Serialize::LAZY_EMPTY_COLLECTION
Constants included from Validation
Class Attribute Summary collapse
-
.collection_validations ⇒ Object
readonly
Returns the value of attribute collection_validations.
-
.indexes ⇒ Object
readonly
Returns the value of attribute indexes.
-
.instance_name ⇒ Object
readonly
Returns the value of attribute instance_name.
-
.instance_type ⇒ Object
readonly
Returns the value of attribute instance_type.
-
.organization ⇒ Object
readonly
Returns the value of attribute organization.
-
.sort_by_field ⇒ Object
readonly
Returns the value of attribute sort_by_field.
-
.sort_direction ⇒ Object
readonly
Returns the value of attribute sort_direction.
Instance Attribute Summary collapse
-
#index_caches ⇒ Hash?
readonly
Hash of { field_name => { key => item } } or nil.
-
#lutaml_register ⇒ Object
readonly
Returns the value of attribute lutaml_register.
Attributes included from Serialize
Class Method Summary collapse
- .apply_mappings(data, format, options = {}) ⇒ Object
- .as(format, instance, options = {}) ⇒ Object
-
.collection_structured_format?(_format) ⇒ Boolean
Hook: returns true for formats that use structured (tree-based) serialization like XML.
-
.collection_unwrapped_to(_format, _mappings, _instance, _options) ⇒ Object
Hook for unwrapped serialization (e.g., XML).
-
.collection_unwrapped_to?(_format) ⇒ Boolean
Hook: returns true if this format handles unwrapped serialization specially.
- .from(format, data, options = {}) ⇒ Object
-
.index(name, by:) ⇒ Object
Named index with optional proc for custom key extraction Example: index :email, by: ->(item) { item.email.downcase }.
-
.index_by(*fields) ⇒ Object
Index by one or more fields for O(1) lookups Example: index_by :id, :email.
- .index_configured? ⇒ Boolean
- .inherited(subclass) ⇒ Object
- .instances(name, type, options = {}, &block) ⇒ Object
- .of(format, data, options = {}) ⇒ Object
-
.organizes(name, group_class) ⇒ Object
Declare that this Collection produces organized instances of a GroupClass.
- .sort(by:, order: :asc) ⇒ Object (also: ordered)
- .sort_configured? ⇒ Boolean
- .to(format, instance, options = {}) ⇒ Object
-
.validate_collection(if_cond: nil, unless_cond: nil, &block) ⇒ Object
Define collection-level validations.
-
.validates_all_present(field, message: nil) ⇒ Object
Validate that all instances have a specific attribute.
-
.validates_max_count(count, message: nil) ⇒ Object
Validate maximum count requirement.
-
.validates_min_count(count, message: nil) ⇒ Object
Validate minimum count requirement.
-
.validates_uniqueness_of(field, message: nil) ⇒ Object
Validate uniqueness of a field across all instances in the collection.
-
.wrap_unwrapped_input(_format, _mappings, data) ⇒ Object
Hook for wrapping unwrapped input (e.g., XML).
Instance Method Summary collapse
- #<<(item) ⇒ Object
- #[](index) ⇒ Object
- #[]=(index, value) ⇒ Object
- #apply_sort! ⇒ Object
-
#build_index_caches! ⇒ Object
Build index caches for all configured indexes.
-
#collect_validation_errors(register) ⇒ Array<Lutaml::Model::ValidationFailedError>
Adds collection-level validations on top of the instance-level ones.
- #collection ⇒ Object
- #collection=(collection) ⇒ Object
- #difference(other) ⇒ Object
- #each(&block) ⇒ Object
- #empty? ⇒ Boolean
-
#fetch(key) ⇒ Object?
Fetch an item by key (only for single-index collections).
-
#find_by(field, key) ⇒ Object?
Find an item by index field and key.
- #first ⇒ Object
-
#initialize(items = [], lutaml_register: Lutaml::Model::Config.default_register) ⇒ Collection
constructor
A new instance of Collection.
- #intersection(other) ⇒ Object
- #last ⇒ Object
- #order_defined? ⇒ Boolean
- #push(item) ⇒ Object
- #size ⇒ Object
- #sort_items! ⇒ Object
- #to_format(format, options = {}) ⇒ Object
- #union(other) ⇒ Object
Methods included from Serialize
#attr_value, #attribute_exist?, #define_singleton_attribute_methods, #extract_register_id, #finalize_deserialization, 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_in_reference_store, 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, #values_set_for, wrap_generator_state
Methods included from Liquefiable
Methods included from Validation
#element_order, #format_element_sequences, new_registry, #order_names, validate, #validate, #validate!, validate!, #validate_helper, #validate_sequence!, visiting?
Methods included from ComparableModel
#already_compared?, #attributes_hash, #calculate_hash, #comparison_key, #eql?, #hash, included, #same_class?
Methods included from Serialize::Builder
#mixed_content?, #order_tracking_enabled?, #ordered?, #record_mutation, #record_mutation_collection
Constructor Details
#initialize(items = [], lutaml_register: Lutaml::Model::Config.default_register) ⇒ Collection
Returns a new instance of Collection.
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/lutaml/model/collection.rb', line 438 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_validations ⇒ Object (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 |
.indexes ⇒ Object (readonly)
Returns the value of attribute indexes.
78 79 80 |
# File 'lib/lutaml/model/collection.rb', line 78 def indexes @indexes end |
.instance_name ⇒ Object (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_type ⇒ Object (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 |
.organization ⇒ Object (readonly)
Returns the value of attribute organization.
78 79 80 |
# File 'lib/lutaml/model/collection.rb', line 78 def organization @organization end |
.sort_by_field ⇒ Object (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_direction ⇒ Object (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_caches ⇒ Hash? (readonly)
Returns Hash of { field_name => { key => item } } or nil.
545 546 547 |
# File 'lib/lutaml/model/collection.rb', line 545 def index_caches @index_caches end |
#lutaml_register ⇒ Object (readonly)
Returns the value of attribute lutaml_register.
436 437 438 |
# File 'lib/lutaml/model/collection.rb', line 436 def lutaml_register @lutaml_register end |
Class Method Details
.apply_mappings(data, format, options = {}) ⇒ Object
380 381 382 |
# File 'lib/lutaml/model/collection.rb', line 380 def apply_mappings(data, format, = {}) super(data, format, .merge(collection: true)) end |
.as(format, instance, options = {}) ⇒ Object
324 325 326 327 328 329 330 331 332 333 |
# File 'lib/lutaml/model/collection.rb', line 324 def as(format, instance, = {}) 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 |
.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.
358 359 360 |
# File 'lib/lutaml/model/collection.rb', line 358 def collection_structured_format?(_format) false end |
.collection_unwrapped_to(_format, _mappings, _instance, _options) ⇒ Object
Hook for unwrapped serialization (e.g., XML). XML overrides to serialize each mapping separately.
370 371 372 |
# File 'lib/lutaml/model/collection.rb', line 370 def collection_unwrapped_to(_format, _mappings, _instance, ) raise NotImplementedError end |
.collection_unwrapped_to?(_format) ⇒ Boolean
Hook: returns true if this format handles unwrapped serialization specially. XML overrides to return true for :xml format.
364 365 366 |
# File 'lib/lutaml/model/collection.rb', line 364 def collection_unwrapped_to?(_format) false end |
.from(format, data, options = {}) ⇒ Object
335 336 337 338 339 340 341 342 343 |
# File 'lib/lutaml/model/collection.rb', line 335 def from(format, data, = {}) mappings = mappings_for(format) if collection_structured_format?(format) && mappings.no_root? data = wrap_unwrapped_input(format, mappings, data) end super(format, data, .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
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, = {}, &block) if (invalid_opts = .keys - ALLOWED_OPTIONS).any? raise Lutaml::Model::InvalidAttributeOptionsError.new(name, invalid_opts) end attribute(name, type, collection: true, validations: block, **) @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
345 346 347 348 349 350 351 352 353 |
# File 'lib/lutaml/model/collection.rb', line 345 def of(format, data, = {}) 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, .merge(from_collection: true)) end |
.organizes(name, group_class) ⇒ Object
Declare that this Collection produces organized instances of a GroupClass.
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
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 319 320 321 322 |
# File 'lib/lutaml/model/collection.rb', line 310 def to(format, instance, = {}) # Wrap before the branch, not inside it: only XML reaches the # unwrapped path today, but a raw JSON::State must never survive # into either arm. = Lutaml::Model::Serialize.wrap_generator_state() mappings = mappings_for(format) if mappings.no_root? && collection_unwrapped_to?(format) collection_unwrapped_to(format, mappings, instance, ) else super(format, instance, .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
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 = { if_cond: if_cond, unless_cond: unless_cond } @collection_validations << [block, ] end |
.validates_all_present(field, message: nil) ⇒ Object
Validate that all instances have a specific attribute
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.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? = "all items must have #{field}, but #{missing_items.size} items are missing it" errors.add(:collection, || ) ctx.add_errors(errors.) end end end |
.validates_max_count(count, message: nil) ⇒ Object
Validate maximum count requirement
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 = "collection must have at most #{count} items, but has #{collection.size}" errors.add(:collection, || ) ctx.add_errors(errors.) end end end |
.validates_min_count(count, message: nil) ⇒ Object
Validate minimum count requirement
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 = "collection must have at least #{count} items, but has #{collection.size}" errors.add(:collection, || ) ctx.add_errors(errors.) end end end |
.validates_uniqueness_of(field, message: nil) ⇒ Object
Validate uniqueness of a field across all instances in the collection
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, ) ctx.add_errors(errors.) end end end |
.wrap_unwrapped_input(_format, _mappings, data) ⇒ Object
Hook for wrapping unwrapped input (e.g., XML). XML overrides to wrap raw data in a fake root tag.
376 377 378 |
# File 'lib/lutaml/model/collection.rb', line 376 def wrap_unwrapped_input(_format, _mappings, data) data end |
Instance Method Details
#<<(item) ⇒ Object
505 506 507 |
# File 'lib/lutaml/model/collection.rb', line 505 def <<(item) push(item) end |
#[](index) ⇒ Object
515 516 517 |
# File 'lib/lutaml/model/collection.rb', line 515 def [](index) collection[index] end |
#[]=(index, value) ⇒ Object
519 520 521 522 523 |
# File 'lib/lutaml/model/collection.rb', line 519 def []=(index, value) collection[index] = value sort_items! build_index_caches! end |
#apply_sort! ⇒ Object
593 594 595 596 597 598 599 600 601 |
# File 'lib/lutaml/model/collection.rb', line 593 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 |
#build_index_caches! ⇒ Object
Build index caches for all configured indexes
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 |
# File 'lib/lutaml/model/collection.rb', line 548 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 |
#collect_validation_errors(register) ⇒ Array<Lutaml::Model::ValidationFailedError>
Adds collection-level validations on top of the instance-level ones
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.
Hooks into the guarded pipeline rather than overriding #validate, so the collection rules run inside the cycle guard and a self-referencing collection runs them once rather than twice.
633 634 635 636 637 638 639 640 641 642 643 |
# File 'lib/lutaml/model/collection.rb', line 633 def collect_validation_errors(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 |
#collection ⇒ Object
467 468 469 |
# File 'lib/lutaml/model/collection.rb', line 467 def collection instance_variable_get(:"@#{self.class.instance_name}") end |
#collection=(collection) ⇒ Object
471 472 473 474 475 |
# File 'lib/lutaml/model/collection.rb', line 471 def collection=(collection) instance_variable_set(:"@#{self.class.instance_name}", collection) sort_items! build_index_caches! end |
#difference(other) ⇒ Object
485 486 487 |
# File 'lib/lutaml/model/collection.rb', line 485 def difference(other) self.class.new(items - other.items) end |
#each(&block) ⇒ Object
489 490 491 |
# File 'lib/lutaml/model/collection.rb', line 489 def each(&block) collection.each(&block) end |
#empty? ⇒ Boolean
525 526 527 |
# File 'lib/lutaml/model/collection.rb', line 525 def empty? collection&.empty? end |
#fetch(key) ⇒ Object?
Fetch an item by key (only for single-index collections)
583 584 585 586 587 588 589 590 591 |
# File 'lib/lutaml/model/collection.rb', line 583 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
572 573 574 575 576 577 |
# File 'lib/lutaml/model/collection.rb', line 572 def find_by(field, key) return nil unless @index_caches cache = @index_caches[field.to_sym] cache&.fetch(key, nil) end |
#first ⇒ Object
497 498 499 |
# File 'lib/lutaml/model/collection.rb', line 497 def first collection.first end |
#intersection(other) ⇒ Object
481 482 483 |
# File 'lib/lutaml/model/collection.rb', line 481 def intersection(other) self.class.new(items & other.items) end |
#last ⇒ Object
501 502 503 |
# File 'lib/lutaml/model/collection.rb', line 501 def last collection.last end |
#order_defined? ⇒ Boolean
529 530 531 |
# File 'lib/lutaml/model/collection.rb', line 529 def order_defined? self.class.sort_configured? end |
#push(item) ⇒ Object
509 510 511 512 513 |
# File 'lib/lutaml/model/collection.rb', line 509 def push(item) collection.push(item) sort_items! build_index_caches! end |
#size ⇒ Object
493 494 495 |
# File 'lib/lutaml/model/collection.rb', line 493 def size collection.size end |
#sort_items! ⇒ Object
533 534 535 536 537 538 539 540 |
# File 'lib/lutaml/model/collection.rb', line 533 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
462 463 464 465 |
# File 'lib/lutaml/model/collection.rb', line 462 def to_format(format, = {}) = Lutaml::Model::Serialize.wrap_generator_state() super(format, .merge(collection: true)) end |
#union(other) ⇒ Object
477 478 479 |
# File 'lib/lutaml/model/collection.rb', line 477 def union(other) self.class.new((items + other.items).uniq) end |