module Eco module API module Common module People class PersonEntry @@cached_warnings = {} def initialize(data, parser:, mapper:, dependencies: {}, logger: ::Logger.new(IO::NULL)) raise "Constructor needs a PersonParser. Given: #{parser}" if !parser.is_a?(Eco::API::Common::People::PersonParser) raise "Expecting Mapper object. Given: #{mapper}" if mapper && !mapper.is_a?(Eco::Data::Mapper) @source = data @parser = parser @mapper = mapper @deps = dependencies @logger = logger if parsing? # PARSING @raw_entry = data init_attr_trackers @aliased_entry = mapped_entry(@raw_entry) @int_row = internal_entry(@aliased_entry) # internally named attrs scoped by mapper and parser against the current row @int_attrs = @int_row.keys else # SERIALIZING @person = data @int_row = internal_entry(@person) @aliased_entry = mapped_entry(@int_row) @int_attrs = @parser.all_attrs end @core_attrs = @parser.target_attrs_core(@int_attrs) @details_attrs = @parser.target_attrs_details(@int_attrs) @account_attrs = @parser.target_attrs_account(@int_attrs) end def parsing? !is_person?(@source) end def serializing? !parsing? end # SHORTCUTS def id @int_row["id"] end def external_id @int_row["external_id"] end def name @int_row["name"] end def email @int_row["email"] end def supervisor_id @int_row["supervisor_id"] end def supervisor_id=(value) @int_row["supervisor_id"] = value end def to_s(options) options = into_a(options) case when options.include?(:identify) "'#{name}' ('#{external_id}': '#{email}')" else @int_row.each.map do |k, v| "'#{k}': '#{v.to_json}'" end.join(" | ") end end # SETTERS def set_core(person, exclude: nil) scoped_attrs = @core_attrs - into_a(exclude) @int_row.slice(*scoped_attrs).each do |attr, value| set_to_core(person, attr, value) end end def set_details(person, exclude: nil) person.add_details(@parser.schema) if !person.details || !person.details.schema_id scoped_attrs = @details_attrs - into_a(exclude) @int_row.slice(*scoped_attrs).each do |attr, value| set_to_details(person, attr, value) end end def set_account(person, exclude: nil) person.account = {} if !person.account person.account.permissions_preset = nil unless person.account.permissions_preset = "custom" scoped_attrs = @account_attrs - into_a(exclude) @int_row.slice(*scoped_attrs).each do |attr, value| set_to_account(person, attr, value) end end # GETTERS def entry(klass: nil) return klass.new(to_hash) if klass to_hash end def to_hash @aliased_entry.each.map do |int_attr, v| [to_external(int_attr), v || ""] if to_external(int_attr) end.compact.to_h end private def set_to_core (person, attr, value) value = value&.downcase if attr == "email" person.send("#{attr}=", value&.strip) end def set_to_account (person, attr, value) return if !person.account multiple = ["policy_group_ids", "filter_tags"].include?(attr) if multiple value = @parser.parse(:multiple, value) value = value.map { |v| v&.upcase } if attr == "filter_tags" # preserve previous order current = into_a(person.account.send(attr)) value = (current & value) + (value - current) end person.account.send("#{attr}=", value) end def set_to_details (person, attr, value) return if !person.details field = person.details.get_field(attr) fatal("Field '#{attr}' does not exist in details of schema: '#{person.details.schema_id}'") if !field value = @parser.parse(:multiple, value) if field.multiple if @parser.defined?(field.type.to_sym) value = @parser.parse(field.type.to_sym, value, deps: {"attr" => attr}) end person.details[attr] = value end def get_from_core (person, attr) person.send(attr) end def get_from_account (person, attr) return "-" if !person.account multiple = ["policy_group_ids", "filter_tags"].include?(attr) value = person.account.send(attr) @parser.serialize(:multiple, value) if multiple end def get_from_details (person, attr) return "-" if !person.details || !person&.details&.schema_id field = person.details.get_field(attr) fatal("Field '#{attr}' does not exist in details of schema: '#{person.details.schema_id}'") if !field value = person.details[attr] value = @parser.serialize(:date, value) if field.type == "date" value = @parser.serialize(:multiple, value) if field.multiple value end # INIT # when parsing: def init_attr_trackers # internal <-> external attributes int_aliased = @parser.all_attrs.select { |attr| to_external(attr) } ext_alias = int_aliased.map { |attr| to_external(attr) } # virtual attrs (non native internal attr that require aliasing): ext_vi_aliased = attributes(@raw_entry).select do |attr| !ext_alias.include?(attr) && @mapper.external?(attr) end int_vi_aliased = ext_vi_aliased.map { |attr| @mapper.to_internal(attr) } @aliased_attrs = int_aliased + int_vi_aliased int_unlinked = @parser.undefined_attrs.select { |attr| !to_external(attr) } # those with parser or alias: int_linked = @parser.all_attrs - int_unlinked ext_aliased = ext_alias + ext_vi_aliased # those that are direct external to internal: ext_direct = attributes(@raw_entry) - ext_aliased # to avoid collisions between internal names: @direct_attrs = ext_direct - int_linked end # MAPPED ENTRY (when and where applicable) # return entry with internal names and external values def mapped_entry(data) return aliased_entry(data) if parsing? serialized_entry(data) end # parse: here we aliase attribute names def aliased_entry(raw_data) aliased_hash = @aliased_attrs.map do |attr| [attr, raw_data[to_external(attr)]] end.to_h direct_hash = @direct_attrs.map do |attr| [attr, raw_data[attr]] end.to_h aliased_hash.merge!(direct_hash) end def hash_attr(attr, value) return value if value.is_a?(Hash) { attr => value } end # serializing: run serializers (overrides existing keys) def serialized_entry(unserialized_entry) serial_attrs = @parser.defined_attrs.reduce({}) do |serial_hash, attr| deps = @deps[attr] || {} serial_attr = @parser.serialize(attr, @person, deps: deps) serial_hash.merge(hash_attr(attr, serial_attr)) end unserialized_entry.merge(serial_attrs) end # returns entry with interal names and values def internal_entry(data) return parsed_entry(data) if parsing? unserialized_entry(data) end # parse: to internal entry (parse values) def parsed_entry(aliased_entry) parsed = @parser.defined_attrs.map do |attr| value = @parser.parse(attr, aliased_entry) [attr, value] end.to_h aliased_entry.merge(parsed) end # serializing: to internal entry (create the internal entry out of a person) def unserialized_entry(person) core_hash = @parser.target_attrs_core.reduce({}) do |hash, attr| value = get_from_core(person, attr) hash.merge(hash_attr(attr, value)) end details_hash = @parser.target_attrs_details.reduce({}) do |hash, attr| value = get_from_details(person, attr) hash.merge(hash_attr(attr, value)) end account_hash = @parser.target_attrs_account.reduce({}) do |hash, attr| value = get_from_account(person, attr) hash.merge(hash_attr(attr, value)) end details_hash.merge(account_hash).merge(core_hash) #core_hash.merge(account_hash).merge(details_hash) end def to_internal(value) return value if !@mapper attr = value case value when Array return value.map do |v| to_internal(v) end.compact when String case when @mapper.external?(value) attr = @mapper.to_internal(value) when @mapper.external?(value.strip) unless cached_warning("external", "spaces", value) logger.warn("The external person field name '#{value}' contains additional spaces in the reference file") end attr = @mapper.to_internal(value.strip) when @mapper.internal?(value) || @mapper.internal?(value.strip) || @mapper.internal?(value.strip.downcase) unless cached_warning("external", "reversed", value) logger.warn("The mapper [external, internal] attribute names may be declared reversedly for EXTERNAL attribute: '#{value}'") end end end return nil unless @parser.all_attrs.include?(attr) end def to_external(value) return value if !@mapper attr = value case value when Array return value.map do |v| to_external(v) end.compact when String case when @mapper.internal?(value) attr = @mapper.to_external(value) when @mapper.internal?(value.strip) unless cached_warning("internal", "spaces", value) logger.warn("The internal person field name '#{value}' contains additional spaces in the reference file") end attr = @mapper.to_external(value.strip) when @mapper.external?(value) || @mapper.external?(value.strip) || @mapper.external?(value.strip.downcase) unless cached_warning("internal", "reversed", value) logger.warn("The mapper [external, internal] attribute names may be declared reversedly for INTERNAL attribute: '#{value}'") end end end return nil unless !@raw_entry || attributes(@raw_entry).include?(attr) attr end # LOGGER def logger @logger || ::Logger.new(IO::NULL) end def cached_warning(*args) unless exists = !!@@cached_warnings.dig(*args) args.reduce(@@cached_warnings) do |cache, level| cache[level] = {} if !cache.key?(level) cache[level] end end exists end def fatal(msg) logger.fatal(msg) exit end # HELPERS def into_a(value) value = [] if value == nil value = [].push(value) unless value.is_a?(Array) value end def attributes(value) case value when CSV::Row value&.headers when Hash value&.keys when PersonEntry @parser.target_attrs_core else [] end end def is_person?(value) value.is_a?(Ecoportal::API::Internal::Person) end end end end end end