Skip to content

Commit 2d06e39

Browse files
authored
Add handling for Data classes (#259)
Closes #252. Ruby 3.2.0 introduced the [Data](https://bugs.ruby-lang.org/issues/16122) construct, which looks a lot like an object to this project, but uses `#members` instead of instance variables. The solution is to introduce an object inspection tree builder and operation tree builder that are essentially the same as the corresponding builders for default objects, but know to read the members of the Data class instead of instance variables.
1 parent 53c9c28 commit 2d06e39

File tree

9 files changed

+310
-1
lines changed

9 files changed

+310
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Features
6+
7+
- Add better support for Data object diffing. [#259](https://github.com/splitwise/super_diff/pull/224)
8+
39
## 0.12.1 - 2024-04-26
410

511
Note that since 0.12.0 has been yanked, changes for this version are listed

lib/super_diff/basic.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Basic
2626
InspectionTreeBuilders::Primitive,
2727
InspectionTreeBuilders::TimeLike,
2828
InspectionTreeBuilders::DateLike,
29+
InspectionTreeBuilders::DataObject,
2930
InspectionTreeBuilders::DefaultObject
3031
)
3132

@@ -34,7 +35,8 @@ module Basic
3435
OperationTreeBuilders::Hash,
3536
OperationTreeBuilders::TimeLike,
3637
OperationTreeBuilders::DateLike,
37-
OperationTreeBuilders::CustomObject
38+
OperationTreeBuilders::CustomObject,
39+
OperationTreeBuilders::DataObject
3840
)
3941

