super_diff 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37e66ee8a26406e1aa13a554cd457192cdfbbe2fc23ce0657875ac15b1d3eb99
4
- data.tar.gz: 2bd8e0afbde52a4b3f8735645b0561c988977bdddfed24027dd59026b2b63579
3
+ metadata.gz: 7cdffdbc9c2efd5ebdfa358abac1e6ce6a425d77bbfa7214cdbfa7ee15ca26f0
4
+ data.tar.gz: ef20df7cc577a962b2d3ecc6cf64a6b5dff78afef297f0b85ae256b886f4e686
5
5
  SHA512:
6
- metadata.gz: a2d2b35cbd46653fba718db0c30fe0cea8ea5b5669685b2886f9b74bb8236c3c52d02bb0dc0bb6387654e05ddbe6ee4d7fb4505c797cbff13f093eba5d9c79e9
7
- data.tar.gz: 5aa0e9bba24d471de9b04c369b93979cc2d9882326caf80f0654d6c31adbd37ec2bdb2e76a8402f0497bf4c8edaeb380bb24b004fb1d6611596f87f6fcc4349a
6
+ metadata.gz: 28b06a12e7f183d48087c15355a784db6efc4e07ca0628d259b67bd354e3777f5580d2ee3b02f6c8811146457e362aee6330af9ffa648a23162850d2bb87ec5d
7
+ data.tar.gz: add9a4eebfc6e65499e528ffab4cfbc25719a7b17053ff040c1a99e5ea7cf4649876341b6e15f68326b950846c3897e2fa4f51747ca68127232d9410eb4a3d72
data/README.md CHANGED
@@ -92,6 +92,17 @@ you'd get this instead:
92
92
 
93
93
  [user-docs]: ./docs/users/getting-started.md
94
94
 
95
+ ### Optional Extensions
96
+
97
+ If you need diffs for binary strings (`Encoding::ASCII_8BIT`),
98
+ require the binary string integration:
99
+
100
+ ```ruby
101
+ require "super_diff/binary_string"
102
+ ```
103
+
104
+ This enables hex-dump diffs and keeps binary data out of the expectation text.
105
+
95
106
  ## Support
96
107
 
97
108
  My goal for this library is to improve your development experience.
@@ -113,7 +124,7 @@ for more on how to do that.
113
124
  `super_diff` is [tested][gh-actions] to work with
114
125
  Ruby >= 3.1,
115
126
  RSpec 3.x,
116
- and Rails >= 6.1.
127
+ and Rails >= 7.0.
117
128
 
118
129
  [gh-actions]: https://github.com/splitwise/super_diff/actions?query=workflow%3ASuperDiff
119
130
 
@@ -43,6 +43,13 @@ module SuperDiff
43
43
 
44
44
  attr_reader :sequence_matcher, :original_expected, :original_actual
45
45
 
46
+ # override
47
+ def should_compare?(_operation, _next_operation)
48
+ # Don't try to build a nested operation tree for individual line changes, even if
49
+ # there's an applicable string operation tree builder.
50
+ false
51
+ end
52
+
46
53
  def split_into_lines(string)
47
54
  string.scan(/.*(?:\r|\n|\r\n|\Z)/)
