Skip to content

Commit 54174a0

Browse files
committed
Refactor diffing logic into separate classes
1 parent 68f93ee commit 54174a0

File tree

12 files changed

+412
-459
lines changed

12 files changed

+412
-459
lines changed

lib/super_diff/differ.rb

Lines changed: 6 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -1,320 +1,27 @@
1-
require "patience_diff"
2-
require_relative "csi"
3-
require_relative "pretty_printers/array"
1+
require_relative "differs/detector"
2+
require_relative "differs/object"
43

54
module SuperDiff
65
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-
166
def self.call(expected:, actual:)
177
new(expected: expected, actual: actual).call
188
end
199

2010
def initialize(expected:, actual:)
2111
@expected = expected
2212
@actual = actual
23-
@sequence_matcher = PatienceDiff::SequenceMatcher.new
2413
end
2514

2615
def call
27-
if expected == actual
28-
""
16+
if expected.is_a?(actual.class)
17+
Differs::Detector.call(expected.class).call(expected, actual)
2918
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)
3920
end
4021
end
4122

4223
private
4324

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
31926
end
32027
end

lib/super_diff/differs/array.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
require "patience_diff"
2+
3+
require_relative "collection"
4+
5+
module SuperDiff
6+
module Differs
7+
class Array < Collection
8+
def initialize(expected, actual)
9+
super(expected, actual)
10+
@sequence_matcher = PatienceDiff::SequenceMatcher.new
11+
end
12+
13+
def fail
14+
diff = build_diff("[", "]") do |event|
15+
inspect(event[:collection][event[:index]])
16+
end
17+
18+
<<~OUTPUT.strip
19+
Differing arrays.
20+
21+
#{style :deleted, "Expected: #{expected.inspect}"}
22+
#{style :inserted, " Actual: #{actual.inspect}"}
23+
24+
Diff:
25+
26+
#{diff}
27+
OUTPUT
28+
end
29+
30+
protected
31+
32+
def events
33+
opcodes.flat_map do |code, a_start, a_end, b_start, b_end|
34+
if code == :delete
35+
(a_start..a_end).map do |index|
36+
{
37+
state: :deleted,
38+
index: index,
39+
collection: expected
40+
}
41+
end
42+
elsif code == :insert
43+
(b_start..b_end).map do |index|
44+
{
45+
state: :inserted,
46+
index: index,
47+
collection: actual
48+
}
49+
end
50+
else
51+
(b_start..b_end).map do |index|
52+
{
53+
state: :equal,
54+
index: index,
55+
collection: actual
56+
}
57+
end
58+
end
59+
end
60+
end
61+
62+
private
63+
64+
attr_reader :sequence_matcher
65+
66+
def opcodes
67+
sequence_matcher.diff_opcodes(expected, actual)
68+
end
69+
end
70+
end
71+
end

0 commit comments

Comments
 (0)