Module: JSONSpectacular::DiffDescriptions

Included in:
Matcher
Defined in:
lib/json_spectacular/diff_descriptions.rb

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object

rubocop:disable Metrics/MethodLength



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# 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