Skip to content

Commit 4b7da9c

Browse files
committed
Add support for diffing within objects
Take the following example: Expected: [1, 2, [:a, :b, :c], 4] Actual: [1, 2, [:a, :x, :c], 4] We *could* display this diff like this: [ 1, 2, - [:a, :b, :c], + [:a, :x, :c], 4 ] However, it would be much more helpful to show a diff like this: [ 1, 2, [ :a, - :b, + :x, :c ], 4 ] In order to do this, the first step is to treat groups of operations which consist of a delete followed immediately by an insert as a "change". (So in this case, the fact that we are deleting `[:a, :b, :c]`) and adding `[:a, :x, :c]` is a change.) From there, we modify all diff formatters so that they look for these change operations. If we encounter one, we can then run a diff on the deleted (or "left") value and the inserted (or "right") value. This will spit out a new set of operations, which we can then run through whatever diff formatter is appropriate for the two values. There's some trickery involved here, particularly when it comes to hashes and objects, but that's the 10,000-foot view. One thing to note is that in order to do that last step, we have to add a diff formatter factory which looks at the values being diffed and figures out which class to use. Out of necessity I've extended this strategy to the other types of classes we have as well. Also a part of this change was adding a new type of classes called "operation sequences". This is a collection of operations that knows how to format itself.
1 parent 605540b commit 4b7da9c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1277
-313
lines changed

lib/super_diff.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,45 @@
1+
require "patience_diff"
2+
require "pry-byebug"
3+
4+
require_relative "super_diff/csi"
5+
require_relative "super_diff/errors"
6+
require_relative "super_diff/helpers"
7+
require_relative "super_diff/value_inspection"
8+
9+
require_relative "super_diff/equality_matchers/base"
10+
require_relative "super_diff/equality_matchers/array"
11+
require_relative "super_diff/equality_matchers/hash"
12+
require_relative "super_diff/equality_matchers/multi_line_string"
13+
require_relative "super_diff/equality_matchers/single_line_string"
14+
require_relative "super_diff/equality_matchers/object"
15+
require_relative "super_diff/equality_matchers"
116
require_relative "super_diff/equality_matcher"
17+
18+
require_relative "super_diff/operation_sequences/base"
19+
require_relative "super_diff/operation_sequences/array"
20+
require_relative "super_diff/operation_sequences/hash"
21+
require_relative "super_diff/operation_sequences/object"
22+
23+
require_relative "super_diff/operational_sequencers/base"
24+
require_relative "super_diff/operational_sequencers/array"
25+
require_relative "super_diff/operational_sequencers/hash"
26+
require_relative "super_diff/operational_sequencers/object"
27+
require_relative "super_diff/operational_sequencers"
28+
require_relative "super_diff/operational_sequencer"
29+
30+
require_relative "super_diff/diff_formatters/collection"
31+
require_relative "super_diff/diff_formatters/base"
32+
require_relative "super_diff/diff_formatters/array"
33+
require_relative "super_diff/diff_formatters/hash"
34+
require_relative "super_diff/diff_formatters/multi_line_string"
35+
require_relative "super_diff/diff_formatters/object"
36+
require_relative "super_diff/diff_formatters"
37+
require_relative "super_diff/diff_formatter"
38+
39+
require_relative "super_diff/differs/base"
40+
require_relative "super_diff/differs/array"
41+
require_relative "super_diff/differs/hash"
42+
require_relative "super_diff/differs/string"
43+
require_relative "super_diff/differs/object"
44+
require_relative "super_diff/differs"
45+
require_relative "super_diff/differ"

lib/super_diff/diff_formatter.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module SuperDiff
2+
class DiffFormatter
3+
def self.call(*args)
4+
raise "We don't need this anymore"
5+
new(*args).call
6+
end
7+
8+
def initialize(
9+
operations,
10+
indent_level:,
11+
add_comma: false,
12+
extra_classes: []
13+
)
14+
@operations = operations
15+
@indent_level = indent_level
16+
@add_comma = add_comma
17+
@extra_classes = extra_classes
18+
end
19+
20+
def call
21+
resolved_class.call(
22+
operations,
23+
indent_level: indent_level,
24+
add_comma: add_comma
25+
)
26+
end
27+
28+
private
29+
30+
attr_reader :operations, :indent_level, :add_comma, :extra_classes
31+
32+
def resolved_class
33+
(DiffFormatters::DEFAULTS + extra_classes).detect do |klass|
34+
klass.applies_to?(operations)
35+
end
36+
end
37+
end
38+
end

