specdiff 0.3.0.pre.rc1 → 0.3.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.
@@ -13,10 +13,11 @@ class Specdiff::Hashprint
13
13
  NEWLINE = "\n".freeze
14
14
 
15
15
  def call(thing)
16
+ @recursion_trail = []
16
17
  @indentation_level = 0
17
18
  @indentation_per_level = SPACE * INDENTATION_SPACES
18
19
  @indent = ""
19
- @skip_next_opening_indent = false
20
+ @next_value_is_hash_value = false
20
21
 
21
22
  @output = StringIO.new
22
23
 
@@ -41,37 +42,39 @@ private
41
42
  recalculate_indent
42
43
  end
43
44
 
44
- def with_indentation_level(temporary_level)
45
- old_level = @indentation_level
46
- @indentation_level = temporary_level
47
- recalculate_indent
48
-
49
- yield
50
-
51
- @indentation_level = old_level
52
- recalculate_indent
53
- end
54
-
55
- def skip_next_opening_indent
56
- @skip_next_opening_indent = true
45
+ def next_value_is_hash_value
46
+ @next_value_is_hash_value = true
57
47
 
58
48
  nil
59
49
  end
60
50
 
61
- def this_indent_should_be_skipped
62
- if @skip_next_opening_indent
63
- @skip_next_opening_indent = false
51
+ def outputting_hash_value?
52
+ if @next_value_is_hash_value
53
+ @next_value_is_hash_value = false
64
54
  true
65
55
  else
66
56
  false
67
57
  end
68
58
  end
69
59
 
60
+ def track_recursion(thing)
61
+ @recursion_trail.push(thing)
62
+ result = yield
63
+ @recursion_trail.pop
64
+ result
65
+ end
66
+
67
+ def deja_vu?(current_place)
68
+ @recursion_trail.any? { |previous_place| previous_place == current_place }
69
+ end
70
+
70
71
  # #=== allows us to rely on Module implementing #=== instead of relying on the
71
72
  # thing (which could be any kind of wacky object) having to implement
72
73
  # #is_a? or #kind_of?
73
74
  def output(thing)
74
- if Hash === thing
75
+ if deja_vu?(thing)
76
+ output_deja_vu(thing)
77
+ elsif Hash === thing
75
78
  output_hash(thing)
76
79
  elsif Array === thing
77
80
  output_array(thing)
@@ -86,69 +89,93 @@ private
86
89
  COLON = ":".freeze
87
90
 
88
91
  def output_hash(hash)
89
- @output << @indent unless this_indent_should_be_skipped
92
+ @output << @indent unless outputting_hash_value?
90
93
 
91
94
  @output << HASH_OPEN
92
- # unless hash.empty?
93
- @output << NEWLINE
94
-
95
- increase_indentation
96
- hash.each do |key, value|
97
- @output << @indent
98
-
99
- if key.is_a?(Symbol)
100
- @output << key
101
- @output << COLON
102
- @output << SPACE
103
- else
104
- @output << ::Specdiff.diff_inspect(key)
105
- @output << SPACE
106
- @output << HASHROCKET
107
- @output << SPACE
95
+ @output << NEWLINE
96
+
97
+ increase_indentation
98
+ track_recursion(hash) do
99
+ try_to_sort(hash)
100
+ .each do |key, value|
101
+ @output << @indent
102
+
103
+ if key.is_a?(Symbol)
104
+ @output << key
105
+ @output << COLON
106
+ @output << SPACE
107
+ else
108
+ @output << ::Specdiff.diff_inspect(key)
109
+ @output << SPACE
110
+ @output << HASHROCKET
111
+ @output << SPACE
112
+ end
113
+
114
+ next_value_is_hash_value
115
+ output(value)
116
+
117
+ @output << COMMA
118
+ @output << NEWLINE
108
119
  end
120
+ end
121
+ decrease_indentation
109
122
 
110
- skip_next_opening_indent
111
- output(value)
112
-
113
- @output << COMMA
114
- @output << NEWLINE
115
- end
116
- decrease_indentation
123
+ @output << @indent
124
+ @output << HASH_CLOSE
125
+ end
117
126
 