4042
config.add_extra_operation_tree_classes(

lib/super_diff/basic/inspection_tree_builders.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ module InspectionTreeBuilders
66
:CustomObject,
77
"super_diff/basic/inspection_tree_builders/custom_object"
88
)
9+
autoload(
10+
:DataObject,
11+
"super_diff/basic/inspection_tree_builders/data_object"
12+
)
913
autoload(
1014
:DefaultObject,
1115
"super_diff/basic/inspection_tree_builders/default_object"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module SuperDiff
2+
module Basic
3+
module InspectionTreeBuilders
4+
class DataObject < Core::AbstractInspectionTreeBuilder
5+
def self.applies_to?(value)
6+
SuperDiff::Core::Helpers.ruby_version_matches?("~> 3.2") &&
7+
value.is_a?(Data)
8+
end
9+
10+
def call
11+
Core::InspectionTree.new do |t1|
12+
t1.as_lines_when_rendering_to_lines(
13+
collection_bookend: :open
14+
) do |t2|
15+
t2.add_text "#<data #{object.class.name} "
16+
17+
# stree-ignore
18+
t2.when_rendering_to_lines do |t3|
19+
t3.add_text "{"
20+
end
21+
end
22+
23+
t1.nested { |t2| t2.insert_hash_inspection_of(object.to_h) }
24+
25+
t1.as_lines_when_rendering_to_lines(
26+
collection_bookend: :close
27+
) do |t2|
28+
# stree-ignore
29+
t2.when_rendering_to_lines do |t3|
30+
t3.add_text "}"
31+
end
32+
33+
t2.add_text ">"
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end
40+
end

lib/super_diff/basic/operation_tree_builders.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ module OperationTreeBuilders
66
:CustomObject,
77
"super_diff/basic/operation_tree_builders/custom_object"
88
)
9+
autoload(
10+
:DataObject,
11+
"super_diff/basic/operation_tree_builders/data_object"
12+
)
913
autoload(
1014
:DefaultObject,
1115
"super_diff/basic/operation_tree_builders/default_object"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module SuperDiff
2+
module Basic
3+
module OperationTreeBuilders
4+
class DataObject < CustomObject
5+
def self.applies_to?(expected, actual)
6+
SuperDiff::Core::Helpers.ruby_version_matches?("~> 3.2") &&
7+
expected.class == actual.class && expected.is_a?(Data)
8+
end
9+
10+
protected
11+
12+
def attribute_names
13+
expected.members & actual.members
14+
end
15+
end
16+
end
17+
end
18+
end

spec/support/models/point.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
if defined?(Data)
2+
module SuperDiff
3+
module Test
4+
Point = Data.define(:x, :y)
5+
end
6+
end
7+
end
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
require "spec_helper"
2+
3+
if defined?(Data)
4+
RSpec.describe SuperDiff, type: :unit do
5+
describe ".inspect_object" do
6+
context "given as_lines: false" do
7+
subject(:output) do
8+
described_class.inspect_object(object, as_lines: false)
9+
end
10+
11+
context "given an anonymous Data object" do
12+
let(:object) { Data.define(:x, :y).new(1, 2) }
13+
14+
it "shows the data" do
15+
expect(output).to eq("#<data x: 1, y: 2>")
16+
end
17+
end
18+
19+
context "given a named Data object" do
20+
let(:object) { SuperDiff::Test::Point.new(1, 2) }
21+
22+
it "shows the data" do
23+
expect(output).to eq("#<data SuperDiff::Test::Point x: 1, y: 2>")
24+
end
25+
end
26+
27+
context "given a Data object that defines #attributes_for_super_diff" do
28+
let(:klass) do
29+
Data.define(:x, :y) do
30+
def attributes_for_super_diff
31+
{ beep: :boop }
32+
end
33+
end
34+
end
35+
let(:object) { klass.new(1, 2) }
36+
37+
it "uses the custom attributes" do
38+
expect(output).to start_with("#<#<Class:0x").and end_with(
39+
"beep: :boop>"
40+
)
41+
end
42+
end
43+
end
44+
45+
context "given as_lines: true" do
46+
subject(:tiered_lines) do
47+
described_class.inspect_object(
48+
object,
49+
as_lines: true,
50+
type: :noop,
51+
indentation_level: 1
52+
)
53+
end
54+
55+
context "given an anonymous Data object" do
56+
let(:object) { Data.define(:x, :y).new(1, 2) }
57+
58+
it "shows the data" do
59+
expect(tiered_lines).to match(
60+
[
61+
an_object_having_attributes(
62+
value: "#<data {",
63+
collection_bookend: :open
64+
),
65+
an_object_having_attributes(
66+
prefix: "x: ",
67+
value: "1",
68+
add_comma: true
69+
),
70+
an_object_having_attributes(
71+
prefix: "y: ",
72+
value: "2",
73+
add_comma: false
74+
),
75+
an_object_having_attributes(
76+
value: "}>",
77+
collection_bookend: :close
78+
)
79+
]
80+
)
81+
end
82+
end
83+
84+
context "given a named Data object" do
85+
let(:object) { SuperDiff::Test::Point.new(1, 2) }
86+
87+
it "shows the data" do
88+
expect(tiered_lines).to match(
89+
[
90+
an_object_having_attributes(
91+
value: "#<data SuperDiff::Test::Point {",
92+
collection_bookend: :open
93+
),
94+
an_object_having_attributes(
95+
prefix: "x: ",
96+
value: "1",
97+
add_comma: true
98+
),
99+
an_object_having_attributes(
100+
prefix: "y: ",
101+
value: "2",
102+
add_comma: false
103+
),
104+
an_object_having_attributes(
105+
value: "}>",
106+
collection_bookend: :close
107+
)
108+
]
109+
)
110+
end
111+
end
112+
113+
context "given a Data object that defines #attributes_for_super_diff" do
114+
let(:klass) do
115+
Data.define(:x, :y) do
116+
def attributes_for_super_diff
117+
{ beep: :boop }
118+
end
119+
end
120+
end
121+
let(:object) { klass.new(1, 2) }
122+
123+
it "uses the custom attributes" do
124+
expect(tiered_lines).to match(
125+
[
126+
an_object_having_attributes(
127+
value: /\A#<#<Class:0x.*> {/,
128+
collection_bookend: :open
129+
),
130+
an_object_having_attributes(
131+
prefix: "beep: ",
132+
value: ":boop",
133+
add_comma: false
134+
),
135+
an_object_having_attributes(
136+
value: "}>",
137+
collection_bookend: :close
138+
)
139+
]
140+
)
141+
end
142+
end
143+
end
144+
end
145+
end
146+
end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require "spec_helper"
2+
3+
if defined?(Data)
4+
RSpec.describe SuperDiff, type: :unit do
5+
describe ".diff" do
6+
subject(:diff) { described_class.diff(a, b) }
7+
8+
context "when given two Data objects of the same class" do
9+
let(:a) { SuperDiff::Test::Point.new(1, 2) }
10+
let(:b) { SuperDiff::Test::Point.new(1, 3) }
11+
12+
it "diffs their member attributes" do
13+
expected_output =
14+
SuperDiff::Core::Helpers
15+
.style(color_enabled: true) do
16+
plain_line " #<SuperDiff::Test::Point {"
17+
plain_line " x: 1,"
18+
expected_line "- y: 2"
19+
actual_line "+ y: 3"
20+
plain_line " }>"
21+
end
22+
.to_s
23+
.chomp
24+
25+
expect(diff).to eq(expected_output)
26+
end
27+
28+
context "when the Data class defines #attributes_for_super_diff" do
29+
let(:klass) do
30+
Class.new(Data.define(:attribute)) do
31+
def self.to_s = "TestClass"
32+
33+
def attributes_for_super_diff
34+
{ attribute: :does_not_matter }
35+
end
36+
end
37+
end
38+
39+
let(:a) { klass.new(1) }
40+
let(:b) { klass.new(2) }
41+
42+
it "diffs their member attributes" do
43+
expected_output =
44+
SuperDiff::Core::Helpers
45+
.style(color_enabled: true) do
46+
plain_line " #<TestClass {"
47+
expected_line "- attribute: 1"
48+
actual_line "+ attribute: 2"
49+
plain_line " }>"
50+
end
51+
.to_s
52+
.chomp
53+
54+
expect(diff).to eq(expected_output)
55+
end
56+
end
57+
end
58+
59+
context "when given two Data objects of different classes" do
60+
let(:a) { SuperDiff::Test::Point.new(1, 2) }
61+
let(:b) { Data.define(:one, :two).new(1, 2) }
62+
63+
it "raises" do
64+
expect { SuperDiff.diff(a, b) }.to raise_error(
65+
SuperDiff::Core::NoDifferAvailableError
66+
)
67+
end
68+
end
69+
70+
context "when given a Data object and a hash" do
71+
let(:a) { Data.define(:one, :two).new(1, 2) }
72+
let(:b) { { one: 1, two: 2 } }
73+
74+
it "raises" do
75+
expect { SuperDiff.diff(a, b) }.to raise_error(
76+
SuperDiff::Core::NoDifferAvailableError
77+
)
78+
end
79+
end
80+
end
81+
end
82+
end

0 commit comments

Comments
 (0)