48
55
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module Differs
6
+ class BinaryString < Core::AbstractDiffer
7
+ def self.applies_to?(expected, actual)
8
+ SuperDiff::BinaryString.applies_to?(expected, actual)
9
+ end
10
+
11
+ protected
12
+
13
+ def operation_tree_builder_class
14
+ OperationTreeBuilders::BinaryString
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module Differs
6
+ autoload(
7
+ :BinaryString,
8
+ 'super_diff/binary_string/differs/binary_string'
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module InspectionTreeBuilders
6
+ class BinaryString < Core::AbstractInspectionTreeBuilder
7
+ def self.applies_to?(value)
8
+ SuperDiff::BinaryString.applies_to?(value)
9
+ end
10
+
11
+ def call
12
+ Core::InspectionTree.new do |t|
13
+ t.add_text "<binary string (#{object.bytesize} bytes)>"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module InspectionTreeBuilders
6
+ autoload(
7
+ :BinaryString,
8
+ 'super_diff/binary_string/inspection_tree_builders/binary_string'
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTreeBuilders
6
+ class BinaryString < Basic::OperationTreeBuilders::MultilineString
7
+ BYTES_PER_LINE = 16
8
+ private_constant :BYTES_PER_LINE
9
+
10
+ def self.applies_to?(expected, actual)
11
+ SuperDiff::BinaryString.applies_to?(expected, actual)
12
+ end
13
+
14
+ def initialize(*args)
15
+ args.first[:expected] = binary_to_hex(args.first[:expected])
16
+ args.first[:actual] = binary_to_hex(args.first[:actual])
17
+
18
+ super
19
+ end
20
+
21
+ protected
22
+
23
+ def build_operation_tree
24
+ OperationTrees::BinaryString.new([])
25
+ end
26
+
27
+ private
28
+
29
+ def split_into_lines(string)
30
+ super.map { |line| line.delete_suffix("\n") }.reject(&:empty?)
31
+ end
32
+
33
+ def binary_to_hex(data)
34
+ data
35
+ .each_byte
36
+ .each_slice(BYTES_PER_LINE)
37
+ .with_index
38
+ .map { |bytes, index| format_hex_line(index * BYTES_PER_LINE, bytes) }
39
+ .join("\n")
40
+ end
41
+
42
+ def format_hex_line(offset, bytes)
43
+ hex_pairs = bytes
44
+ .map { |b| format('%02x', b) }
45
+ .each_slice(2)
46
+ .map(&:join)
47
+ .join(' ')
48
+
49
+ ascii = bytes.map { |b| printable_char(b) }.join
50
+
51
+ format('%<offset>08x: %<hex>-39s %<ascii>s', offset:, hex: hex_pairs, ascii:)
52
+ end
53
+
54
+ def printable_char(byte)
55
+ byte >= 32 && byte < 127 ? byte.chr : '.'
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTreeBuilders
6
+ autoload(
7
+ :BinaryString,
8
+ 'super_diff/binary_string/operation_tree_builders/binary_string'
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTreeFlatteners
6
+ class BinaryString < Core::AbstractOperationTreeFlattener
7
+ def build_tiered_lines
8
+ operation_tree.map do |operation|
9
+ Core::Line.new(
10
+ type: operation.name,
11
+ indentation_level: indentation_level,
12
+ value: operation.value
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTreeFlatteners
6
+ autoload(
7
+ :BinaryString,
8
+ 'super_diff/binary_string/operation_tree_flatteners/binary_string'
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTrees
6
+ class BinaryString < Core::AbstractOperationTree
7
+ def self.applies_to?(value)
8
+ SuperDiff::BinaryString.applies_to?(value)
9
+ end
10
+
11
+ protected
12
+
13
+ def operation_tree_flattener_class
14
+ OperationTreeFlatteners::BinaryString
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperDiff
4
+ module BinaryString
5
+ module OperationTrees
6
+ autoload(
7
+ :BinaryString,
8
+ 'super_diff/binary_string/operation_trees/binary_string'
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'super_diff/binary_string/differs'
4
+ require 'super_diff/binary_string/inspection_tree_builders'
5
+ require 'super_diff/binary_string/operation_trees'
6
+ require 'super_diff/binary_string/operation_tree_builders'
7
+ require 'super_diff/binary_string/operation_tree_flatteners'
8
+
9
+ module SuperDiff
10
+ module BinaryString
11
+ def self.applies_to?(*values)
12
+ values.all? { |value| value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT }
13
+ end
14
+
15
+ SuperDiff.configure do |config|
16
+ config.prepend_extra_differ_classes(Differs::BinaryString)
17
+ config.prepend_extra_inspection_tree_builder_classes(
18
+ InspectionTreeBuilders::BinaryString
19
+ )
20
+ end
21
+ end
22
+ end
@@ -41,16 +41,7 @@ module SuperDiff
41
41
 
42
42
  def panes
43
43
  @panes ||=
44
- BuildPanes.call(dirty_panes: padded_dirty_panes, lines: lines)
45
- end
46
-
47
- def padded_dirty_panes
48
- @padded_dirty_panes ||=
49
- combine_congruent_panes(
50
- dirty_panes
51
- .map(&:padded)
52
- .map { |pane| pane.capped_to(0, lines.size - 1) }
53
- )
44
+ BuildPanes.call(dirty_panes: dirty_panes, lines: lines)
54
45
  end
55
46
 
56
47
  def dirty_panes
@@ -109,8 +100,8 @@ module SuperDiff
109
100
 
110
101
  def all_indentation_levels
111
102
  lines
103
+ .reject(&:complete_bookend?)
112
104
  .map(&:indentation_level)
113
- .select(&:positive?)
114
105
  .uniq
115
106
  end
116
107
 
@@ -140,19 +131,15 @@ module SuperDiff
140
131
  def normalized_box_groups_at_decreasing_indentation_levels_within(pane)
141
132
  box_groups_at_decreasing_indentation_levels_within(pane).map(
142
133
  &method(:filter_out_boxes_fully_contained_in_others)
143
- ).map(&method(:combine_congruent_boxes))
134
+ ).map(&method(:combine_contiguous_boxes))
144
135
  end
145
136
 
146
137
  def box_groups_at_decreasing_indentation_levels_within(pane)
147
138
  boxes_within_pane = boxes.select { |box| box.fits_fully_within?(pane) }
148
139
 
149
- possible_indentation_levels =
150
- boxes_within_pane
151
- .map(&:indentation_level)
152
- .select(&:positive?)
153
- .uniq
154
- .sort
155
- .reverse
140
+ levels = boxes_within_pane.map(&:indentation_level).uniq
141
+
142
+ possible_indentation_levels = levels.sort.reverse
156
143
 
157
144
  possible_indentation_levels.map do |indentation_level|
158
145
  boxes_within_pane.select do |box|
@@ -162,26 +149,29 @@ module SuperDiff
162
149
  end
163
150
 
164
151
  def filter_out_boxes_fully_contained_in_others(boxes)
165
- sorted_boxes =
166
- boxes.sort_by do |box|
167
- [box.indentation_level, box.range.begin, box.range.end]
168
- end
169
-
170
- boxes.reject do |box2|
171
- sorted_boxes.any? do |box1|
172
- !box1.equal?(box2) && box1.fully_contains?(box2)
173
- end
152
+ # First, sorts boxes by beginning ascending, range descending. (Boxes may
153
+ # never share beginnings, so the latter may be useless, but this is at least
154
+ # sufficient if unnecessary.)
155
+ #
156
+ # Then, iterate through each box, keeping track of the farthest "end" of any
157
+ # box seen so far. If the current box we are on ends before (or on) that farthest
158
+ # end, we know there is some box earlier in the sequence that begins <= this one
159
+ # (because of the prior sorting), and ends >= this one; that is, the current box
160
+ # is fully contained, and we can filter it out.
161
+ sorted = boxes.sort_by { |box| [box.range.begin, -box.range.end] }
162
+ max_end = -1
163
+
164
+ sorted.reject do |box|
165
+ contained = box.range.end <= max_end
166
+ max_end = box.range.end if box.range.end > max_end
167
+ contained
174
168
  end
175
169
  end
176
170
 
177
- def combine_congruent_boxes(boxes)
171
+ def combine_contiguous_boxes(boxes)
178
172
  combine(boxes, on: :indentation_level)
179
173
  end
180
174
 
181
- def combine_congruent_panes(panes)
182
- combine(panes, on: :type)
183
- end
184
-
185
175
  def combine(spannables, on:)
186
176
  criterion = on
187
177
  spannables.reduce([]) do |combined_spannables, spannable|
@@ -347,19 +337,6 @@ module SuperDiff
347
337
  def extended_to(new_end)
348
338
  self.class.new(type: type, range: range.begin..new_end)
349
339
  end
350
-
351
- def padded
352
- self.class.new(type: type, range: Range.new(range.begin, range.end))
353
- end
354
-
355
- def capped_to(beginning, ending)
356
- new_beginning = [range.begin, beginning].max
357
- new_ending = [range.end, ending].min
358
- self.class.new(
359
- type: type,
360
- range: Range.new(new_beginning, new_ending)
361
- )
362
- end
363
340
  end
364
341
 
365
342
  class BuildBoxes
@@ -42,10 +42,16 @@ module SuperDiff
42
42
  end
43
43
 
44
44
  def comparing_singleline_strings?
45
+ return false if comparing_binary_strings?
46
+
45
47
  expected.is_a?(String) && actual.is_a?(String) &&
46
48
  !expected.include?("\n") && !actual.include?("\n")
47
49
  end
48
50
 
51
+ def comparing_binary_strings?
52
+ defined?(BinaryString) && BinaryString.applies_to?(expected, actual)
53
+ end
54
+
49
55
  def helpers
50
56
  @helpers ||= RSpecHelpers.new
51
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SuperDiff
4
- VERSION = '0.18.0'
4
+ VERSION = '0.19.0'
5
5
  end
data/super_diff.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
27
27
  s.files = %w[README.md super_diff.gemspec] + Dir['lib/**/*']
28
28
  s.executables = Dir['exe/**/*'].map { |f| File.basename(f) }
29
29
 
30
- s.add_dependency 'attr_extras', '>= 6.2.4'
31
- s.add_dependency 'diff-lcs'
32
- s.add_dependency 'patience_diff'
30
+ s.add_dependency 'attr_extras', '>= 6.2.4', '< 8'
31
+ s.add_dependency 'diff-lcs', '~> 1.5'
32
+ s.add_dependency 'patience_diff', '~> 1.2'
33
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: super_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elliot Winkler
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-12-05 00:00:00.000000000 Z
12
+ date: 2026-05-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: attr_extras
@@ -18,6 +18,9 @@ dependencies:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: 6.2.4
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '8'
21
24
  type: :runtime
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -25,34 +28,37 @@ dependencies:
25
28
  - - ">="
26
29
  - !ruby/object:Gem::Version
27
30
  version: 6.2.4
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '8'
28
34
  - !ruby/object:Gem::Dependency
29
35
  name: diff-lcs
30
36
  requirement: !ruby/object:Gem::Requirement
31
37
  requirements:
32
- - - ">="
38
+ - - "~>"
33
39
  - !ruby/object:Gem::Version
34
- version: '0'
40
+ version: '1.5'
35
41
  type: :runtime
36
42
  prerelease: false
37
43
  version_requirements: !ruby/object:Gem::Requirement
38
44
  requirements:
39
- - - ">="
45
+ - - "~>"
40
46
  - !ruby/object:Gem::Version
41
- version: '0'
47
+ version: '1.5'
42
48
  - !ruby/object:Gem::Dependency
43
49
  name: patience_diff
44
50
  requirement: !ruby/object:Gem::Requirement
45
51
  requirements:
46
- - - ">="
52
+ - - "~>"
47
53
  - !ruby/object:Gem::Version
48
- version: '0'
54
+ version: '1.2'
49
55
  type: :runtime
50
56
  prerelease: false
51
57
  version_requirements: !ruby/object:Gem::Requirement
52
58
  requirements:
53
- - - ">="
59
+ - - "~>"
54
60
  - !ruby/object:Gem::Version
55
- version: '0'
61
+ version: '1.2'
56
62
  description: |
57
63
  SuperDiff is a gem that hooks into RSpec to intelligently display the
58
64
  differences between two data structures of any type.
@@ -139,6 +145,17 @@ files:
139
145
  - lib/super_diff/basic/operation_trees/default_object.rb
140
146
  - lib/super_diff/basic/operation_trees/hash.rb
141
147
  - lib/super_diff/basic/operation_trees/multiline_string.rb
148
+ - lib/super_diff/binary_string.rb
149
+ - lib/super_diff/binary_string/differs.rb
150
+ - lib/super_diff/binary_string/differs/binary_string.rb
151
+ - lib/super_diff/binary_string/inspection_tree_builders.rb
152
+ - lib/super_diff/binary_string/inspection_tree_builders/binary_string.rb
153
+ - lib/super_diff/binary_string/operation_tree_builders.rb
154
+ - lib/super_diff/binary_string/operation_tree_builders/binary_string.rb
155
+ - lib/super_diff/binary_string/operation_tree_flatteners.rb
156
+ - lib/super_diff/binary_string/operation_tree_flatteners/binary_string.rb
157
+ - lib/super_diff/binary_string/operation_trees.rb
158
+ - lib/super_diff/binary_string/operation_trees/binary_string.rb
142
159
  - lib/super_diff/core.rb
143
160
  - lib/super_diff/core/abstract_differ.rb
144
161
  - lib/super_diff/core/abstract_inspection_tree_builder.rb