118
- @output << @indent
119
- # end
127
+ def try_to_sort(hash)
128
+ return hash unless hash.keys.all? { |k| String === k || Symbol === k }
120
129
 
121
- @output << HASH_CLOSE
130
+ hash.sort_by { |k, _v| k.to_s }
122
131
  end
123
132
 
124
133
  ARRAY_OPEN = "[".freeze
125
134
  ARRAY_CLOSE = "]".freeze
126
135
 
127
136
  def output_array(array)
128
- @output << @indent unless this_indent_should_be_skipped
137
+ @output << @indent unless outputting_hash_value?
129
138
 
130
139
  @output << ARRAY_OPEN
140
+ @output << NEWLINE
131
141
 
132
- # unless array.empty?
133
- @output << NEWLINE
134
-
135
- increase_indentation
142
+ increase_indentation
143
+ track_recursion(array) do
136
144
  array.each do |element|
137
145
  output(element)
138
146
  @output << COMMA
139
147
  @output << NEWLINE
140
148
  end
141
- decrease_indentation
142
-
143
- @output << @indent
144
- # end
149
+ end
150
+ decrease_indentation
145
151
 
152
+ @output << @indent
146
153
  @output << ARRAY_CLOSE
147
154
  end
148
155
 
149
156
  def output_unknown(thing)
150
- @output << @indent unless this_indent_should_be_skipped
157
+ @output << @indent unless outputting_hash_value?
151
158
 
152
159
  @output << ::Specdiff.diff_inspect(thing)
153
160
  end
161
+
162
+ # The stdlib inspect code returns this when you have recursive structures.
163
+ STANDARD_INSPECT_RECURSIVE_ARRAY = "[...]".freeze
164
+ STANDARD_INSPECT_RECURSIVE_HASH = "{...}".freeze
165
+
166
+ def output_deja_vu(thing)
167
+ @output << @indent unless outputting_hash_value?
168
+
169
+ case thing
170
+ when Array
171
+ # "#<Array ##{thing.object_id}>"
172
+ @output << STANDARD_INSPECT_RECURSIVE_ARRAY
173
+ when Hash
174
+ # "#<Hash ##{thing.object_id}>"
175
+ @output << STANDARD_INSPECT_RECURSIVE_HASH
176
+ else
177
+ # this should never happen
178
+ raise "Specdiff::Hashprint missing deja vu for: #{thing.inspect}"
179
+ end
180
+ end
154
181
  end
@@ -6,11 +6,17 @@ class Specdiff::Inspect
6
6
  new.call(...)
7
7
  end
8
8
 
9
+ def initialize
10
+ @recursion_trail = []
11
+ end
12
+
9
13
  # #=== allows us to rely on Module implementing #=== instead of relying on the
10
14
  # thing (which could be any kind of wacky object) having to implement
11
15
  # #is_a? or #kind_of?
12
16
  def call(thing)
13
- if Time === thing
17
+ if Hash === thing || Array === thing
18
+ recursive_replace_inspect(thing).inspect
19
+ elsif Time === thing
14
20
  "#<Time: #{thing.strftime(TIME_FORMAT)}>"
15
21
  elsif DateTime === thing
16
22
  "#<DateTime: #{thing.rfc3339}>"
@@ -18,6 +24,12 @@ class Specdiff::Inspect
18
24
  "#<Date: #{thing.strftime(DATE_FORMAT)}>"
19
25
  elsif defined?(BigDecimal) && BigDecimal === thing
20
26
  "#<BigDecimal: #{thing.to_s('F')}>"
27
+ elsif rspec_matcher?(thing)
28
+ # Turns out rspec depends on the recursion in its inspection logic to
29
+ # print the "description" of rspec matchers, in situations such as when
30
+ # using multi-matchers (.all, .or or .and), or when nesting them inside
31
+ # eachother (such as match([have_attributes(...)])).
32
+ thing.description
21
33
  else
22
34
  begin
23
35
  thing.inspect
@@ -27,6 +39,12 @@ class Specdiff::Inspect
27
39
  end
