# frozen_string_literal: true require 'integrity/checksum' # Provides helper methods to compute footprints class FootprintService class << self # Compute the footprint # @param klass {Class} a class inheriting from Footprintable # @param item {Footprintable} an instance of the provided class def compute_footprint(klass, item) Integrity::Checksum.text(FootprintService.footprint_data(klass, item)) end # Return the original data string used to compute the footprint # @param klass {Class} a class inheriting from Footprintable # @param item {Footprintable} an instance of the provided class # @param array {Boolean} if true, the result is return on the form of an array, otherwise a concatenated string is returned def footprint_data(klass, item, array: false) raise TypeError unless item.is_a? klass sort_on = item.sort_on_field previous = klass.where("#{sort_on} < ?", item[sort_on]) .order("#{sort_on} DESC") .limit(1) columns = FootprintService.footprint_columns(klass) columns = columns.map do |c| comparable(item[c]) rescue ActiveModel::MissingAttributeError nil end res = columns.push(previous.first ? previous.first.footprint : '') array ? res.map(&:to_s) : res.join.to_s end # Return an ordered array of the columns used in the footprint computation # @param klass {Class} a class inheriting from Footprintable def footprint_columns(klass) %w[id].concat(klass.columns.map(&:name).delete_if { |c| %w[id footprint updated_at].concat(klass.columns_out_of_footprint).include? c }.sort) end # Logs a debugging message to help finding why a footprint is invalid # @param klass {Class} a class inheriting from Footprintable # @param item {Footprintable} an instance of the provided class def debug_footprint(klass, item) columns = FootprintService.footprint_columns(klass) current = FootprintService.footprint_data(klass, item, array: true) saved = FootprintDebug.find_by(footprint: item.footprint, klass: klass.name) return saved if Rails.env.test? if saved.nil? puts "Debug data not found for #{klass} [ id: #{item.id} ]" else puts "Debug footprint for #{klass} [ id: #{item.id} ]" puts '-----------------------------------------' puts "columns: [ #{columns.join(', ')} ]" puts "current: #{current}" puts " saved: #{saved.format_data(item.id)}" puts '-----------------------------------------' item.footprint_children.map(&:debug_footprint) end others = FootprintDebug.where('klass = ? AND data LIKE ? AND id != ?', klass, "#{item.id}%", saved&.id) puts "other possible matches IDs: #{others.map(&:id)}" puts '-----------------------------------------' end private # Return a comparable value for jsonb fields (with keys ordered alphabetically) def comparable(value) return value unless value.is_a? Hash value.sort.to_h end end end