module Eco::API::Common::People class DefaultParsers module Helpers module ExpectedHeaders include Eco::Language::AuxiliarLogger private def require_headers!(raw_headers) abort("Missing headers in CSV") unless raw_headers&.any? empty = [] raw_headers.each_with_index do |header, idx| empty << idx if header.to_s.strip.empty? end abort("Empty headers in column(s): #{empty.join(', ')}") if empty.any? true end def check_headers!(raw_headers, order_check: false) # rubocop:disable Metrics/AbcSize unmatch = [] unmatch = unmatched_headers(raw_headers) if order_check missing = missing_headers(raw_headers) unknown = unknown_headers(raw_headers) criteria = [unknown, missing[:direct], missing[:indirect], unmatch] return if criteria.all?(&:empty?) msg = "Detected possible HEADER / FIELD ISSUES !!!\n" # requires exact match unless unmatch.empty? msg << "File headers/fields do NOT exactly match the expected:\n" msg << " * Expected: #{expected_headers}\n" expected, given = unmatch.first msg << " * First unmatch => Given: '#{given}' where expected '#{expected}'\n" missed = expected_headers - raw_headers unless missed.empty? msg << " * Missing headers/fields:\n" msg << " - #{missed.join("\n - ")}\n" end end msg << "Missing or Wrong HEADER names in the file:\n" msg << " * UNKNOWN (or not used?): #{unknown}\n" unless unknown.empty? msg << " * MISSING HEADER/FIELD: #{missing[:direct]}\n" unless missing[:direct].empty? unless (data = missing[:indirect]).empty? msg << " * MISSING INDIRECTLY:\n" data.each do |ext, info| msg << " - '#{ext}' => " msg << (info[:attrs] || {}).map do |status, attrs| if status == :inactive "makes inactive: #{attrs}" elsif status == :active "there could be missing info in: #{attrs}" end end.compact.join("; ") msg << "\n" end end log(:warn) { msg } msg = "There were issues identified on the file header/field names. Aborting..." abort(msg) if options.dig(:input, :header_check, :must_be_valid) sleep(2) end # @return [Hash] with missing `:direct` and `:indirect` attrs, where # - `:direct` [Array] refers to direct attrs # - `:indirect` [Hash] refers to indirect attrs that are `:active` or `:inactive`. def missing_headers(raw_headers) # rubocop:disable Metrics/AbcSize int_head = internal_present_or_active(raw_headers) external = raw_headers.select do |e| i = fields_mapper.to_internal(e) int_head.include?(i) end ext_present = present_internal_expected_headers(int_head) | external ext_miss = expected_headers - ext_present { direct: [], indirect: {} }.tap do |missing| ext_miss.each do |ext| next unless (int = fields_mapper.to_internal(ext)) missing[:direct] << ext if all_internal_attrs.include?(int) related_attrs_requirements = required_attrs.values.select do |req| dep = req.dependant?(int) affects = dep && !int_head.include?(int) in_header = int_head.include?(req.attr) affects || (dep && !in_header) end next if related_attrs_requirements.empty? data = missing[:indirect][ext] = {} data[:int] = int data[:attrs] = {} related_attrs_requirements.each_with_object(data[:attrs]) do |req, attrs| status = req.active?(*int_head) ? :active : :inactive attrs[status] ||= [] attrs[status] << req.attr end end end end # The input file header names as expected def expected_headers @expected_headers ||= fields_mapper.list(:external).compact.uniq end def present_internal_expected_headers(internal_headers) expected_headers.select do |ext| int = fields_mapper.to_internal(ext) internal_headers.include?(int) end end def unmatched_headers(raw_headers) expected_headers.zip(raw_headers).reject do |(expected, given)| expected == given end end def unknown_headers(raw_headers) (raw_headers - expected_headers) - all_internal_attrs end def fields_mapper session.fields_mapper end def required_attrs @required_attrs ||= person_parser.required_attrs.to_h {|ra| [ra.attr, ra]} end def all_internal_attrs @all_internal_attrs ||= [].tap do |int_attrs| known_int_attrs = person_parser.all_attrs(include_defined_parsers: true) known_int_attrs |= fields_mapper.list(:internal).compact int_attrs.concat(known_int_attrs) end end # Scopes what internal attrs appear in headers as they are def internal_present_or_active(raw_headers, inactive_requirements = {}) # rubocop:disable Metrics/AbcSize # internal attrs that are not being mapped int_all = all_internal_attrs.reject {|i| fields_mapper.external?(i)} hint = raw_headers & int_all hext = raw_headers - hint int_present = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact update_inactive = proc do inactive_requirements.dup.each do |attr, req| next unless req.active?(*int_present) inactive_requirements.delete(attr) int_present << attr update_inactive.call end end required_attrs.each_value do |req| next if int_present.include?(req) if req.active?(*int_present) inactive_requirements.delete(req.attr) int_present << req.attr update_inactive.call else inactive_requirements[req.attr] = req end end int_present end def person_parser session.entry_factory.person_parser end def abort(msg) super(msg, raising: false) if defined?(super) end end end end end