module Ecoportal module API module Common module Content # Class to handle a plain Array embedded in a Hashed model. # @note # - Its purpose is to handle an Array of basic objects (i.e. `Date`, `String`, `Number`) class ArrayModel < Content::DoubleModel class TypeMismatchedComparison < StandardError def initialize(this: nil, that: msg = "Trying to compare objects with different behavior.") msg << " From object with 'order_matters: #{this.order_matters?}' and 'uniq: #{this.uniq?}'." if this msg << " To object where 'order_matters: #{that.order_matters?}' and 'uniq: #{that.uniq?}'." if that super(msg) end end include Enumerable class << self attr_accessor :order_matters, :uniq # @param a [ArrayModel] # @param b [ArrayModel] # @return [Boolean] `true` if both elements have same behaviour def same_type?(a, b) # rubocop:disable Naming/MethodParameterName msg = "To use this comparison both objects should be `ArrayModel`" raise msg unless a.is_a?(ArrayModel) && b.is_a?(ArrayModel) (a.order_matters? == b.order_matters?) && (a.uniq? == b.uniq?) end end inheritable_class_vars :order_matteres, :uniq def initialize(doc = [], parent: self, key: nil, read_only: self.class.read_only?) super(doc, parent: parent, key: key, read_only: read_only) end def order_matters? self.class.order_matters end def uniq? self.class.uniq end def length count end def empty? count&.zero? end def present? count&.positive? end def each(&block) return to_enum(:each) unless block _items.each(&block) end # @return [Array] the array element represented by this object def _items replace_doc([]) unless doc.is_a?(Array) doc.tap {|d| d.uniq! if uniq?} end def to_s(delimiter: "|") map(&:to_s).join(delimiter) end # @see #_items # @return [Array] a **copy** of the `Array` elements def to_a _items.slice(0..-1) end # @param other [Object, Array, ArrayModel] the value(s) of the new object # @return [ArrayModel] a new object with the current class def new_from(other) self.class.new(into_a(other)) end # @return [ArrayModel] a copy of the current object def dup new_from(to_a) end # @return [Integer] the position of the element in the `Array` def index(value) _items.index(value) end # Retrieves the element of a certain position # @param pos [Integer] the position of the element # @return [Date, String, Number] def [](pos) _items[pos] end # Sets the element of a certain position # @param pos [Integer] the position of the element # @param value [String, Date, Number] the element # @return [Date, String, Number] def []=(pos, value) _items[pos] = value on_change self[pos] end # Compares with an `Array` or another `ArrayModel` # @param other [ArrayModel, Array] def ==(other) return true if equal?(other) return false unless (other.class == self.class) || other.is_a?(Array) case other when Array self == new_from(other) when ArrayModel raise TypeMismatchedComparison.new(this: self, that: other) unless self.class.same_type?(self, other) if order_matters? _items == other.to_a else (_items - other.to_a).empty? && (other.to_a - _items).empty? end end end # @return [Boolean] `true` if `value` is present, `false` otherwise def include?(value) _items.include?(value) end def include_any?(*value) value.any? {|v| _items.include?(v)} end def include_all?(*value) value.all? {|v| _items.include?(v)} end # Adds an element to the subjacent `Array` # @note if the class variable `uniq` is `true`, it skips duplicates def <<(value) _items.concat(into_a(value)).tap do |doc| doc.uniq! if uniq? end on_change self end # @see #<< def push!(value) self << value end # @see #<< # @note same as #push! but for multiple elements def concat!(values) self << values end # Resets the `Array` by keeping its reference and adds the value(s) # @param other [Object, Array, ArrayModel] the value(s) to be added def <(other) _items.clear self << other end # Clears the `Array` keeping its reference def clear! _items.clear on_change self end # Concat to new def +(other) new_from(to_a + into_a(other)) end # Join # @param other [Object, Array, ArrayModel] the value(s) to be joined # @return [ArrayModel] a new object instance with the intersection done def |(other) oth = new_from(other) - self new_from(to_a + oth.to_a) end # Intersect # @param other [Object, Array, ArrayModel] the value(s) to be deleted # @return [ArrayModel] a new object instance with the intersection done def &(other) dup.tap do |out| dup.tap do |delta| delta.delete!(*into_a(other)) out.delete!(*into_a(delta)) end end end # Subtract # @param value [Object, Array, ArrayModel] the value(s) to be deleted # @return [ArrayModel] a **copy** of the object with the elements subtracted def -(other) dup.tap do |copy| copy.delete!(*into_a(other)) end end # Deletes `values` from the `Array` def delete!(*values) values.map do |v| deletion!(v) end.tap do |_r| on_change end end # Swaps two values' positions # @note this will work with first instances when **not** `uniq?` # @param val1 [Object] the first value to swap # @param val2 [Object] the second value to swap # @return [Integer] the new of `value1`, `nil` if it wasn't moved def swap(val_1, val_2) index(val_2).tap do |dest| if dest && (pos = index(val_1)) _items[dest] = val_1 _items[pos] = val_2 end end end def insert_one(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED) idx = index(value) return idx if idx && uniq? pos = if used_param?(pos) && pos pos elsif used_param?(before) && before index(before) elsif used_param?(after) && after if (idx = index(after)) then idx + 1 end end # use last position as default pos ||= length pos.tap do |_i| _items.insert(pos, value) on_change end end # TODO def move(value, pos: NOT_USED, _before: NOT_USED, _after: NOT_USED) return unless (idx = index(value)) on_change unless idx == pos pos end protected def on_change # to be overriden by child classes end private def into_a(value) raise "Can't convert to 'Array' a 'Hash', as is a key_value pair Enumerable" if value.is_a?(Hash) return value.to_a.slice(0..-1) if value.is_a?(Enumerable) [].push(value).compact end def deletion!(value) return _items.delete(value) if uniq? return unless (idx = _items.index(value)) _items.slice!(idx) end end end end end end