module Eco module Data module Hashes class ArrayDiff extend Eco::Language::Models::ClassHelpers class << self def key(value = nil) return @key unless value @key = value.to_s end def key? !!@key end def compare(*attrs) compared_attrs.push(*attrs.map(&:to_s)).uniq! end def case_sensitive(value = nil) @case_sensitive = false unless instance_variable_defined?(:@case_sensitive) return @case_sensitive unless value @case_sensitive = !!value end def case_sensitive? !!@case_sensitive end def compared_attrs @compared_attrs ||= [] @compared_attrs end end attr_reader :source1, :source2 attr_reader :src_h1, :src_h2 attr_reader :logger class_resolver :diff_result_class, "Eco::Data::Hash::DiffResult" def initialize(source1, source2, logger: ::Logger.new(IO::NULL), **kargs) @logger = logger @options = kargs @source1 = source1 @source2 = source2 @src_h1 = by_key(source1) @src_h2 = by_key(source2) raise "Missing source1" unless !!self.src_h1 raise "Missing source2" unless !!self.src_h2 end # @note # - A `Hash::DiffResult` object, offers `hash_diff` with the attrs that have changed value # - It also allows to know the original value # @return [Hash] where `key` is the key of the record, and `value` a `DiffResult` object def diffs @diffs ||= source_results.select do |res| res.diff? end end # @return [Boolean] wheter or not there are differences. def diffs? !diffs.empty? end def source_results @source_results ||= paired_sources.each_with_object([]) do |(src1, src2), res| args = { key: key, compare: compared_attrs, case_sensitive: case_sensitive? } res << diff_result_class.new(src1, src2, **args) end end protected # It pairs the hashes of `source1` and `source2` # @note # - It also ensures they are in their Hash form (with string keys) # - This will merge entries of the same source that hold the same `key` attr value (latest wins) def paired_sources keys1 = src_h1.keys; keys2 = src_h2.keys all_keys = keys1 | keys2 all_keys.map {|key| [src_h1[key], src_h2[key]]} end def key @key ||= options_or(:key) do self.class.key end.tap do |k| raise "missing main key attr to pair records. Given: #{k}" unless k.is_a?(String) end end def case_sensitive? @case_sensitive ||= options_or(:case_sensitive) { self.class.case_sensitive? } end def compared_attrs @compared_attrs ||= options_or(:compared_attrs) do self.class.compared_attrs end.yield_self do |attrs| raise "compared_attrs should be an array" unless attrs.is_a?(Array) attrs.map(&:to_s) end end private def options_or(opt) opt = opt.to_sym return @options[opt] if @options.key?(opt) yield end def symbolize_keys(hash) hash.each_with_object({}) do |(k, v), h| h[k.to_sym] = v end end def stringify_keys(hash) hash.each_with_object({}) do |(k, v), h| h[k.to_s] = v end end def by_key(content) to_array_of_hashes(content).each_with_object({}) do |item, out| out[item[key]] = item end end def to_array_of_hashes(content) case content when Hash logger.error("Input data as 'Hash' not supported. Expecting 'Enumerable' or 'String'") exit(1) when String to_array_of_hashes(Eco::CSV.parse(content)) when Enumerable sample = content.to_a.first case sample when Hash, Array, ::CSV::Row Eco::CSV::Table.new(content).to_array_of_hashes else logger.error("Input content 'Array' of '#{sample.class}' is not supported.") exit(1) end else logger.error("Could not obtain any data out content: '#{content.class}'") exit(1) end end end end end end