module JSONSpectacular::DiffDescriptions

Public Class Methods

included(base) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 7
def self.included(base) # rubocop:disable Metrics/MethodLength
  base.class_eval do # rubocop:disable Metrics/BlockLength
    # Adds diff descriptions to the failure message until the all the nodes of the
    # expected and actual values have been compared, and all the differences (and the
    # paths to them) have been included.
    #
    # For Hashes and Arrays, it recursively calls itself to compare all nodes and
    # elements.
    #
    # @param [String, Number, Boolean, Array, Hash] actual_value current node of the
    #        actual value being compared to the corresponding node of the expected
    #        value
    # @param [String, Number, Boolean, Array, Hash] expected_value current node of
    #        the expected value being compared to the corresponding node of the
    #        actual value
    # @param [String] path path to the current nodes being compared, relative to the
    #        root full objects
    # @return void Diff descriptions are appended directly to message
    def add_diff_to_message(actual_value, expected_value, path = '')
      diffs_sorted_by_name = Hashdiff
                             .diff(actual_value, expected_value)
                             .sort_by { |a| a[1] }

      diffs_grouped_by_name =
        diffs_sorted_by_name.each_with_object({}) do |diff, memo|
          operator, name, value = diff
          memo[name] ||= {}
          memo[name][operator] = value
        end

      diffs_grouped_by_name.each do |name, difference|
        resolve_and_append_diff_to(
          path, name, expected_value, actual_value, difference
        )
      end
    end

    def resolve_and_append_diff_to(
      path,
      name,
      expected_value,
      actual_value,
      difference
    )
      extra_value, missing_value, different_value =
        resolve_changes(difference, expected_value, actual_value, name)

      full_path = !path.empty? ? "#{path}.#{name}" : name

      if non_empty_hash?(missing_value) && non_empty_hash?(extra_value)
        add_diff_to_message(missing_value, extra_value, full_path)
      elsif non_empty_array?(missing_value) && non_empty_array?(extra_value)
        [missing_value.length, extra_value.length].max.times do |i|
          add_diff_to_message(missing_value[i], extra_value[i], full_path)
        end
      elsif difference.key?('~')
        value = value_at_path(expected_value, name)
        append_diff_to_message(full_path, value, different_value)
      else
        append_diff_to_message(full_path, extra_value, missing_value)
      end
    end

    def resolve_changes(difference, expected_value, actual_value, name)
      missing_value = difference['-'] || value_at_path(actual_value, name)
      extra_value = difference['+'] || value_at_path(expected_value, name)
      different_value = difference['~']

      [extra_value, missing_value, different_value]
    end

    def append_diff_to_message(path, expected, actual)
      append_to_message(
        path,
        get_diff(path, expected: expected, actual: actual)
      )
    end

    def non_empty_hash?(target)
      target.is_a?(Hash) && target.any?
    end

    def non_empty_array?(target)
      target.is_a?(Array) && target.any?
    end

    def append_to_message(attribute, diff_description)
      return if already_reported_difference?(attribute)

      @message += diff_description
      @reported_differences[attribute] = true
    end

    def already_reported_difference?(attribute)
      @reported_differences.key?(attribute)
    end

    def value_at_path(target, attribute_path)
      keys = attribute_path.split(/[\[\].]/)

      keys = keys.map do |key|
        if key.to_i.zero? && key != '0'
          key
        else
          key.to_i
        end
      end

      result = target

      keys.each do |key|
        result = result[key] unless key == ''
      end

      result
    end

    def get_diff(attribute, options = {})
      diff_description = ''
      diff_description += "#{attribute}\n"
      diff_description += "Expected: #{format_value(options[:expected])}\n"
      diff_description + "Actual: #{format_value(options[:actual])}\n\n"
    end

    def format_value(value)
      if value.is_a?(String)
        "'#{value}'"
      else
        value
      end
    end
  end
end

Public Instance Methods

add_diff_to_message(actual_value, expected_value, path = '') click to toggle source