lib/super_diff/diff_formatters.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module SuperDiff
2+
module DiffFormatters
3+
DEFAULTS = [Array, Hash, MultiLineString]
4+
end
5+
end

lib/super_diff/diff_formatters/array.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
require_relative "../helpers"
2-
31
module SuperDiff
42
module DiffFormatters
5-
class Array
6-
def self.call(operations, indent:)
3+
class Array < Base
4+
def self.applies_to?(operations)
5+
operations.is_a?(OperationSequences::Array)
6+
end
7+
8+
def call
79
Collection.call(
810
open_token: "[",
911
close_token: "]",
12+
collection_prefix: collection_prefix,
13+
build_item_prefix: -> (op) { "" },
1014
operations: operations,
11-
indent: indent
12-
) do |op|
13-
Helpers.inspect_object(op.collection[op.index])
14-
end
15+
indent_level: indent_level,
16+
add_comma: add_comma?
17+
)
1518
end
1619
end
1720
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module SuperDiff
2+
module DiffFormatters
3+
class Base
4+
def self.applies_to?(operations)
5+
raise NotImplementedError
6+
end
7+
8+
def self.call(*args)
9+
new(*args).call
10+
end
11+
12+
def initialize(
13+
operations,
14+
indent_level:,
15+
collection_prefix: "",
16+
add_comma: false
17+
)
18+
@operations = operations
19+
@indent_level = indent_level
20+
@collection_prefix = collection_prefix
21+
@add_comma = add_comma
22+
end
23+
24+
def call
25+
raise NotImplementedError
26+
end
27+
28+
private
29+
30+
attr_reader :operations, :indent_level, :collection_prefix
31+
32+
def add_comma?
33+
@add_comma
34+
end
35+
end
36+
end
37+
end
Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,90 @@
1-
require_relative "../helpers"
2-
31
module SuperDiff
42
module DiffFormatters
53
class Collection
64
ICONS = { delete: "-", insert: "+" }
75
STYLES = { insert: :inserted, delete: :deleted, noop: :normal }
86

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
7+
def self.call(*args, &block)
8+
new(*args, &block).call
239
end
2410

2511
def initialize(
2612
open_token:,
2713
close_token:,
2814
operations:,
29-
indent:,
30-
&diff_line_for
15+
indent_level:,
16+
add_comma:,
17+
collection_prefix:,
18+
build_item_prefix:
3119
)
3220
@open_token = open_token
3321
@close_token = close_token
3422
@operations = operations
35-
@indent = indent
36-
@diff_line_for = diff_line_for
23+
@indent_level = indent_level
24+
@add_comma = add_comma
25+
@collection_prefix = collection_prefix
26+
@build_item_prefix = build_item_prefix
3727
end
3828

3929
def call
40-
[" #{open_token}", *contents, " #{close_token}"].join("\n")
30+
lines.join("\n")
4131
end
4232

4333
private
4434

45-
attr_reader :open_token, :close_token, :operations, :indent,
46-
:diff_line_for
35+
attr_reader :open_token, :close_token, :operations, :indent_level,
36+
:add_comma, :collection_prefix, :build_item_prefix#, :diff_line_for
37+
38+
def lines
39+
[
40+
" #{indentation}#{collection_prefix}#{open_token}",
41+
*contents,
42+
" #{indentation}#{close_token}#{comma}"
43+
]
44+
end
4745