28
40
  end
29
41
 
42
+ private def rspec_matcher?(thing)
43
+ defined?(::Specdiff::RSpecIntegration) &&
44
+ ::RSpec::Support.is_a_matcher?(thing) &&
45
+ thing.respond_to?(:description)
46
+ end
47
+
30
48
  private def inspect_anyway(uninspectable)
31
49
  "#<uninspectable #{class_of(uninspectable)}>"
32
50
  end
@@ -38,4 +56,79 @@ class Specdiff::Inspect
38
56
  singleton_class.ancestors
39
57
  .find { |ancestor| !ancestor.equal?(singleton_class) }
40
58
  end
59
+
60
+ # recursion below
61
+
62
+ InspectWrapper = Struct.new(:text) do
63
+ def inspect
64
+ text
65
+ end
66
+ end
67
+
68
+ private def recursive_replace_inspect(thing)
69
+ if deja_vu?(thing)
70
+ # I've just been in this place before
71
+ # And I know it's my time to go...
72
+ return InspectWrapper.new(inspect_deja_vu(thing))
73
+ end
74
+
75
+ case thing
76
+ when Array
77
+ track_recursion(thing) do
78
+ thing.map { |element| recursive_replace_inspect(element) }
79
+ end
80
+ when Hash
81
+ track_recursion(thing) do
82
+ new_hash = {}
83
+
84
+ thing.each do |key, value|
85
+ new_hash[recursive_replace_inspect(key)] = recursive_replace_inspect(value)
86
+ end
87
+
88
+ new_hash
89
+ end
90
+ else
91
+ wrap_inspect(thing)
92
+ end
93
+ rescue SystemStackError => e
94
+ wrap_inspect(
95
+ thing,
96
+ text: "#{e.class}: #{e.message}\n\n" \
97
+ "encountered when inspecting #{thing.inspect}"
98
+ )
99
+ end
100
+
101
+ private def track_recursion(thing)
102
+ @recursion_trail.push(thing)
103
+ result = yield
104
+ @recursion_trail.pop
105
+ result
106
+ end
107
+
108
+ private def deja_vu?(current_place)
109
+ @recursion_trail.any? { |previous_place| previous_place == current_place }
110
+ end
111
+
112
+ private def wrap_inspect(thing, text: :_use_diff_inspect)
113
+ text = call(thing) if text == :_use_diff_inspect
114
+ InspectWrapper.new(text)
115
+ end
116
+
117
+ # The stdlib inspect code returns this when you have recursive structures.
118
+ STANDARD_INSPECT_RECURSIVE_ARRAY = "[...]".freeze
119
+ STANDARD_INSPECT_RECURSIVE_HASH = "{...}".freeze
120
+
121
+ private def inspect_deja_vu(thing)
122
+ case thing
123
+ when Array
124
+ # "#<Array ##{thing.object_id}>"
125
+ STANDARD_INSPECT_RECURSIVE_ARRAY
126
+ when Hash
127
+ # "#<Hash ##{thing.object_id}>"
128
+ STANDARD_INSPECT_RECURSIVE_HASH
129
+ else
130
+ # this should never happen
131
+ raise "Specdiff::Inspect missing deja vu for: #{thing.inspect}"
132
+ end
133
+ end
41
134
  end
@@ -1,3 +1,7 @@
1
+ raise "rspec must be required before specdiff/rspec!" unless defined?(RSpec)
2
+ raise "RSpec::Support is missing????" unless defined?(RSpec::Support)
3
+
4
+ # https://github.com/rspec/rspec-support/blob/v3.13.1/lib/rspec/support/differ.rb
1
5
  class RSpec::Support::Differ
2
6
  alias old_diff diff
3
7
 
@@ -13,21 +17,12 @@ end
13
17
 
14
18
  # This stops rspec from truncating strings w/ ellipsis, as well as making the
15
19
  # "inspect" output consistent with specdiff's.
20
+ # https://github.com/rspec/rspec-support/blob/v3.13.1/lib/rspec/support/object_formatter.rb
16
21
  class RSpec::Support::ObjectFormatter
