Skip to content

Commit 605540b

Browse files
committed
Extract OperationalSequencers, DiffFormatters, and Differs
* Operational sequencers are things that perform a diff on two data structures and spit out a sort of internal representation of the diff as a sequence of operations. * Diff formatters take those operations and then create a string representation of the diff for display purposes. * Differs combine the two steps of generating an operational sequence and then formatting that as a diff.
1 parent ea05626 commit 605540b

File tree

18 files changed

+515
-237
lines changed

18 files changed

+515
-237
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require_relative "../helpers"
2+
3+
module SuperDiff
4+
module DiffFormatters
5+
class Array
6+
def self.call(operations, indent:)
7+
Collection.call(
8+
open_token: "[",
9+
close_token: "]",
10+
operations: operations,
11+
indent: indent
12+
) do |op|
13+
Helpers.inspect_object(op.collection[op.index])
14+
end
15+
end
16+
end
17+
end
18+
end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require_relative "../helpers"
2+
3+
module SuperDiff
4+
module DiffFormatters
5+
class Collection
6+
ICONS = { delete: "-", insert: "+" }
7+
STYLES = { insert: :inserted, delete: :deleted, noop: :normal }
8+
9+
def self.call(
10+
open_token:,
11+
close_token:,
12+
operations:,
13+
indent:,
14+
&diff_line_for
15+
)
16+
new(
17+
open_token: open_token,
18+
close_token: close_token,
19+
operations: operations,
20+
indent: indent,
21+
&diff_line_for
22+
).call
23+
end
24+
25+
def initialize(
26+
open_token:,
27+
close_token:,
28+
operations:,
29+
indent:,
30+
&diff_line_for
31+
)
32+
@open_token = open_token
33+
@close_token = close_token
34+
@operations = operations
35+
@indent = indent
36+
@diff_line_for = diff_line_for
37+
end
38+
39+
def call
40+
[" #{open_token}", *contents, " #{close_token}"].join("\n")
41+
end
42+
43+
private
44+
45+
attr_reader :open_token, :close_token, :operations, :indent,
46+
:diff_line_for
47+
48+
def contents
49+
operations.map do |op|
50+
index = op.index
51+
collection = op.collection
52+
53+
icon = ICONS.fetch(op.name, " ")
54+
style_name = STYLES.fetch(op.name, :normal)
55+
chunk = build_chunk(
56+
diff_line_for.(op),
57+
indent: indent,
58+
icon: icon
59+
)
60+
61+
if index < collection.length - 1
62+
chunk << ","
63+
end
64+
65+
style_chunk(style_name, chunk)
66+
end
67+
end
68+
69+
def build_chunk(text, indent:, icon:)
70+
text
71+
.split("\n")
72+
.map { |line| icon + (" " * (indent - 1)) + line }
73+
.join("\n")
74+
end
75+
76+
def style_chunk(style_name, chunk)
77+
chunk
78+
.split("\n")
79+
.map { |line| Helpers.style(style_name, line) }
80+
.join("\n")
81+
end
82+
end
83+
end
84+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require_relative "../helpers"
2+
require_relative "collection"
3+
4+
module SuperDiff
5+
module DiffFormatters
6+
class Hash
7+
def self.call(operations, indent:)
8+
Collection.call(
9+
open_token: "{",
10+
close_token: "}",
11+
operations: operations,
12+
indent: indent
13+
) do |op|
14+
key = op.key
15+
inspected_value = Helpers.inspect_object(op.collection[op.key])
16+
17+
if key.is_a?(Symbol)
18+
"#{key}: #{inspected_value}"
19+
else
20+
"#{key.inspect} => #{inspected_value}"
21+
end
22+
end
23+
end
24+
end
25+
end
26+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require_relative "../helpers"
2+
3+
module SuperDiff
4+
module DiffFormatters
5+
class MultiLineString
6+
def self.call(operations, indent:)
7+
new(operations, indent: indent).call
8+
end
9+
10+
def initialize(operations, indent:)
11+
@operations = operations
12+
@indent = indent
13+
end
14+
15+
def call
16+
lines.join("\n")
17+
end
18+
19+
private
20+
21+
attr_reader :operations, :indent
22+
23+
def lines
24+
operations.map do |op|
25+
text = op.collection[op.index]
26+
27+
case op.name
28+
when :noop
29+
Helpers.style(:normal, " #{text}")
30+
when :insert
31+
Helpers.style(:inserted, "+ #{text}")
32+
when :delete
33+
Helpers.style(:deleted, "- #{text}")
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end

