# rubocop:disable Style/Attr module Eco module API module Common module People # Class meant to offer a _collection_ of entries, normally used to get parsed input data. # @attr_reader entries [Array] the entries that match the same search criterion. # @param property [String] the property of the entry model that triggered the error # (base of the search criterion). def initialize(msg, candidates: [], property: "email") @candidates = candidates @property = property super("#{msg} #{candidates_summary}") end # @param with_index [Boolean] to add an index to each candidate description. # @return [Array] the `candidates` identified def identify_candidates(with_index: false) candidates.map.each_with_index do |entry, i| index = with_index ? "#{i}. " : "" "#{index} #{entry.identify}" end end # @return [Person] the `candidate` in the `index` position def candidate(index) candidates[index] end private def candidates_summary lines = ["The following entries have the same '#{property}':"] lines.concat(identify_candidates(with_index: true)).join("\n ") end end # build the shortcuts of Collection attr_collection :id, :external_id, :email, :name, :supervisor_id alias_method :entries, :to_a def initialize(data = [], klass:, factory:) super(data, klass: klass, factory: factory) @caches_init = false end # @!group Main identifier helpers def id(*args) attr('id', *args).first end def external_id(*args) attr('external_id', *args).first end def [](id_or_ext) id(id_or_ext) || external_id(id_or_ext) end # @!endgroup # @!group Special filters def filter_tags_any(tags) attr("filter_tags", tags, default_modifier.any.insensitive) end def filter_tags_all(tags) attr("filter_tags", tags, default_modifier.all.insensitive) end def policy_group_ids_any(ids) attr("policy_group_ids", ids, default_modifier.any.insensitive) end def policy_group_ids_all(ids) attr("policy_group_ids", ids, default_modifier.all.insensitive) end # @!endgroup # @!group Searchers # Search function to find an `entry` based on one of different options # It searches an entry using the parameters given. # @note This is how the search function actually works: # 1. if eP `id` is given, returns the entry (if found), otherwise... # 2. if `external_id` is given, returns the entry (if found), otherwise... # 3. if `strict` is `false` and `email` is given: # - if there is only 1 entry with that email, returns that entry, otherwise... # - if found but, there are many candidate entries, it raises MultipleSearchResults error # - if entry `external_id` matches `email`, returns that entry # @raise MultipleSearchResults if there are multiple entries with the same `email` # and there's no other criteria to find the entry. It only gets to this point if # `external_id` was **not** provided and we are **not** in 'strict' search mode. # However, it could be we were in `strict` mode and `external_id` was not provided. # @param id [String] the `internal id` of the person # @param external_id [String] the `exernal_id` of the person # @param email [String] the `email` of the person # @param strict [Boolean] if should perform a `:soft` or a `:strict` search. `strict` # will avoid repeated email addresses. # @return [Entry, nil] the entry we were searching, or `nil` if not found. def entry(id: nil, external_id: nil, email: nil, strict: false) init_caches # normalize values ext_id = !external_id.to_s.strip.empty? && external_id.strip email = !email.to_s.strip.empty? && email.downcase.strip e = nil e ||= @by_id[id]&.first e ||= @by_external_id[ext_id]&.first e ||= entry_by_email(email) unless strict && ext_id e end # Search function to find an `entry` based on one of different options # see Eco::API::Common::People::Entries#entry def find(object, strict: false) id = attr_value(object, "id") external_id = attr_value(object, "external_id") email = attr_value(object, "email") entry(id: id, external_id: external_id, email: email, strict: strict) end # @!endgroup # @!group Basic Collection Methods # Override `each` to make it work with supervisor hiearchy def each(&block) return to_enum(:each) unless block @items.each(&block) end def exclude(object) exclude_people(into_a(object)) end def exclude_people(list) discarded = list.map do |person| find(person) end.compact newFrom to_a - discarded end # TODO: it should rather use the the people-to-csv case somehow # Helper to dump the entries into a CSV # @param filename [String] the destination file def export(filename) header = each_with_object([]) do |entry, hds| hds.push(*entry.internal_entry.keys).uniq! end CSV.open(filename, "w") do |csv| csv << header each do |entry| csv << entry.internal_entry.values_at(*header) end end end # @!group Groupping methods def email_id_maps email_present.group_by(:email).transform_values(&:id) end def group_by_supervisor to_h(:supervisor_id) end def to_h(attr = "id") super(attr || "id") end # @!endgroup protected def on_change @caches_init = false end private def entry_by_email(email, prevent_multiple_match: false) return nil unless email candidates = @by_email[email] || [] return candidates.first if candidates.length == 1 if prevent_multiple_match && !candidates.empty? msg = "Multiple search results match the criteria." raise MultipleSearchResults.new(msg, candidates: candidates, property: "email") end @by_external_id[email]&.first end def init_caches return if @caches_init @by_id = no_nil_key(to_h) @by_external_id = no_nil_key(to_h('external_id')) @by_email = no_nil_key(to_h('email')) @caches_init = true end def no_nil_key(hash) hash.tap {|h| h.delete(nil)} end end end end end end # rubocop:enable Style/Attr