module Eco module API module Common module People class PersonEntry # 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) @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) if parsing? @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 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? !@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 # @return [String, nil] the _internal id_ of this person if defined. def id @internal_entry["id"] end def id? @internal_entry.key?("id") end # @return [String, nil] the _external id_ of this person if defined. def external_id @internal_entry["external_id"] end def external_id? @internal_entry.key?("external_id") end # @return [String, nil] the _name_ of this person if defined. def name @internal_entry["name"] end def name? @internal_entry.key?("name") end # @return [String, nil] the _email_ of this person if defined. def email @internal_entry["email"] end def email? @internal_entry.key?("email") end # @return [String, nil] the _supervisor id_ of this person if defined. def supervisor_id @internal_entry["supervisor_id"] end def supervisor_id=(value) @internal_entry["supervisor_id"] = value end def supervisor_id? @internal_entry.key?("supervisor_id") end def filter_tags @internal_entry["filter_tags"] end def filter_tags? @internal_entry.key?("filter_tags") 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 @internal_entry.each.map do |k, v| "'#{k}': '#{v.to_json}'" end.join(" | ") end end # 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] core attributes that should not be set/changed to the person. def set_core(person, exclude: nil) 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] schema field attributes that should not be set/changed to the person. def set_details(person, exclude: nil) 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] 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 = @emap.account_attrs - into_a(exclude) @internal_entry.slice(*scoped_attrs).each do |attr, value| _set_to_account(person, attr, value) end end # 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 # 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 def internal_entry @internal_entry end def doc return @person.doc if instance_variable_defined?(:@person) && @person core_attrs = @emap.core_attrs details_attrs = @emap.details_attrs account_attrs = @emap.account_attrs internal_entry.slice(*core_attrs).tap do |core_hash| unless details_attrs.empty? schema_id = @person_parser.schema.id details_fields = @person_parser.schema.doc["fields"].each_with_object([]) do |fld, flds| if details_attrs.include?(fld.alt_id) flds << fld.merge("value" => internal_entry[fld.alt_id]).slice("id", "alt_id", "type", "name", "shared", "multiple", "value") end end core_hash.merge!({ "details" => { "schema_id" => schema_id, "fields" => details_fields } }) end unless account_attrs.empty? account_hash = internal_entry.slice(*account_attrs) core_hash.merge!({ "account" => account_hash }) end end 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 = @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) return if !person.details 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 @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) person.send(attr) end def _get_from_account (person, attr) return nil if !person.account multiple = ["policy_group_ids", "filter_tags"].include?(attr) value = person.account.send(attr) value = @person_parser.serialize(:multiple, value) if multiple value end 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 = @person_parser.serialize(:date, value) if field.type == "date" value = @person_parser.serialize(:multiple, value) if field.multiple value end # MAPPED ENTRY (when and where applicable) # 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 # 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 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 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 = @person_parser.defined_attrs.reduce({}) do |serial_hash, attr| deps = @deps[attr] || {} serial_attr = @person_parser.serialize(attr, @person, deps: deps) serial_hash.merge(hash_attr(attr, serial_attr)) end unserialized_entry.merge(serial_attrs) end # 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 # 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 = @person_parser.active_attrs(aliased_entry).map do |attr| value = @person_parser.parse(attr, aliased_entry) [attr, value] end.to_h aliased_entry.merge(parsed) end # 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 = @person_parser.target_attrs_core.reduce({}) do |hash, attr| value = _get_from_core(person, attr) hash.merge(hash_attr(attr, value)) end 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 = @person_parser.target_attrs_account.reduce({}) do |hash, attr| value = _get_from_account(person, attr) hash.merge(hash_attr(attr, value)) end # 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 # LOGGER def logger @logger || ::Logger.new(IO::NULL) 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 end end end end end