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::INTERNAL_ATTRIBUTES, Serialize::LAZY_EMPTY_COLLECTION
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_no_root_to(_format, _mappings, _instance, _options) ⇒ Object
Hook for structured-format no_root serialization (e.g., XML).
-
.collection_no_root_to?(_format) ⇒ Boolean
Hook: returns true if this format handles no_root serialization specially.
-
.collection_structured_format?(_format) ⇒ Boolean
Hook: returns true for formats that use structured (tree-based) serialization like XML.
- .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_no_root_input(_format, _mappings, data) ⇒ Object
Hook for structured-format no_root input wrapping (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.
- #collection ⇒ Object
- #collection=(collection) ⇒ Object
- #difference(other) ⇒ Object
- #each ⇒ 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
-
#validate(register: Lutaml::Model::Config.default_register) ⇒ Array<Lutaml::Model::ValidationFailedError>
Override validate to support both instance and collection-level validations.
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
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
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_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.
540 541 542 |
# File 'lib/lutaml/model/collection.rb', line 540 def index_caches @index_caches end |
#lutaml_register ⇒ Object (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, = {}) super(data, format, .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, = {}) 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.
366 367 368 |
# File 'lib/lutaml/model/collection.rb', line 366 def collection_no_root_to(_format, _mappings, _instance, ) 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.
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.
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, = {}) mappings = mappings_for(format) if collection_structured_format?(format) && mappings.no_root? data = wrap_no_root_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
341 342 343 344 345 346 347 348 349 |
# File 'lib/lutaml/model/collection.rb', line 341 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 |
# File 'lib/lutaml/model/collection.rb', line 310 def to(format, instance, = {}) mappings = mappings_for(format) if mappings.no_root? && collection_no_root_to?(format) collection_no_root_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.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? = "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_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 |
#collection ⇒ Object
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 |
#each ⇒ Object
484 485 486 |
# File 'lib/lutaml/model/collection.rb', line 484 def each(&) collection.each(&) end |
#empty? ⇒ 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)
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
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 |
#first ⇒ Object
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 |
#last ⇒ Object
496 497 498 |
# File 'lib/lutaml/model/collection.rb', line 496 def last collection.last end |
#order_defined? ⇒ 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 |
#size ⇒ Object
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, = {}) super(format, .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.
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 |