4846
def contents
4947
operations.map do |op|
50-
index = op.index
51-
collection = op.collection
48+
if op.name == :change
49+
max_size = [op.left_collection.size, op.right_collection.size].max
50+
add_comma = op.index < max_size - 1
51+
op.child_operations.to_diff(
52+
indent_level: indent_level + 1,
53+
collection_prefix: build_item_prefix.call(op),
54+
add_comma: add_comma
55+
)
56+
else
57+
collection = op.collection
58+
icon = ICONS.fetch(op.name, " ")
59+
style_name = STYLES.fetch(op.name, :normal)
60+
chunk = build_chunk(
61+
Helpers.inspect_object(op.value, single_line: false),
62+
prefix: build_item_prefix.call(op),
63+
icon: icon
64+
)
5265

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-
)
66+
if op.index < collection.size - 1
67+
chunk << ","
68+
end
6069

61-
if index < collection.length - 1
62-
chunk << ","
70+
style_chunk(style_name, chunk)
6371
end
64-
65-
style_chunk(style_name, chunk)
6672
end
6773
end
6874

69-
def build_chunk(text, indent:, icon:)
70-
text
71-
.split("\n")
72-
.map { |line| icon + (" " * (indent - 1)) + line }
73-
.join("\n")
75+
def build_chunk(content, prefix:, icon:)
76+
lines =
77+
if content.is_a?(ValueInspection)
78+
[
79+
indentation(offset: 1) + prefix + content.beginning,
80+
*content.middle.map { |line| indentation(offset: 2) + line },
81+
indentation(offset: 1) + content.end
82+
]
83+
else
84+
[indentation(offset: 1) + prefix + content]
85+
end
86+
87+
lines.map { |line| icon + " " + line }.join("\n")
7488
end
7589

7690
def style_chunk(style_name, chunk)
@@ -79,6 +93,18 @@ def style_chunk(style_name, chunk)
7993
.map { |line| Helpers.style(style_name, line) }
8094
.join("\n")
8195
end
96+
97+
def indentation(offset: 0)
98+
" " * ((indent_level + offset) * 2)
99+
end
100+
101+
def comma
102+
if add_comma
103+
","
104+
else
105+
""
106+
end
107+
end
82108
end
83109
end
84110
end

lib/super_diff/diff_formatters/hash.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
require_relative "../helpers"
2-
require_relative "collection"
3-
41
module SuperDiff
52
module DiffFormatters
6-
class Hash
7-
def self.call(operations, indent:)
3+
class Hash < Base
4+
def self.applies_to?(operations)
5+
operations.is_a?(OperationSequences::Hash)
6+
end
7+
8+
def call
89
Collection.call(
910
open_token: "{",
1011
close_token: "}",
12+
collection_prefix: collection_prefix,
13+
build_item_prefix: -> (op) {
14+
if op.key.is_a?(Symbol)
15+
"#{op.key}: "
16+
else
17+
"#{op.key.inspect} => "
18+
end
19+
},
1120
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
21+
indent_level: indent_level,
22+
add_comma: add_comma?
23+
)
2324
end
2425
end
2526
end

lib/super_diff/diff_formatters/multi_line_string.rb

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
require_relative "../helpers"
2-
31
module SuperDiff
42
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
3+
class MultiLineString < Base
4+
def self.applies_to?(operations)
5+
operations.is_a?(OperationSequences::MultiLineString)
136
end
147

158
def call
@@ -18,19 +11,18 @@ def call
1811

1912
private
2013

21-
attr_reader :operations, :indent
22-
2314
def lines
24-
operations.map do |op|
25-
text = op.collection[op.index]
26-
15+
operations.inject([]) do |array, op|
2716
case op.name
28-
when :noop
29-
Helpers.style(:normal, " #{text}")
30-
when :insert
31-
Helpers.style(:inserted, "+ #{text}")
17+
when :change
18+
array << Helpers.style(:deleted, "- #{op.left_value}")
19+
array << Helpers.style(:inserted, "+ #{op.right_value}")
3220
when :delete
33-
Helpers.style(:deleted, "- #{text}")
21+
array << Helpers.style(:deleted, "- #{op.value}")
22+
when :insert
23+
array << Helpers.style(:inserted, "+ #{op.value}")
24+
else
25+
array << Helpers.style(:normal, " #{op.value}")
3426
end
3527
end
3628
end

0 commit comments

Comments
 (0)