17
- class SpecdiffCustomInspector < BaseInspector
18
- def self.can_inspect?(_)
19
- true
20
- end
21
-
22
- def inspect
23
- ::Specdiff.diff_inspect(object)
24
- end
25
- end
26
-
27
- remove_const("INSPECTOR_CLASSES")
28
- const_set("INSPECTOR_CLASSES", [SpecdiffCustomInspector])
29
-
30
22
  def format(object)
31
23
  ::Specdiff.diff_inspect(object)
32
24
  end
33
25
  end
26
+
27
+ # marker for successfully loading this integration
28
+ class Specdiff::RSpecIntegration; end # rubocop: disable Lint/EmptyClass
@@ -1,3 +1,3 @@
1
1
  module Specdiff
2
- VERSION = "0.3.0-rc1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,6 +1,6 @@
1
- require "hashdiff"
2
- require "json"
1
+ raise "webmock must be required before specdiff/webmock" unless defined?(WebMock)
3
2
 
3
+ # https://github.com/bblimke/webmock/blob/v3.23.0/lib/webmock/request_body_diff.rb
4
4
  module WebMock
5
5
  class RequestBodyDiff
6
6
  def initialize(request_signature, request_stub)
@@ -39,3 +39,6 @@ module WebMock
39
39
  end
40
40
  end
41
41
  end
42
+
43
+ # marker for successfully loading this integration
44
+ class Specdiff::WebmockIntegration; end # rubocop: disable Lint/EmptyClass
data/lib/specdiff.rb CHANGED
@@ -6,19 +6,14 @@ require_relative "specdiff/hashprint"
6
6
  require_relative "specdiff/compare"
7
7
 
8
8
  module Specdiff
9
- # Compare two things, returns a Specdiff::Diff.
10
9
  def self.diff(...)
11
10
  ::Specdiff::Compare.call(...)
12
11
  end
13
12
 
14
- # Use Specdiff's implementation for turning a nested hash/array structure
15
- # into a string. Optimized for diff quality.
16
13
  def self.hashprint(...)
17
14
  ::Specdiff::Hashprint.call(...)
18
15
  end
19
16
 
20
- # Use Specdiff's inspect, which has some extra logic layered in for
21
- # dates/time/bigdecimal. For most objects this just delegates to #inspect.
22
17
  def self.diff_inspect(...)
23
18
  ::Specdiff::Inspect.call(...)
24
19
  end
data/specdiff.gemspec CHANGED
@@ -6,10 +6,18 @@ Gem::Specification.new do |spec|
6
6
  spec.name = "specdiff"
7
7
  spec.version = Specdiff::VERSION
8
8
  spec.authors = ["Odin Heggvold Bekkelund"]
9
- spec.email = ["odinhb@protonmail.com"]
9
+ spec.email = ["odin.heggvold.bekkelund@dev.dodo.no"]
10
10
 
11
- spec.summary = "Improved request body diffs for webmock"
12
- spec.homepage = "https://github.com/odinhb/specdiff"
11
+ spec.summary = "Improved diffing for WebMock and RSpec"
12
+ spec.description = <<~TXT
13
+ Specdiff aims to improve both RSpec's and WebMock's diffing by applying \
14
+ opinionated heuristics, and comes with integrations (monkey-patches) for \
15
+ both. Particularly noteworthy improvements are made to working with deeply \
16
+ nested hash/array structures in RSpec, and plaintext/xml request bodies in \
17
+ WebMock.
18
+ TXT
19
+
20
+ spec.homepage = "https://github.com/dodoas/specdiff"
13
21
  spec.license = "MIT"
14
22
  spec.required_ruby_version = ">= 3.0.0"
15
23
 
@@ -17,13 +25,12 @@ Gem::Specification.new do |spec|
17
25
  spec.metadata["source_code_uri"] = spec.homepage
18
26
  spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
19
27
 
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
- `git ls-files -z`.split("\x0").reject do |f|
24
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
- end
26
- end
28
+ spec.files =
29
+ Dir["*.gemspec"] +
30
+ Dir["*.md"] +
31
+ Dir["*.txt"] +
32
+ Dir[".gitignore"] +
33
+ Dir["lib/**/*.rb"]
27
34
  spec.require_paths = ["lib"]