Adds diff descriptions to the failure message until the all the nodes of the expected and actual values have been compared, and all the differences (and the paths to them) have been included.

For Hashes and Arrays, it recursively calls itself to compare all nodes and elements.

@param [String, Number, Boolean, Array, Hash] actual_value current node of the

actual value being compared to the corresponding node of the expected
value

@param [String, Number, Boolean, Array, Hash] expected_value current node of

the expected value being compared to the corresponding node of the
actual value

@param [String] path path to the current nodes being compared, relative to the

root full objects

@return void Diff descriptions are appended directly to message

# File lib/json_spectacular/diff_descriptions.rb, line 25
def add_diff_to_message(actual_value, expected_value, path = '')
  diffs_sorted_by_name = Hashdiff
                         .diff(actual_value, expected_value)
                         .sort_by { |a| a[1] }

  diffs_grouped_by_name =
    diffs_sorted_by_name.each_with_object({}) do |diff, memo|
      operator, name, value = diff
      memo[name] ||= {}
      memo[name][operator] = value
    end

  diffs_grouped_by_name.each do |name, difference|
    resolve_and_append_diff_to(
      path, name, expected_value, actual_value, difference
    )
  end
end
already_reported_difference?(attribute) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 100
def already_reported_difference?(attribute)
  @reported_differences.key?(attribute)
end
append_diff_to_message(path, expected, actual) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 78
def append_diff_to_message(path, expected, actual)
  append_to_message(
    path,
    get_diff(path, expected: expected, actual: actual)
  )
end
append_to_message(attribute, diff_description) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 93
def append_to_message(attribute, diff_description)
  return if already_reported_difference?(attribute)

  @message += diff_description
  @reported_differences[attribute] = true
end
format_value(value) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 131
def format_value(value)
  if value.is_a?(String)
    "'#{value}'"
  else
    value
  end
end
get_diff(attribute, options = {}) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 124
def get_diff(attribute, options = {})
  diff_description = ''
  diff_description += "#{attribute}\n"
  diff_description += "Expected: #{format_value(options[:expected])}\n"
  diff_description + "Actual: #{format_value(options[:actual])}\n\n"
end
non_empty_array?(target) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 89
def non_empty_array?(target)
  target.is_a?(Array) && target.any?
end
non_empty_hash?(target) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 85
def non_empty_hash?(target)
  target.is_a?(Hash) && target.any?
end
resolve_and_append_diff_to( path, name, expected_value, actual_value, difference ) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 44
def resolve_and_append_diff_to(
  path,
  name,
  expected_value,
  actual_value,
  difference
)
  extra_value, missing_value, different_value =
    resolve_changes(difference, expected_value, actual_value, name)

  full_path = !path.empty? ? "#{path}.#{name}" : name

  if non_empty_hash?(missing_value) && non_empty_hash?(extra_value)
    add_diff_to_message(missing_value, extra_value, full_path)
  elsif non_empty_array?(missing_value) && non_empty_array?(extra_value)
    [missing_value.length, extra_value.length].max.times do |i|
      add_diff_to_message(missing_value[i], extra_value[i], full_path)
    end
  elsif difference.key?('~')
    value = value_at_path(expected_value, name)
    append_diff_to_message(full_path, value, different_value)
  else
    append_diff_to_message(full_path, extra_value, missing_value)
  end
end
resolve_changes(difference, expected_value, actual_value, name) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 70
def resolve_changes(difference, expected_value, actual_value, name)
  missing_value = difference['-'] || value_at_path(actual_value, name)
  extra_value = difference['+'] || value_at_path(expected_value, name)
  different_value = difference['~']

  [extra_value, missing_value, different_value]
end
value_at_path(target, attribute_path) click to toggle source
# File lib/json_spectacular/diff_descriptions.rb, line 104
def value_at_path(target, attribute_path)
  keys = attribute_path.split(/[\[\].]/)

  keys = keys.map do |key|
    if key.to_i.zero? && key != '0'
      key
    else
      key.to_i
    end
  end

  result = target

  keys.each do |key|
    result = result[key] unless key == ''
  end

  result
end