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
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.
- #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
-
#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?, #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_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?, #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 |
#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 |
#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.
630 631 632 633 634 635 636 637 638 639 640 |
# File 'lib/lutaml/model/collection.rb', line 630 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 |