lib/super_diff/differs/array.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require_relative "../operational_sequencers/array"
2+
require_relative "../diff_formatters/array"
3+
4+
module SuperDiff
5+
module Differs
6+
class Array
7+
def self.call(expected, actual, indent:)
8+
new(expected, actual, indent: indent).call
9+
end
10+
11+
def initialize(expected, actual, indent:)
12+
@expected = expected
13+
@actual = actual
14+
@indent = indent
15+
end
16+
17+
def call
18+
DiffFormatters::Array.call(
19+
OperationalSequencers::Array.call(expected, actual),
20+
indent: indent
21+
)
22+
end
23+
24+
private
25+
26+
attr_reader :expected, :actual, :indent
27+
end
28+
end
29+
end

lib/super_diff/differs/hash.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require_relative "../operational_sequencers/hash"
2+
require_relative "../diff_formatters/hash"
3+
4+
module SuperDiff
5+
module Differs
6+
class Hash
7+
def self.call(expected, actual, indent:)
8+
new(expected, actual, indent: indent).call
9+
end
10+
11+
def initialize(expected, actual, indent:)
12+
@expected = expected
13+
@actual = actual
14+
@indent = indent
15+
end
16+
17+
def call
18+
DiffFormatters::Hash.call(
19+
OperationalSequencers::Hash.call(expected, actual),
20+
indent: indent
21+
)
22+
end
23+
24+
private
25+
26+
attr_reader :expected, :actual, :indent
27+
end
28+
end
29+
end

lib/super_diff/equality_matchers/array.rb

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
require "patience_diff"
2-
3-
require_relative "collection"
1+
require_relative "../differs/array"
2+
require_relative "base"
43

54
module SuperDiff
65
module EqualityMatchers
7-
class Array < Collection
8-
def initialize(expected, actual)
9-
super(expected, actual)
10-
@sequence_matcher = PatienceDiff::SequenceMatcher.new
11-
end
12-
6+
class Array < Base
137
def fail
14-
diff = build_diff("[", "]") do |event|
15-
inspect(event[:collection][event[:index]])
16-
end
17-
188
<<~OUTPUT.strip
199
Differing arrays.
2010
21-
#{style :deleted, "Expected: #{expected.inspect}"}
22-
#{style :inserted, " Actual: #{actual.inspect}"}
11+
#{Helpers.style :deleted, "Expected: #{expected.inspect}"}
12+
#{Helpers.style :inserted, " Actual: #{actual.inspect}"}
2313
2414
Diff:
2515
@@ -29,42 +19,8 @@ def fail
2919

3020
protected
3121

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)
22+
def diff
23+
Differs::Array.call(expected, actual, indent: 4)
6824
end
6925
end
7026
end
Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
require_relative "../csi"
2-
31
module SuperDiff
42
module EqualityMatchers
53
class Base
6-
ICONS = { deleted: "-", inserted: "+" }
7-
STYLES = { inserted: :inserted, deleted: :deleted, equal: :normal }
8-
COLORS = { normal: :plain, inserted: :green, deleted: :red }
9-
104
def self.call(expected, actual)
115
new(expected, actual).call
126
end
@@ -31,45 +25,6 @@ def call
3125
def fail
3226
raise NotImplementedError
3327
end
34-
35-
def style(style_name, text)
36-
Csi::ColorHelper.public_send(COLORS.fetch(style_name), text)
37-
end
38-
39-
def plural_type_for(value)
40-
case value
41-
when Numeric then "numbers"
42-
when String then "strings"
43-
when Symbol then "symbols"
44-
else "objects"
45-
end
46-
end
47-
48-
def inspect(value)
49-
case value
50-
when ::Hash
51-
value.inspect.
52-
gsub(/([^,]+)=>([^,]+)/, '\1 => \2').
53-
gsub(/:(\w+) => /, '\1: ').
54-
gsub(/\{([^{}]+)\}/, '{ \1 }')
55-
when String
56-
newline = "⏎"
57-
value.gsub(/\r\n/, newline).gsub(/\n/, newline).inspect
58-
else
59-
inspected_value = value.inspect
60-
match = inspected_value.match(/\A#<([^ ]+)(.*)>\Z/)
61-
62-
if match
63-
[
64-
"#<#{match.captures[0]} {",
65-
*match.captures[1].split(" ").map { |line| " " + line },
66-
"}>"
67-
].join("\n")
68-
else
69-
inspected_value
70-
end
71-
end
72-
end
7328
end
7429
end
7530
end

0 commit comments

Comments
 (0)