28
35
 
29
36
  spec.add_dependency "hashdiff", "~> 1.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specdiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.pre.rc1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Odin Heggvold Bekkelund
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-02 00:00:00.000000000 Z
11
+ date: 2024-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashdiff
@@ -38,33 +38,23 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.5'
41
- description:
41
+ description: 'Specdiff aims to improve both RSpec''s and WebMock''s diffing by applying
42
+ opinionated heuristics, and comes with integrations (monkey-patches) for both. Particularly
43
+ noteworthy improvements are made to working with deeply nested hash/array structures
44
+ in RSpec, and plaintext/xml request bodies in WebMock.
45
+
46
+ '
42
47
  email:
43
- - odinhb@protonmail.com
48
+ - odin.heggvold.bekkelund@dev.dodo.no
44
49
  executables: []
45
50
  extensions: []
46
51
  extra_rdoc_files: []
47
52
  files:
48
- - ".rspec"
49
- - ".rubocop.yml"
50
- - ".tool-versions"
53
+ - ".gitignore"
51
54
  - CHANGELOG.md
52
- - Gemfile
53
- - Gemfile.lock
55
+ - DEVELOPMENT.md
54
56
  - LICENSE.txt
55
57
  - README.md
56
- - Rakefile
57
- - assets/webmock_json_with_specdiff.png
58
- - assets/webmock_text_with_specdiff.png
59
- - examples/rspec/.rspec
60
- - examples/rspec/Gemfile
61
- - examples/rspec/Gemfile.lock
62
- - examples/rspec/spec/example_spec.rb
63
- - examples/rspec/spec/spec_helper.rb
64
- - examples/webmock/Gemfile
65
- - examples/webmock/Gemfile.lock
66
- - examples/webmock/json.rb
67
- - examples/webmock/text.rb
68
58
  - glossary.txt
69
59
  - lib/specdiff.rb
70
60
  - lib/specdiff/colorize.rb
@@ -72,7 +62,7 @@ files:
72
62
  - lib/specdiff/config.rb
73
63
  - lib/specdiff/diff.rb
74
64
  - lib/specdiff/differ.rb
75
- - lib/specdiff/differ/hashdiff.rb
65
+ - lib/specdiff/differ/hash.rb
76
66
  - lib/specdiff/differ/not_found.rb
77
67
  - lib/specdiff/differ/text.rb
78
68
  - lib/specdiff/hashprint.rb
@@ -84,13 +74,13 @@ files:
84
74
  - lib/specdiff/version.rb
85
75
  - lib/specdiff/webmock.rb
86
76
  - specdiff.gemspec
87
- homepage: https://github.com/odinhb/specdiff
77
+ homepage: https://github.com/dodoas/specdiff
88
78
  licenses:
89
79
  - MIT
90
80
  metadata:
91
- homepage_uri: https://github.com/odinhb/specdiff
92
- source_code_uri: https://github.com/odinhb/specdiff
93
- changelog_uri: https://github.com/odinhb/specdiff/CHANGELOG.md
81
+ homepage_uri: https://github.com/dodoas/specdiff
82
+ source_code_uri: https://github.com/dodoas/specdiff
83
+ changelog_uri: https://github.com/dodoas/specdiff/CHANGELOG.md
94
84
  post_install_message:
95
85
  rdoc_options: []
96
86
  require_paths:
@@ -102,12 +92,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
92
  version: 3.0.0
103
93
  required_rubygems_version: !ruby/object:Gem::Requirement
104
94
  requirements:
105
- - - ">"
95
+ - - ">="
106
96
  - !ruby/object:Gem::Version
107
- version: 1.3.1
97
+ version: '0'
108
98
  requirements: []
109
99
  rubygems_version: 3.2.22
110
100
  signing_key:
111
101
  specification_version: 4
112
- summary: Improved request body diffs for webmock
102
+ summary: Improved diffing for WebMock and RSpec
113
103
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper