lib/eco/api/common/people/person_entry.rb in eco-helpers-0.6.17 vs lib/eco/api/common/people/person_entry.rb in eco-helpers-0.7.1

- old
+ new

@@ -1,377 +1,318 @@ module Eco module API module Common module People class PersonEntry - @@cached_warnings = {} + # This class is meant to provide a common interface to access entries of source data that come in different formats. + # @note + # - if `data` is a `Person` object, its behaviour is `serialise`. + # - if `data` is **not** a `Person` object, it does a `parse`. + # - currently **in rework**, so there may be subtle differences that make it temporarily unstable (yet it is reliable). + # @param data [Hash, Person] `Person` object to be serialized or hashed entry (`CSV::Row` is accepted). + # @param person_parser [Common::People::PersonParser] parser/serializer of person attributes (it contains a set of attribute parsers). + # @param attr_map [Eco::Data::Mapper] mapper to translate attribute names from _external_ to _internal_ names and _vice versa_. + # @param dependencies [Hash] hash where _keys_ are internal attribute names. It is mostly used to deliver final dependencies to attribute parsers/serializers. + # @param logger [Common::Session::Logger, ::Logger] object to manage logs. + def initialize(data, person_parser:, attr_map:, dependencies: {}, logger: ::Logger.new(IO::NULL)) + raise "Constructor needs a PersonParser. Given: #{parser}" if !person_parser.is_a?(Eco::API::Common::People::PersonParser) + raise "Expecting Mapper object. Given: #{attr_map}" if attr_map && !attr_map.is_a?(Eco::Data::Mapper) - 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 + @person_parser = person_parser + @deps = dependencies + @logger = logger + @attr_map = attr_map + @emap = PersonEntryAttributeMapper.new(@source, person_parser: @person_parser, attr_map: @attr_map, logger: @logger) - @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 + @external_entry = data + @serialized_entry = mapped_entry(@external_entry) + @internal_entry = internal_entry(@serialized_entry) + else # SERIALIZING + @person = data + @internal_entry = internal_entry(@person) + @serialized_entry = mapped_entry(@internal_entry) + #@external_entry = external_entry 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 + # To know if currently the object is in parse or serialize mode. + # @return [Boolean] returns `true` if we are **parsing**, `false` otherwise. def parsing? - !is_person?(@source) + !@source.is_a?(Ecoportal::API::Internal::Person) end + # To know if currently the object is in parse or serialize mode. + # @return [Boolean] returns `true` if we are **serializing**, `false` otherwise. def serializing? !parsing? end - # SHORTCUTS - + # @return [String, nil] the _internal id_ of this person if defined. def id - @int_row["id"] + @internal_entry["id"] end + # @return [String, nil] the _external id_ of this person if defined. def external_id - @int_row["external_id"] + @internal_entry["external_id"] end + # @return [String, nil] the _name_ of this person if defined. def name - @int_row["name"] + @internal_entry["name"] end + # @return [String, nil] the _email_ of this person if defined. def email - @int_row["email"] + @internal_entry["email"] end + # @return [String, nil] the _supervisor id_ of this person if defined. def supervisor_id - @int_row["supervisor_id"] + @internal_entry["supervisor_id"] end def supervisor_id=(value) - @int_row["supervisor_id"] = value + @internal_entry["supervisor_id"] = value end + # Provides a reference of this person. + # @return [String] string summary of this person identity. 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| + @internal_entry.each.map do |k, v| "'#{k}': '#{v.to_json}'" end.join(" | ") end end - # SETTERS + # Setter to fill in all the `core` properties of the `Person` that are present in the `Entry`. + # @note it only sets those core properties defined in the entry. + # Meaning that if an core property is not present in the entry, this will not be set on the target person. + # @param person [Person] the person we want to set the core values to. + # @param exclude [String, Array<String>] core attributes that should not be set/changed to the person. def set_core(person, exclude: nil) - scoped_attrs = @core_attrs - into_a(exclude) - @int_row.slice(*scoped_attrs).each do |attr, value| + scoped_attrs = @emap.core_attrs - into_a(exclude) + @internal_entry.slice(*scoped_attrs).each do |attr, value| set_to_core(person, attr, value) end end + # Setter to fill in all the schema `details` fields of the `Person` that are present in the `Entry`. + # @note it only sets those details properties defined in the entry. + # Meaning that if an details property is not present in the entry, this will not be set on the target person. + # @param person [Person] the person we want to set the schema fields' values to. + # @param exclude [String, Array<String>] schema field attributes that should not be set/changed to the person. 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| + person.add_details(@person_parser.schema) if !person.details || !person.details.schema_id + scoped_attrs = @emap.details_attrs - into_a(exclude) + @internal_entry.slice(*scoped_attrs).each do |attr, value| set_to_details(person, attr, value) end end + # Setter to fill in the `account` properties of the `Person` that are present in the `Entry`. + # @note it only sets those account properties defined in the entry. + # Meaning that if an account property is not present in the entry, this will not be set on the target person. + # @param person [Person] the person we want to set the account values to. + # @param exclude [String, Array<String>] account properties that should not be set/changed to the person. 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| + scoped_attrs = @emap.account_attrs - into_a(exclude) + @internal_entry.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 + # Entry represented in a `Hash` with **external** attribute names and values thereof. + # @note normally used to obtain a **serialized entry**. + # @return [Hash] with **external** names and values. + def to_hash + external_entry 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 + # Entry represented in a `Hash` with **external** attribute names and values thereof. + # @note normally used to obtain a **serialized entry**. + # @return [Hash] with **external** names and values. + def external_entry + @emap.all_attrs.each_with_object({}) do |attr, hash| + unless hash.key?(ext_attr = @emap.to_external(attr)) + hash[ext_attr] = @serialized_entry[attr] + end + end end private - def set_to_core (person, attr, value) + 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) + 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 = @person_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) + 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 + unless field = person.details.get_field(attr) + fatal("Field '#{attr}' does not exist in details of schema: '#{person.details.schema_id}'") + end + value = nil if value.to_s.empty? + value = @person_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}) + if @person_parser.defined?(field.type.to_sym) + value = @person_parser.parse(field.type.to_sym, value, deps: {"attr" => attr}) end person.details[attr] = value end - def get_from_core (person, attr) + def get_from_core (person, attr) person.send(attr) end def get_from_account (person, attr) - return "-" if !person.account + return nil if !person.account multiple = ["policy_group_ids", "filter_tags"].include?(attr) value = person.account.send(attr) - @parser.serialize(:multiple, value) if multiple + @person_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 + def get_from_details(person, attr) + return nil if !person.details || !person&.details&.schema_id + unless field = person.details.get_field(attr) + fatal("Field '#{attr}' does not exist in details of schema: '#{person.details.schema_id}'") + end value = person.details[attr] - value = @parser.serialize(:date, value) if field.type == "date" - value = @parser.serialize(:multiple, value) if field.multiple + value = @person_parser.serialize(:date, value) if field.type == "date" + value = @person_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 + # To obtain an entry with internal names but external values. + # @param data [Hash] external or raw entry (when parsing) or internal or parsed entry (when serializing). + # @return [Hash] 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)]] + # Parsing helper that aliases attribute names (from internal to external names) + # @note **Parse**: here we aliase internal attribute names into external ones. + # @param ext_entry [Hash] entry in raw, with **external** names and values. + # @return [Hash] entry with **internal names** and **external values**. + def aliased_entry(ext_entry) + aliased_hash = @emap.aliased_attrs.map do |attr| + [attr, ext_entry[@emap.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) + ext_entry.slice(*@emap.direct_attrs).merge(aliased_hash) end def hash_attr(attr, value) return value if value.is_a?(Hash) { attr => value } end - # serializing: run serializers (overrides existing keys) + # Serializing helper that serializes values (from internal to external values). + # @note **Serializing**: + # 1. here we tranform internal into external **values**. + # 2. when running the serializers, it overrides existing keys. + # @param unserialized_entry [Hash] entry with **internal** names and values. + # @return [Hash] entry with **internal names** and **external values**. def serialized_entry(unserialized_entry) - serial_attrs = @parser.defined_attrs.reduce({}) do |serial_hash, attr| + serial_attrs = @person_parser.defined_attrs.reduce({}) do |serial_hash, attr| deps = @deps[attr] || {} - serial_attr = @parser.serialize(attr, @person, deps: deps) + serial_attr = @person_parser.serialize(attr, @person, deps: deps) serial_hash.merge(hash_attr(attr, serial_attr)) end unserialized_entry.merge(serial_attrs) end - # returns entry with internal names and values + # To obtain an entry with internal names but external values. + # @param data [Hash, Ecoportal::API::V1::Person] alised_entry (when parsing) or person (when serializing). + # @return [Hash] the `internal entry` with the **internal** attributes names and values. def internal_entry(data) return parsed_entry(data) if parsing? unserialized_entry(data) end - # parse: to internal entry (parse values) + # Parsing helper that just **parses the values** that have a parser/serializer defined. + # @param aliased_entry [Hash] the entry with the _internal attribute_ names but the _external values_. + # @return [Hash] the `internal entry` with the **internal** attributes names and values. def parsed_entry(aliased_entry) - parsed = @parser.defined_attrs.map do |attr| - value = @parser.parse(attr, aliased_entry) + parsed = @person_parser.defined_attrs.map do |attr| + value = @person_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) + # Serializing helper that just creates the _internal entry_ out of a `Person` object. + # @note + # - when unnesting attributes, the overriding precedence for collisions is + # - `core` -> _overrides_ -> `account` -> _overrides_ -> `details` + # - to keep things consistent, the `internal entry` hash has keys in this order: + # - `core`, `account`, `details`. + # @param person [Ecoportal::API::V1::Person] the `Person` object to transform into an _internal entry_. + # @return [Hash] the `internal entry` with the **internal** attributes names and values. def unserialized_entry(person) - core_hash = @parser.target_attrs_core.reduce({}) do |hash, attr| + core_hash = @person_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| + details_hash = @person_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| + account_hash = @person_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) + # merge by core overriding account and details + rh = details_hash.merge(account_hash).merge(core_hash) + # resort hash keys + sorted_keys = core_hash.keys | account_hash.keys | details_hash.keys + sorted_keys.reduce({}) {|h,k| h[k] = rh[k]; h} 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