|
1 |
| -require "patience_diff" |
2 |
| -require_relative "csi" |
3 |
| -require_relative "pretty_printers/array" |
| 1 | +require_relative "differs/detector" |
| 2 | +require_relative "differs/object" |
4 | 3 |
|
5 | 4 | module SuperDiff
|
6 | 5 | class Differ
|
7 |
| - BLACK = Csi::FourBitColor.new(:white) |
8 |
| - LIGHT_RED = Csi::TwentyFourBitColor.new(r: 73, g: 62, b: 71) |
9 |
| - RED = Csi::TwentyFourBitColor.new(r: 116, g: 78, b: 84) |
10 |
| - LIGHT_GREEN = Csi::TwentyFourBitColor.new(r: 51, g: 81, b: 81) |
11 |
| - GREEN = Csi::TwentyFourBitColor.new(r: 81, g: 115, b: 105) |
12 |
| - ICONS = { deleted: "-", inserted: "+" } |
13 |
| - STYLES = { inserted: :inserted, deleted: :deleted, equal: :normal } |
14 |
| - COLORS = { normal: :plain, inserted: :green, deleted: :red } |
15 |
| - |
16 | 6 | def self.call(expected:, actual:)
|
17 | 7 | new(expected: expected, actual: actual).call
|
18 | 8 | end
|
19 | 9 |
|
20 | 10 | def initialize(expected:, actual:)
|
21 | 11 | @expected = expected
|
22 | 12 | @actual = actual
|
23 |
| - @sequence_matcher = PatienceDiff::SequenceMatcher.new |
24 | 13 | end
|
25 | 14 |
|
26 | 15 | def call
|
27 |
| - if expected == actual |
28 |
| - "" |
| 16 | + if expected.is_a?(actual.class) |
| 17 | + Differs::Detector.call(expected.class).call(expected, actual) |
29 | 18 | else
|
30 |
| - if expected.is_a?(String) && actual.is_a?(String) |
31 |
| - diff_strings |
32 |
| - elsif expected.is_a?(Array) && actual.is_a?(Array) |
33 |
| - diff_arrays |
34 |
| - elsif expected.is_a?(Hash) && actual.is_a?(Hash) |
35 |
| - diff_hashes |
36 |
| - else |
37 |
| - diff_objects |
38 |
| - end |
| 19 | + Differs::Object.call(expected, actual) |
39 | 20 | end
|
40 | 21 | end
|
41 | 22 |
|
42 | 23 | private
|
43 | 24 |
|
44 |
| - attr_reader :expected, :actual, :color, :sequence_matcher |
45 |
| - |
46 |
| - def diff_strings |
47 |
| - if expected.include?("\n") || actual.include?("\n") |
48 |
| - a = expected.split(/\n/).map { |line| "#{line}⏎" } |
49 |
| - b = actual.split(/\n/).map { |line| "#{line}⏎" } |
50 |
| - opcodes = sequence_matcher.diff_opcodes(a, b) |
51 |
| - |
52 |
| - diff = opcodes.flat_map do |code, a_start, a_end, b_start, b_end| |
53 |
| - if code == :equal |
54 |
| - b[b_start..b_end].map { |line| normal(" #{line}") } |
55 |
| - elsif code == :insert |
56 |
| - b[b_start..b_end].map { |line| inserted("+ #{line}") } |
57 |
| - else |
58 |
| - a[a_start..a_end].map { |line| deleted("- #{line}") } |
59 |
| - end |
60 |
| - end.join("\n") |
61 |
| - |
62 |
| - <<~OUTPUT.strip |
63 |
| - Differing strings. |
64 |
| -
|
65 |
| - #{deleted "Expected: #{inspect(expected)}"} |
66 |
| - #{inserted " Actual: #{inspect(actual)}"} |
67 |
| -
|
68 |
| - Diff: |
69 |
| -
|
70 |
| - #{diff} |
71 |
| - OUTPUT |
72 |
| - else |
73 |
| - <<~OUTPUT.strip |
74 |
| - Differing strings. |
75 |
| -
|
76 |
| - #{deleted "Expected: #{inspect(expected)}"} |
77 |
| - #{inserted " Actual: #{inspect(actual)}"} |
78 |
| - OUTPUT |
79 |
| - end |
80 |
| - end |
81 |
| - |
82 |
| - def diff_arrays |
83 |
| - a, b = expected, actual |
84 |
| - opcodes = sequence_matcher.diff_opcodes(a, b) |
85 |
| - larger_collection = |
86 |
| - if actual.length > expected.length |
87 |
| - actual |
88 |
| - else |
89 |
| - expected |
90 |
| - end |
91 |
| - |
92 |
| - events = opcodes.flat_map do |code, a_start, a_end, b_start, b_end| |
93 |
| - if code == :delete |
94 |
| - (a_start..a_end).map do |index| |
95 |
| - { |
96 |
| - state: :deleted, |
97 |
| - index: index, |
98 |
| - collection: expected, |
99 |
| - larger_collection: larger_collection |
100 |
| - } |
101 |
| - end |
102 |
| - elsif code == :insert |
103 |
| - (b_start..b_end).map do |index| |
104 |
| - { |
105 |
| - state: :inserted, |
106 |
| - index: index, |
107 |
| - collection: actual, |
108 |
| - larger_collection: larger_collection |
109 |
| - } |
110 |
| - end |
111 |
| - else |
112 |
| - (b_start..b_end).map do |index| |
113 |
| - { |
114 |
| - state: :equal, |
115 |
| - index: index, |
116 |
| - collection: actual, |
117 |
| - larger_collection: larger_collection |
118 |
| - } |
119 |
| - end |
120 |
| - end |
121 |
| - end |
122 |
| - |
123 |
| - contents = events.map do |event| |
124 |
| - index = event[:index] |
125 |
| - collection = event[:collection] |
126 |
| - # larger_collection = event[:larger_collection] |
127 |
| - |
128 |
| - icon = ICONS.fetch(event[:state], " ") |
129 |
| - style_name = STYLES.fetch(event[:state], :normal) |
130 |
| - chunk = build_chunk( |
131 |
| - inspect(collection[index]), |
132 |
| - indent: 4, |
133 |
| - icon: icon |
134 |
| - ) |
135 |
| - |
136 |
| - if index < collection.length - 1 |
137 |
| - chunk << "," |
138 |
| - end |
139 |
| - |
140 |
| - style_chunk(style_name, chunk) |
141 |
| - end |
142 |
| - |
143 |
| - diff = [" [", *contents, " ]"].join("\n") |
144 |
| - |
145 |
| - <<~OUTPUT.strip |
146 |
| - Differing arrays. |
147 |
| -
|
148 |
| - #{deleted "Expected: #{expected.inspect}"} |
149 |
| - #{inserted " Actual: #{actual.inspect}"} |
150 |
| -
|
151 |
| - Diff: |
152 |
| -
|
153 |
| - #{diff} |
154 |
| - OUTPUT |
155 |
| - end |
156 |
| - |
157 |
| - def diff_hashes |
158 |
| - all_keys = (expected.keys | actual.keys) |
159 |
| - |
160 |
| - events = all_keys.inject([]) do |array, key| |
161 |
| - if expected.include?(key) |
162 |
| - if actual.include?(key) |
163 |
| - if expected[key] == actual[key] |
164 |
| - array << { |
165 |
| - state: :equal, |
166 |
| - key: key, |
167 |
| - collection: actual |
168 |
| - } |
169 |
| - else |
170 |
| - array << { |
171 |
| - state: :deleted, |
172 |
| - key: key, |
173 |
| - collection: expected |
174 |
| - } |
175 |
| - array << { |
176 |
| - state: :inserted, |
177 |
| - key: key, |
178 |
| - collection: actual |
179 |
| - } |
180 |
| - end |
181 |
| - else |
182 |
| - array << { |
183 |
| - state: :deleted, |
184 |
| - key: key, |
185 |
| - collection: expected |
186 |
| - } |
187 |
| - end |
188 |
| - elsif actual.include?(key) |
189 |
| - array << { |
190 |
| - state: :inserted, |
191 |
| - key: key, |
192 |
| - collection: actual |
193 |
| - } |
194 |
| - end |
195 |
| - |
196 |
| - array |
197 |
| - end |
198 |
| - |
199 |
| - if events.any? { |event| event != :equal } |
200 |
| - contents = events.map do |event| |
201 |
| - key = event[:key] |
202 |
| - collection = event[:collection] |
203 |
| - index = collection.keys.index(key) |
204 |
| - # larger_collection = event[:larger_collection] |
205 |
| - |
206 |
| - icon = ICONS.fetch(event[:state], " ") |
207 |
| - style_name = STYLES.fetch(event[:state], :normal) |
208 |
| - entry = |
209 |
| - if key.is_a?(Symbol) |
210 |
| - "#{key}: #{inspect(collection[key])}" |
211 |
| - else |
212 |
| - "#{key.inspect} => #{inspect(collection[key])}" |
213 |
| - end |
214 |
| - |
215 |
| - chunk = build_chunk( |
216 |
| - entry, |
217 |
| - indent: 4, |
218 |
| - icon: icon |
219 |
| - ) |
220 |
| - |
221 |
| - if index < collection.size - 1 |
222 |
| - chunk << "," |
223 |
| - end |
224 |
| - |
225 |
| - style_chunk(style_name, chunk) |
226 |
| - end |
227 |
| - |
228 |
| - diff = [" {", *contents, " }"].join("\n") |
229 |
| - |
230 |
| - <<~OUTPUT.strip |
231 |
| - Differing hashes. |
232 |
| -
|
233 |
| - #{deleted "Expected: #{inspect(expected)}"} |
234 |
| - #{inserted " Actual: #{inspect(actual)}"} |
235 |
| -
|
236 |
| - Diff: |
237 |
| -
|
238 |
| - #{diff} |
239 |
| - OUTPUT |
240 |
| - else |
241 |
| - "" |
242 |
| - end |
243 |
| - end |
244 |
| - |
245 |
| - def diff_objects |
246 |
| - <<~OUTPUT.strip |
247 |
| - Differing #{plural_type_for(actual)}. |
248 |
| -
|
249 |
| - #{deleted "Expected: #{expected.inspect}"} |
250 |
| - #{inserted " Actual: #{actual.inspect}"} |
251 |
| - OUTPUT |
252 |
| - end |
253 |
| - |
254 |
| - def style_chunk(style_name, chunk) |
255 |
| - chunk.split("\n").map { |line| style(style_name, line) }.join("\n") |
256 |
| - end |
257 |
| - |
258 |
| - def style(style_name, text) |
259 |
| - Csi::ColorHelper.public_send(COLORS.fetch(style_name), text) |
260 |
| - end |
261 |
| - |
262 |
| - def normal(text) |
263 |
| - Csi::ColorHelper.plain(text) |
264 |
| - end |
265 |
| - |
266 |
| - def inserted(text) |
267 |
| - Csi::ColorHelper.green(text) |
268 |
| - end |
269 |
| - |
270 |
| - def deleted(text) |
271 |
| - Csi::ColorHelper.red(text) |
272 |
| - end |
273 |
| - |
274 |
| - def faded(text) |
275 |
| - Csi::ColorHelper.dark_grey(text) |
276 |
| - end |
277 |
| - |
278 |
| - def plural_type_for(value) |
279 |
| - case value |
280 |
| - when Numeric then "numbers" |
281 |
| - when String then "strings" |
282 |
| - when Symbol then "symbols" |
283 |
| - else "objects" |
284 |
| - end |
285 |
| - end |
286 |
| - |
287 |
| - def build_chunk(text, indent:, icon:) |
288 |
| - text |
289 |
| - .split("\n") |
290 |
| - .map { |line| icon + (" " * (indent - 1)) + line } |
291 |
| - .join("\n") |
292 |
| - end |
293 |
| - |
294 |
| - def inspect(value) |
295 |
| - case value |
296 |
| - when Hash |
297 |
| - value.inspect. |
298 |
| - gsub(/([^,]+)=>([^,]+)/, '\1 => \2'). |
299 |
| - gsub(/:(\w+) => /, '\1: '). |
300 |
| - gsub(/\{([^{}]+)\}/, '{ \1 }') |
301 |
| - when String |
302 |
| - newline = "⏎" |
303 |
| - value.gsub(/\r\n/, newline).gsub(/\n/, newline).inspect |
304 |
| - else |
305 |
| - inspected_value = value.inspect |
306 |
| - match = inspected_value.match(/\A#<([^ ]+)(.*)>\Z/) |
307 |
| - |
308 |
| - if match |
309 |
| - [ |
310 |
| - "#<#{match.captures[0]} {", |
311 |
| - *match.captures[1].split(" ").map { |line| " " + line }, |
312 |
| - "}>" |
313 |
| - ].join("\n") |
314 |
| - else |
315 |
| - inspected_value |
316 |
| - end |
317 |
| - end |
318 |
| - end |
| 25 | + attr_reader :expected, :actual |
319 | 26 | end
|
320 | 27 | end
|
0 commit comments