# rubocop:disable Style/OptionalBooleanParameter module Eco module Language module Models class Collection include Enumerable BASIC_METHODS = %w[present empty present_all? present_some?].freeze EXTENDED_METHODS = BASIC_METHODS + %w[exclude remove attr attr? attrs unique_attrs contains] class << self def attr_presence(*attrs) block = ->(method) { attrs_create_method(attrs, method) } BASIC_METHODS.each(&block) end def attr_collection(*attrs) block = ->(method) { attrs_create_method(attrs, method) } EXTENDED_METHODS.each(&block) end def attrs_create_method(attrs, method) attrs.each do |attr| attr = attr.to_s if method.include?("attr") attr_method = method.sub("attr", attr) else attr_method = "#{attr}_#{method}" end define_method attr_method do |*args| send(method, attr, *args) end end end end def initialize(data = [], klass:, factory: nil, handy: Eco::Assets::Language.new) raise "Raise klass required, given: #{klass}" unless klass @klass = klass @factory = factory @handy = handy @items = to_klass(data) end # @!group pure collection methods def to_c Collection.new(self, klass: @klass, factory: @factory) end def new newFrom to_a end def newFrom(data) # rubocop:disable Naming/MethodName self.class.new(data, klass: @klass, factory: @factory) end def merge(data) data = data.to_a unless data.is_a?(Array) newFrom to_a + data end def length count end def empty? count.zero? end def each(&block) return to_enum(:each) unless block @items.each(&block) end def <(other) @items.clear self << other end def <<(other) @items.concat(into_a(other)) on_change self end def update(&block) newFrom map(&block) end def delete!(value) self < @items - into_a(value) end # @!endgroup # @!group `attr` dependant methods def exclude(attr, value, modifier = default_modifier) newFrom @items - self.attr(attr, value, modifier) end def remove(attr, value, modifier = default_modifier) self < exclude(attr, value, modifier) end def attr(attr, value = true, modifier = default_modifier) return present(attr, value) if boolean?(value) select do |object| match?(attr_value(object, attr), value, modifier) end.then do |matching| newFrom matching end end def attr?(attr, value = true, modifier = default_modifier) return present(attr, value).length == length if boolean?(value) match?(attrs(attr), value, modifier.new.reverse) end def contains(attr, value, modifier = default_modifier) self.attr(attr, value, modifier.new.pattern) end def attrs(attr) map { |object| attr_value(object, attr) } end def unique_attrs(attr) to_h(attr).keys end def group_by(attr = nil, &block) return to_h(attr) if attr to_a.group_by(&block) if block end # By a specific `attr` or a block # @note either one or the other should be present def to_h(attr, &block) return to_a.group_by(&block) if block raise "And attr or a block are required. Given attr: #{attr}" unless attr to_a.group_by { |object| object.method(attr).call } end # @!endgroup # @!group `attr` presence methods def present(attr, flag = true) block = ->(o) { attr_value_present?(o, attr) == !!flag } # rubocop:disable Style/DoubleNegation newFrom select(&block) end def empty(attr, flag = true) present(attr, !flag) end def present_all?(attr, flag = true) present(attr, flag).length == length end def present_some?(attr, flag = true) present(attr, flag).length.positive? end # @!endgroup protected def on_change # function to be overriden by children classes end def into_a(value) value = [].push(value) if value.is_a?(Hash) || !value.is_a?(Enumerable) value.to_a end private def attr_value(obj, attr) return nil unless obj && attr if obj.is_a?(Hash) obj[attr] elsif obj.respond_to?(attr.to_sym) obj.send(attr) end end def attr_value_present?(obj, attr) return false unless (value = attr_value(obj, attr)) if value.is_a?(Enumerable) value.count > 1 elsif value.is_a?(String) !value.strip.empty? else !value.nil? end end def match?(*args) @handy.match?(*args) end def to_klass(list) into_a(list).map do |v| v.is_a?(@klass) ? v : @factory&.new(v) || @klass.new(v) end end def default_modifier Language::MatchModifier.new end def boolean?(value) value == !!value end end end end end # rubocop:enable Style/OptionalBooleanParameter