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 < Exception def initialize (this: nil, that: msg = "Trying to compare objects with different behavior.") if this msg += " From object with 'order_matters: #{this.order_matters?}' and 'uniq: #{this.uniq?}'." end if that msg += " To object where 'order_matters: #{that.order_matters?}' and 'uniq: #{that.uniq?}'." end 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) raise "To use this comparison both objects should be `ArrayModel`" unless a.is_a?(ArrayModel) && b.is_a?(ArrayModel) (a.order_matters? == b.order_matters?) && (a.uniq? == b.uniq?) end end def initialize(doc = {}, parent: self, key: nil) super(doc, parent: parent, key: key) end def order_matters?; self.class.order_matters; end def uniq?; self.class.uniq; end def length; count; end def empty?; count == 0; end def present?; count > 0; 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 # @see #_items # @return [Array] a **copy** of the `Array` elements def to_a _items.slice(0..-1) end # @param value [Object, Array, ArrayModel] the value(s) of the new object # @return [ArrayModel] a new object with the current class def new_from(value) self.class.new(into_a(value)) 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 []=(post, value) _items[pos] = value on_change self[pos] end # Compares with an `Array` or another `ArrayModel` # @param a [ArrayModel, Array] def ==(a) return true if self.equal?(a) return false unless (a.class == self.class) || a.is_a?(Array) case a when Array self == new_from(a) when ArrayModel return true if raise TypeMismatchedComparison.new(this: self, that: a) unless self.class.same_type?(self, a) if self.order_matters? _items == a.to_a else (_items - a.to_a).empty? && (a.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 value [Object, Array, ArrayModel] the value(s) to be added # @param values [Array] def <(values) _items.clear self << values end # Clears the `Array` keeping its reference def clear! _items.clear on_change self end # Concat to new def +(value) new_from(self.to_a + into_a(value)) end # Join # @param value [Object, Array, ArrayModel] the value(s) to be joined # @return [ArrayModel] a new object instance with the intersection done def |(value) new = new_from(value) - self new_from(to_a + new.to_a) end # Intersect # @param value [Object, Array, ArrayModel] the value(s) to be deleted # @return [ArrayModel] a new object instance with the intersection done def &(value) self.dup.tap do |out| self.dup.tap do |delta| delta.delete!(*into_a(value)) 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 -(value) self.dup.tap do |copy| copy.delete!(*into_a(value)) 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(value1, value2) index(value2).tap do |dest| if dest && pos = index(value1) _items[dest] = value1 _items[pos] = value2 end end end def insert_one(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED) i = index(value) return i if (i && uniq?) pos = case when used_param?(pos) pos when used_param?(before) index(before) when used_param?(after) if i = index(after) i + 1 end else length end pos.tap do |i| unless !i _items.insert(pos, value) on_change end end end # TODO def move(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED) if i = index(value) unless i == pos on_change end pos end 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) if !uniq? if i = _items.index(value) _items.slice!(i) end else _items.delete(value) end end end end end end end