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