specdiff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7da7ac63f4638bd752f83ff41a27488757e43e57c028891d4fd944afd03f58ca
4
+ data.tar.gz: d5a49e6aeb3525a454df920aa7d1b541a0812ffb9f8d0e1813a4bc05b57b006f
5
+ SHA512:
6
+ metadata.gz: 38ae83cd45a69892ca6d22915e0b253447eb12f42165a7673751abbb3993381702a02771ad3181a96d26204e6d171268a14423e85c2323a345df243be918f5a5
7
+ data.tar.gz: f44ce432951f3e2198ed6c4ed4cfc975b4eb201ced235791cd917c2476ca7a60fcb4ce0aff023fd4423f97b7eee2819d395d514b970188a5bc232ce89ee44ea6
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,203 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ require:
5
+ - rubocop-rspec
6
+ - rubocop-rake
7
+
8
+ ##
9
+ # LAYOUT COPS
10
+ ##
11
+
12
+ Layout:
13
+ Enabled: false
14
+
15
+ Layout/LineLength:
16
+ Enabled: true
17
+ Max: 120
18
+
19
+ Layout/SpaceInsideBlockBraces:
20
+ Enabled: true
21
+ EnforcedStyle: space
22
+ SpaceBeforeBlockParameters: no_space
23
+ Layout/SpaceInsideHashLiteralBraces:
24
+ Enabled: true
25
+ EnforcedStyle: no_space
26
+
27
+ Layout/MultilineMethodCallIndentation:
28
+ Enabled: true
29
+ EnforcedStyle: indented
30
+
31
+ ##
32
+ # STYLE COPS
33
+ ##
34
+
35
+ Style:
36
+ Enabled: false
37
+
38
+ Style/TrailingCommaInArrayLiteral:
39
+ Enabled: true
40
+ EnforcedStyleForMultiline: consistent_comma
41
+
42
+ Style/TrailingCommaInHashLiteral:
43
+ Enabled: true
44
+ EnforcedStyleForMultiline: consistent_comma
45
+
46
+ Style/SpecialGlobalVars:
47
+ Enabled: true
48
+ EnforcedStyle: use_english_names
49
+
50
+ ##
51
+ # NAMING COPS
52
+ ##
53
+
54
+ Naming/BlockForwarding: # new in 1.24
55
+ Enabled: false
56
+ Naming/MethodParameterName:
57
+ Enabled: false
58
+
59
+ ##
60
+ # SECURITY COPS
61
+ ##
62
+
63
+ Security/CompoundHash: # new in 1.28
64
+ Enabled: true
65
+ Security/IoMethods: # new in 1.22
66
+ Enabled: true
67
+
68
+ ##
69
+ # LINT COPS
70
+ ##
71
+
72
+ Lint/SuppressedException:
73
+ AllowComments: true
74
+ Lint/AmbiguousAssignment: # new in 1.7
75
+ Enabled: true
76
+ Lint/AmbiguousOperatorPrecedence: # new in 1.21
77
+ Enabled: true
78
+ Lint/AmbiguousRange: # new in 1.19
79
+ Enabled: true
80
+ Lint/ConstantOverwrittenInRescue: # new in 1.31
81
+ Enabled: true
82
+ Lint/DeprecatedConstants: # new in 1.8
83
+ Enabled: true
84
+ Lint/DuplicateBranch: # new in 1.3
85
+ Enabled: true
86
+ IgnoreLiteralBranches: true
87
+ IgnoreConstantBranches: true
88
+ Lint/DuplicateMagicComment: # new in 1.37
89
+ Enabled: true
90
+ Lint/DuplicateMatchPattern: # new in 1.50
91
+ Enabled: true
92
+ Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
93
+ Enabled: true
94
+ Lint/EmptyBlock: # new in 1.1
95
+ Enabled: true
96
+ Lint/EmptyClass: # new in 1.3
97
+ Enabled: true
98
+ Lint/EmptyInPattern: # new in 1.16
99
+ Enabled: true
100
+ Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
101
+ Enabled: true
102
+ Lint/LambdaWithoutLiteralBlock: # new in 1.8
103
+ Enabled: true
104
+ Lint/MixedCaseRange: # new in 1.53
105
+ Enabled: true
106
+ Lint/NoReturnInBeginEndBlocks: # new in 1.2
107
+ Enabled: true
108
+ Lint/NonAtomicFileOperation: # new in 1.31
109
+ Enabled: true
110
+ Lint/NumberedParameterAssignment: # new in 1.9
111
+ Enabled: true
112
+ Lint/OrAssignmentToConstant: # new in 1.9
113
+ Enabled: true
114
+ Lint/RedundantDirGlobSort: # new in 1.8
115
+ Enabled: true
116
+ Lint/RedundantRegexpQuantifiers: # new in 1.53
117
+ Enabled: true
118
+ Lint/RefinementImportMethods: # new in 1.27
119
+ Enabled: true
120
+ Lint/RequireRangeParentheses: # new in 1.32
121
+ Enabled: true
122
+ Lint/RequireRelativeSelfPath: # new in 1.22
123
+ Enabled: true
124
+ Lint/SymbolConversion: # new in 1.9
125
+ Enabled: true
126
+ Lint/ToEnumArguments: # new in 1.1
127
+ Enabled: true
128
+ Lint/TripleQuotes: # new in 1.9
129
+ Enabled: true
130
+ Lint/UnexpectedBlockArity: # new in 1.5
131
+ Enabled: true
132
+ Lint/UnmodifiedReduceAccumulator: # new in 1.1
133
+ Enabled: true
134
+ Lint/UselessRescue: # new in 1.43
135
+ Enabled: true
136
+ Lint/UselessRuby2Keywords: # new in 1.23
137
+ Enabled: true
138
+
139
+ ##
140
+ # METRICS COPS
141
+ ##
142
+
143
+ Metrics:
144
+ Enabled: false
145
+
146
+ ##
147
+ # GEMSPEC COPS
148
+ ##
149
+
150
+ Gemspec/DeprecatedAttributeAssignment: # new in 1.30
151
+ Enabled: true
152
+ Gemspec/DevelopmentDependencies: # new in 1.44
153
+ Enabled: true
154
+ Gemspec/RequireMFA: # new in 1.23
155
+ Enabled: false
156
+ Gemspec/OrderedDependencies:
157
+ Enabled: false
158
+
159
+ ##
160
+ # BUNDLER COPS
161
+ ##
162
+
163
+ Bundler/OrderedGems:
164
+ Enabled: false
165
+
166
+ ##
167
+ # RSPEC COPS
168
+ ##
169
+
170
+ # documentation can be found in
171
+ # http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/<name>
172
+ RSpec:
173
+ Enabled: false
174
+
175
+ RSpec/HookArgument:
176
+ Enabled: true
177
+ EnforcedStyle: each
178
+ RSpec/MessageSpies:
179
+ Enabled: true
180
+ EnforcedStyle: receive
181
+
182
+ ##
183
+ # CAPYBARA COPS
184
+ ##
185
+
186
+ # cant remember installing capybara but whatever
187
+ Capybara:
188
+ Enabled: false
189
+
190
+ ##
191
+ # FACTORYBOT COPS
192
+ ##
193
+
194
+ # cant remember installing factorybot but whatever
195
+ FactoryBot:
196
+ Enabled: false
197
+
198
+ ##
199
+ # RAKE COPS
200
+ ##
201
+
202
+ Rake:
203
+ Enabled: true
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.2.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-11-30
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in specdiff.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+
8
+ gem "rspec", "~> 3.0"
9
+
10
+ gem "rubocop", "~> 1.21"
11
+ gem "rubocop-rspec", '~> 2.25'
12
+ gem 'rubocop-rake', '~> 0.6.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ specdiff (0.1.0)
5
+ diff-lcs (~> 1.5)
6
+ hashdiff (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ diff-lcs (1.5.0)
13
+ hashdiff (1.0.1)
14
+ json (2.6.3)
15
+ language_server-protocol (3.17.0.3)
16
+ parallel (1.23.0)
17
+ parser (3.2.2.4)
18
+ ast (~> 2.4.1)
19
+ racc
20
+ racc (1.7.3)
21
+ rainbow (3.1.1)
22
+ rake (13.1.0)
23
+ regexp_parser (2.8.2)
24
+ rexml (3.2.6)
25
+ rspec (3.12.0)
26
+ rspec-core (~> 3.12.0)
27
+ rspec-expectations (~> 3.12.0)
28
+ rspec-mocks (~> 3.12.0)
29
+ rspec-core (3.12.2)
30
+ rspec-support (~> 3.12.0)
31
+ rspec-expectations (3.12.3)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.12.0)
34
+ rspec-mocks (3.12.6)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.12.0)
37
+ rspec-support (3.12.1)
38
+ rubocop (1.57.2)
39
+ json (~> 2.3)
40
+ language_server-protocol (>= 3.17.0)
41
+ parallel (~> 1.10)
42
+ parser (>= 3.2.2.4)
43
+ rainbow (>= 2.2.2, < 4.0)
44
+ regexp_parser (>= 1.8, < 3.0)
45
+ rexml (>= 3.2.5, < 4.0)
46
+ rubocop-ast (>= 1.28.1, < 2.0)
47
+ ruby-progressbar (~> 1.7)
48
+ unicode-display_width (>= 2.4.0, < 3.0)
49
+ rubocop-ast (1.30.0)
50
+ parser (>= 3.2.1.0)
51
+ rubocop-capybara (2.19.0)
52
+ rubocop (~> 1.41)
53
+ rubocop-factory_bot (2.24.0)
54
+ rubocop (~> 1.33)
55
+ rubocop-rake (0.6.0)
56
+ rubocop (~> 1.0)
57
+ rubocop-rspec (2.25.0)
58
+ rubocop (~> 1.40)
59
+ rubocop-capybara (~> 2.17)
60
+ rubocop-factory_bot (~> 2.22)
61
+ ruby-progressbar (1.13.0)
62
+ unicode-display_width (2.5.0)
63
+
64
+ PLATFORMS
65
+ x86_64-linux
66
+
67
+ DEPENDENCIES
68
+ rake (~> 13.0)
69
+ rspec (~> 3.0)
70
+ rubocop (~> 1.21)
71
+ rubocop-rake (~> 0.6.0)
72
+ rubocop-rspec (~> 2.25)
73
+ specdiff!
74
+
75
+ BUNDLED WITH
76
+ 2.3.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Odin H B
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Specdiff
2
+
3
+ A gem for improving diff output in webmock.
4
+
5
+ Webmock currently has a somewhat hidden feature where it will produce a diff
6
+ between a request body and a registered stub when making an unregistered request
7
+ if they happen to both be json (including setting content type). This gem aims
8
+ to bring text diffing (ala rspec) to webmock via monkey-patch, as well as
9
+ dropping the content type requirement.
10
+
11
+ Specdiff automagically detects the types of provided data and prints a suitable
12
+ diff between them.
13
+
14
+ Check out the examples directory to see what it might look like.
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "specdiff", require: false
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ $ bundle install
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install specdiff
31
+
32
+ ## Usage
33
+
34
+ Put the following in your `spec_helper.rb`. (Or equivalent initializer
35
+ for test environment) You probably don't want to load/use this gem in a release
36
+ environment.
37
+
38
+ ```rb
39
+ # spec_helper.rb
40
+
41
+ require "specdiff"
42
+ require "specdiff/webmock" # optional, webmock patches
43
+
44
+ # optionally, you can turn off terminal colors
45
+ Specdiff.configure do |config|
46
+ config.colorize = true
47
+ end
48
+ ```
49
+
50
+ The webmock patch should make webmock show diffs from the specdiff gem when
51
+ stubs mismatch.
52
+
53
+ ### Direct usage
54
+
55
+ You can also use the gem directly:
56
+
57
+ ```rb
58
+ diff = Specdiff.diff(something, and_something_else)
59
+
60
+ diff.empty? # => true/false, if it is empty you might want to not print the diff, it is probably useless
61
+ diff.to_s # => a string for showing to a developer who may or may not be scratching their head
62
+ ```
63
+
64
+ ## Development
65
+
66
+ Install the software versions specified in `.tool-versions`.
67
+
68
+ Run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests and linter and make sure they're green before starting to make your changes.
69
+
70
+ Run `bundle exec rake -AD` gor a full list of all the available tasks you may use for development purposes.
71
+
72
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment with the gem code loaded.
73
+
74
+ ## Releasing
75
+
76
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
77
+
78
+ ## Contributing
79
+
80
+ Bug reports and pull requests are welcome on GitHub at https://github.com/odinhb/specdiff.
81
+
82
+ ## License
83
+
84
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:test)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new(:lint)
11
+
12
+ task default: %i[test lint]
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "specdiff", require: false, path: "../../"
4
+
5
+ gem "webmock", require: false
6
+ gem "http"
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ specdiff (0.1.0)
5
+ diff-lcs (~> 1.5)
6
+ hashdiff (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.5)
12
+ public_suffix (>= 2.0.2, < 6.0)
13
+ crack (0.4.5)
14
+ rexml
15
+ diff-lcs (1.5.0)
16
+ domain_name (0.6.20231109)
17
+ ffi (1.16.3)
18
+ ffi-compiler (1.0.1)
19
+ ffi (>= 1.0.0)
20
+ rake
21
+ hashdiff (1.0.1)
22
+ http (5.1.1)
23
+ addressable (~> 2.8)
24
+ http-cookie (~> 1.0)
25
+ http-form_data (~> 2.2)
26
+ llhttp-ffi (~> 0.4.0)
27
+ http-cookie (1.0.5)
28
+ domain_name (~> 0.5)
29
+ http-form_data (2.3.0)
30
+ llhttp-ffi (0.4.0)
31
+ ffi-compiler (~> 1.0)
32
+ rake (~> 13.0)
33
+ public_suffix (5.0.4)
34
+ rake (13.1.0)
35
+ rexml (3.2.6)
36
+ webmock (3.19.1)
37
+ addressable (>= 2.8.0)
38
+ crack (>= 0.3.2)
39
+ hashdiff (>= 0.4.0, < 2.0.0)
40
+
41
+ PLATFORMS
42
+ x86_64-linux
43
+
44
+ DEPENDENCIES
45
+ http
46
+ specdiff!
47
+ webmock
48
+
49
+ BUNDLED WITH
50
+ 2.3.5
@@ -0,0 +1,37 @@
1
+ require "webmock"
2
+ require "specdiff"
3
+ require "specdiff/webmock"
4
+
5
+ Specdiff.load!(:json)
6
+
7
+ include WebMock::API
8
+
9
+ WebMock.enable!
10
+ WebMock.show_body_diff! # on by default
11
+
12
+ stub_request(:post, "https://www.example.com")
13
+ .with(
14
+ body: JSON.generate({
15
+ my_hash: "is great",
16
+ the_hash: "is amazing",
17
+ })
18
+ )
19
+ .to_return(status: 400, body: "hello")
20
+
21
+ begin
22
+ HTTP.post(
23
+ "https://www.example.com",
24
+ body: JSON.generate({
25
+ i_had_to_go: "and post a different hash",
26
+ my_hash: "is different",
27
+ }),
28
+ )
29
+ rescue WebMock::NetConnectNotAllowedError => e
30
+ puts "success! webmock stopped the request"
31
+ puts "here is the error text:\n\n"
32
+
33
+ puts e.message
34
+ exit 0
35
+ end
36
+
37
+ puts "nothing was raised??"
@@ -0,0 +1,39 @@
1
+ require "webmock"
2
+ require "specdiff"
3
+ require "specdiff/webmock"
4
+
5
+ include WebMock::API
6
+
7
+ WebMock.enable!
8
+ WebMock.show_body_diff! # on by default
9
+
10
+ stub_request(:post, "https://www.example.com")
11
+ .with(
12
+ body: <<~TEXT1,
13
+ this is the expected body
14
+ that you should send
15
+ nothing less
16
+ nothing more
17
+ TEXT1
18
+ )
19
+ .to_return(status: 400, body: "hello")
20
+
21
+ begin
22
+ HTTP.post(
23
+ "https://www.example.com",
24
+ body: <<~TEXT2,
25
+ this is the unexpected body
26
+ that i should not have sent
27
+ nothing less
28
+ nothing more
29
+ TEXT2
30
+ )
31
+ rescue WebMock::NetConnectNotAllowedError => e
32
+ puts "success! webmock stopped the request"
33
+ puts "here is the error text:\n\n"
34
+
35
+ puts e.message
36
+ exit 0
37
+ end
38
+
39
+ puts "nothing was raised??"
data/glossary.txt ADDED
@@ -0,0 +1,24 @@
1
+ diff
2
+ the return value from the Specdiff::diff method.
3
+ this is not the direct return value from a plugin/differ, that is the
4
+ "raw diff" (or diff.raw)
5
+
6
+ differ
7
+ a class responding to ::diff(a, b) and ::stringify(diff) able to produce a
8
+ human-comprehensible diff output for your terminal
9
+
10
+ plugin
11
+ external differ (responding to more methods), able to be provided by third
12
+ parties or end users themselves
13
+
14
+ type
15
+ a symbol like :text, :json or :hash which denotes the type of data in a way
16
+ which is useful for picking a differ
17
+
18
+ :text
19
+ a string which likely contains plaintext data of some kind
20
+
21
+ side
22
+ an object containing either side of a comparison that needs to be made
23
+ when defining a plugin, the diff method receives two sides: a, b
24
+ most importantly, a side contains a value and a type
@@ -0,0 +1,31 @@
1
+ module Specdiff::Colorize
2
+ module_function
3
+
4
+ def colorize_by_line(string, line_separator = "\n")
5
+ return string if !::Specdiff.config.colorize?
6
+
7
+ string.lines(line_separator).map do |line|
8
+ yield(line)
9
+ end.join
10
+ end
11
+
12
+ def reset_color(text)
13
+ "\e[0m#{text}"
14
+ end
15
+
16
+ def red(text)
17
+ "\e[31m#{text}\e[0m"
18
+ end
19
+
20
+ def green(text)
21
+ "\e[32m#{text}\e[0m"
22
+ end
23
+
24
+ def yellow(text)
25
+ "\e[33m#{text}\e[0m"
26
+ end
27
+
28
+ def blue(text)
29
+ "\e[34m#{text}\e[0m"
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Specdiff
2
+ Config = Struct.new(:colorize, keyword_init: true) do
3
+ def colorize?
4
+ colorize
5
+ end
6
+ end
7
+
8
+ # Read the configuration
9
+ def self.config
10
+ threadlocal[:config] ||= default_configuration
11
+ end
12
+
13
+ # private, used for testing
14
+ def self._set_config(new_config)
15
+ threadlocal[:config] = new_config
16
+ end
17
+
18
+ # Set the configuration
19
+ def self.configure
20
+ yield(config)
21
+ end
22
+
23
+ # Generates the default configuration
24
+ def self.default_configuration
25
+ Config.new(colorize: true)
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ ::Specdiff::Diff = Struct.new(
2
+ :differ, :a, :b, :raw, keyword_init: true,
3
+ ) do
4
+ def differ_id
5
+ differ.id
6
+ end
7
+
8
+ def to_s
9
+ differ.stringify(self)
10
+ end
11
+
12
+ def empty?
13
+ differ == ::Specdiff::Differ::NotFound
14
+ end
15
+
16
+ def types
17
+ [a.type, b.type]
18
+ end
19
+
20
+ def inspect
21
+ if empty?
22
+ "<Specdiff::Diff (empty)>"
23
+ else
24
+ "<Specdiff::Diff w/ #{raw&.bytesize || 0} bytes of #raw diff>"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,85 @@
1
+ require "hashdiff"
2
+ require "pp"
3
+
4
+ class Specdiff::Differ::Hashdiff
5
+ extend ::Specdiff::Colorize
6
+
7
+ def self.diff(a, b)
8
+ # array_path: true returns the path as an array, which differentiates
9
+ # between symbol keys and string keys in hashes, while the string
10
+ # representation does not.
11
+ # hmm it really seems like use_lcs: true gives much less human-readable
12
+ # (human-comprehensible) output when arrays are involved.
13
+ Hashdiff.diff(
14
+ a.value, b.value,
15
+ array_path: true,
16
+ use_lcs: false,
17
+ )
18
+ end
19
+
20
+ def self.stringify(diff)
21
+ diff.raw.pretty_inspect
22
+
23
+ result = +""
24
+
25
+ diff.raw.each do |change|
26
+ type = change[0] # the char '+', '-' or '~'
27
+ path = change[1] # for example key1.key2[2] (as ["key1", :key2, 2])
28
+ path2 = _stringify_path(path)
29
+
30
+ if type == "+"
31
+ value = change[2]
32
+
33
+ result << "added #{path2} with value #{value.inspect}"
34
+ elsif type == "-"
35
+ value = change[2]
36
+
37
+ result << "removed #{path2} with value #{value.inspect}"
38
+ elsif type == "~"
39
+ from = change[2]
40
+ to = change[3]
41
+
42
+ result << "changed #{path2} from #{from.inspect} to #{to.inspect}"
43
+ else
44
+ result << change.inspect
45
+ end
46
+
47
+ result << "\n"
48
+ end
49
+
50
+ colorize_by_line(result) do |line|
51
+ if line.start_with?("removed")
52
+ red(line)
53
+ elsif line.start_with?("added")
54
+ green(line)
55
+ elsif line.start_with?("changed")
56
+ yellow(line)
57
+ else
58
+ reset_color(line)
59
+ end
60
+ end
61
+ end
62
+
63
+ PATH_SEPARATOR = ".".freeze
64
+
65
+ def self._stringify_path(path)
66
+ result = +""
67
+
68
+ path.each do |component|
69
+ if component.is_a?(Numeric)
70
+ result.chomp!(PATH_SEPARATOR)
71
+ result << "[#{component}]"
72
+ elsif component.is_a?(Symbol)
73
+ # by not inspecting symbols they look prettier than strings, but you
74
+ # can still tell the difference in the printed output
75
+ result << component.to_s
76
+ else
77
+ result << component.inspect
78
+ end
79
+
80
+ result << PATH_SEPARATOR
81
+ end
82
+
83
+ result.chomp(PATH_SEPARATOR)
84
+ end
85
+ end
@@ -0,0 +1,24 @@
1
+ # this is the null differ
2
+ class Specdiff::Differ::NotFound
3
+ def self.diff(a, b)
4
+ comparison = "!="
5
+ comparison = "=" if a.value == b.value
6
+
7
+ a_representation = _representation_for(a)
8
+ b_representation = _representation_for(b)
9
+
10
+ "#{a_representation} #{comparison} #{b_representation}"
11
+ end
12
+
13
+ def self._representation_for(side)
14
+ if side.type == :binary
15
+ "<binary content>"
16
+ else
17
+ side.value.inspect
18
+ end
19
+ end
20
+
21
+ def self.stringify(diff)
22
+ diff.raw
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ require "diff/lcs"
2
+ require "diff/lcs/hunk"
3
+
4
+ class Specdiff::Differ::Text
5
+ extend ::Specdiff::Colorize
6
+
7
+ NEWLINE = "\n".freeze
8
+ CONTEXT_LINES = 3
9
+
10
+ # this implementation is based on RSpec::Support::Differ
11
+ # https://github.com/rspec/rspec-support/blob/main/lib/rspec/support/differ.rb
12
+ # and also the hunk generator it uses
13
+ def self.diff(a, b)
14
+ a_value = a.value
15
+ b_value = b.value
16
+
17
+ if a_value.encoding != b_value.encoding
18
+ return <<~MSG
19
+ Strings have different encodings:
20
+ #{a.value.encoding.inspect} != #{b.value.encoding.inspect}
21
+ MSG
22
+ end
23
+
24
+ diff = ""
25
+
26
+ a_lines = a_value.split(NEWLINE).map! { _1.chomp }
27
+ b_lines = b_value.split(NEWLINE).map! { _1.chomp }
28
+ hunks = ::Diff::LCS.diff(a_lines, b_lines).map do |piece|
29
+ ::Diff::LCS::Hunk.new(
30
+ a_lines, b_lines, piece, CONTEXT_LINES, 0,
31
+ )
32
+ end
33
+
34
+ hunks.each_cons(2) do |prev_hunk, current_hunk|
35
+ begin
36
+ if current_hunk.overlaps?(prev_hunk)
37
+ current_hunk.merge(prev_hunk)
38
+ else
39
+ diff << prev_hunk.diff(:unified).to_s
40
+ end
41
+ ensure
42
+ diff << NEWLINE
43
+ end
44
+ end
45
+
46
+ if hunks.last
47
+ diff << hunks.last.diff(:unified).to_s
48
+ end
49
+
50
+ diff << "\n"
51
+
52
+ return colorize_by_line(diff) do |line|
53
+ case line[0].chr
54
+ when "+"
55
+ green(line)
56
+ when "-"
57
+ red(line)
58
+ when "@"
59
+ if line[1].chr == "@"
60
+ blue(line)
61
+ else
62
+ reset_color(line)
63
+ end
64
+ else
65
+ reset_color(line)
66
+ end
67
+ end
68
+ end
69
+
70
+ def self.stringify(diff)
71
+ diff.raw
72
+ end
73
+ end
@@ -0,0 +1,100 @@
1
+ class Specdiff::Differ
2
+ Side = Struct.new(:value, :type, keyword_init: true)
3
+
4
+ def self.call(...)
5
+ new.call(...)
6
+ end
7
+
8
+ def call(raw_a, raw_b)
9
+ a = parse_side(raw_a)
10
+ b = parse_side(raw_b)
11
+
12
+ if a.type == :text && b.type == :binary
13
+ new_b = try_reencode(b.value, a.value.encoding)
14
+ if new_b
15
+ b = b.dup
16
+ b.type = :text
17
+ b.value = new_b
18
+ end
19
+ elsif a.type == :binary && b.type == :text
20
+ new_a = try_reencode(a.value, b.value.encoding)
21
+ if new_a
22
+ a = a.dup
23
+ a.type = :text
24
+ a.value = new_a
25
+ end
26
+ end
27
+
28
+ differ = pick_differ(a, b)
29
+ raw = differ.diff(a, b)
30
+
31
+ if raw.is_a?(::Specdiff::Diff) # detect recursive plugins, such as json
32
+ raw
33
+ else
34
+ ::Specdiff::Diff.new(raw: raw, differ: differ, a: a, b: b)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse_side(raw_value)
41
+ type = detect_type(raw_value)
42
+
43
+ Side.new(value: raw_value, type: type)
44
+ end
45
+
46
+ def detect_type(thing)
47
+ if (type = detect_plugin_types(thing))
48
+ type
49
+ elsif thing.is_a?(Hash)
50
+ :hash
51
+ elsif thing.is_a?(Array)
52
+ :array
53
+ elsif thing.is_a?(String) && thing.encoding == Encoding::BINARY
54
+ :binary
55
+ elsif thing.is_a?(String)
56
+ :text
57
+ elsif thing.nil?
58
+ :nil
59
+ else
60
+ :unknown
61
+ end
62
+ end
63
+
64
+ def detect_plugin_types(thing)
65
+ Specdiff.plugins
66
+ .filter { |plugin| plugin.respond_to?(:detect_type) }
67
+ .detect { |plugin| plugin.detect_type(thing) }
68
+ &.id
69
+ end
70
+
71
+ def try_reencode(binary_string, target_encoding)
72
+ binary_string.encode(target_encoding)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ def pick_differ(a, b)
78
+ if (differ = pick_plugin_differ(a, b))
79
+ differ
80
+ elsif a.type == :text && b.type == :text
81
+ Specdiff::Differ::Text
82
+ elsif a.type == :hash && b.type == :hash
83
+ Specdiff::Differ::Hashdiff
84
+ elsif a.type == :array && b.type == :array
85
+ Specdiff::Differ::Hashdiff
86
+ else
87
+ Specdiff::Differ::NotFound
88
+ end
89
+ end
90
+
91
+ def pick_plugin_differ(a, b)
92
+ Specdiff.plugins
93
+ .detect { |plugin| plugin.compatible?(a, b) }
94
+ end
95
+ end
96
+
97
+ # require only the builtin differs, plugins are optionally loaded later
98
+ require_relative "differ/not_found"
99
+ require_relative "differ/text"
100
+ require_relative "differ/hashdiff"
@@ -0,0 +1,11 @@
1
+ module Specdiff::Plugin
2
+ module ClassMethods
3
+ def compatible?(a, b)
4
+ a.type == id && b.type == id
5
+ end
6
+ end
7
+
8
+ def self.included(plugin)
9
+ plugin.extend(ClassMethods)
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ require "json"
2
+
3
+ class Specdiff::Plugins::Json
4
+ include ::Specdiff::Plugin
5
+
6
+ def self.id
7
+ :json
8
+ end
9
+
10
+ def self.detect_type(thing)
11
+ thing.is_a?(String) && _json_parsable?(thing)
12
+ end
13
+
14
+ def self._json_parsable?(thing)
15
+ return false unless thing.is_a?(String)
16
+
17
+ JSON.parse(thing)
18
+ true
19
+ rescue JSON::ParserError
20
+ false
21
+ end
22
+
23
+ def self.compatible?(a, b)
24
+ a.type == :json || b.type == :json
25
+ end
26
+
27
+ def self.diff(a, b)
28
+ a_value = a.value
29
+ b_value = b.value
30
+
31
+ a_value = JSON.parse(a_value) if a.type == :json
32
+ b_value = JSON.parse(b_value) if b.type == :json
33
+
34
+ ::Specdiff.diff(a_value, b_value)
35
+ end
36
+
37
+ def self.stringify(_diff)
38
+ # since we recurse back into Specdiff, we don't need to stringify in this
39
+ # plugin. the built in hash/array/text differs should do the stringification
40
+ raise "#{self.class}::stringify was called, this should never happen"
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ module Specdiff
2
+ THREADLOCAL_PLUGINS_KEY = :__specdiff_plugins
3
+
4
+ def self.plugins
5
+ threadlocal[THREADLOCAL_PLUGINS_KEY]
6
+ end
7
+
8
+ BUILTIN_PLUGINS = %i[json]
9
+ BUILTIN_TYPES = %i[hash array binary text nil]
10
+
11
+ # Load extra type support, such as support for json strings
12
+ def self.load!(*plugins)
13
+ return if plugins.size == 0
14
+
15
+ plugins.each do |new_plugin|
16
+ if BUILTIN_PLUGINS.include?(new_plugin)
17
+ case new_plugin
18
+ when :json
19
+ require "#{__dir__}/plugins/json"
20
+ load_plugin_class!(::Specdiff::Plugins::Json)
21
+ end
22
+ else
23
+ load_plugin_class!(new_plugin)
24
+ end
25
+ end
26
+
27
+ nil
28
+ end
29
+
30
+ PLUGIN_INTERFACE = [:id, :detect_type, :compatible?, :diff, :stringify].freeze
31
+
32
+ # Load a single plugin class, does not support symbols for builtin plugins.
33
+ def self.load_plugin_class!(plugin)
34
+ plugin_interface = PLUGIN_INTERFACE
35
+
36
+ if plugin.respond_to?(:id)
37
+ if BUILTIN_TYPES.include?(plugin.id)
38
+ plugin_interface = PLUGIN_INTERFACE - [:detect_type]
39
+ end
40
+
41
+ if plugin.id == :unknown
42
+ raise <<~MSG
43
+ plugin #{plugin.inspect} defined #id to = :unknown, but this is not \
44
+ allowed because it would undermine the utility of the #empty? method \
45
+ on the diff.
46
+ MSG
47
+ end
48
+ end
49
+
50
+ missing = plugin_interface.filter do |method_name|
51
+ !plugin.respond_to?(method_name)
52
+ end
53
+
54
+ if missing.any?
55
+ raise <<~MSG
56
+ plugin #{plugin.inspect} does not respond to required methods:
57
+ these are required: #{plugin_interface}
58
+ these were missing: #{missing.inspect}
59
+ MSG
60
+ end
61
+
62
+ ::Specdiff.plugins << plugin
63
+ end
64
+
65
+ # private
66
+ def self._clear_plugins!
67
+ threadlocal[THREADLOCAL_PLUGINS_KEY] = []
68
+ end
69
+ _clear_plugins!
70
+
71
+ module Plugins
72
+ end
73
+ end
@@ -0,0 +1,8 @@
1
+ module Specdiff
2
+ THREADLOCAL_KEY = :__specdiff_threadlocals
3
+
4
+ def self.threadlocal
5
+ Thread.current.thread_variable_get(THREADLOCAL_KEY) ||
6
+ Thread.current.thread_variable_set(THREADLOCAL_KEY, {})
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module Specdiff
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,41 @@
1
+ require "hashdiff"
2
+ require "json"
3
+
4
+ module WebMock
5
+ class RequestBodyDiff
6
+ def initialize(request_signature, request_stub)
7
+ @request_signature = request_signature
8
+ @request_stub = request_stub
9
+ end
10
+
11
+ PrettyPrintableThingy = Struct.new(:specdiff) do
12
+ # webmock does not print the diff if it responds true to this.
13
+ def empty?
14
+ specdiff.empty?
15
+ end
16
+
17
+ # webmock prints the diff by passing us to PP.pp, which in turn uses this
18
+ # method.
19
+ def pretty_print(pp)
20
+ pp.text("\r") # remove a space that isn't supposed to be there
21
+ pp.text(specdiff.to_s)
22
+ end
23
+ end
24
+
25
+ def body_diff
26
+ specdiff = Specdiff.diff(request_stub_body, request_signature.body)
27
+ PrettyPrintableThingy.new(specdiff)
28
+ end
29
+
30
+ attr_reader :request_signature, :request_stub
31
+ private :request_signature, :request_stub
32
+
33
+ private
34
+
35
+ def request_stub_body
36
+ request_stub.request_pattern &&
37
+ request_stub.request_pattern.body_pattern &&
38
+ request_stub.request_pattern.body_pattern.pattern
39
+ end
40
+ end
41
+ end
@@ -0,0 +1 @@
1
+ require_relative "webmock/request_body_diff"
data/lib/specdiff.rb ADDED
@@ -0,0 +1,16 @@
1
+ require_relative "specdiff/version"
2
+ require_relative "specdiff/threadlocal"
3
+ require_relative "specdiff/config"
4
+ require_relative "specdiff/colorize"
5
+
6
+ module Specdiff
7
+ # Diff two things
8
+ def self.diff(...)
9
+ ::Specdiff::Differ.call(...)
10
+ end
11
+ end
12
+
13
+ require_relative "specdiff/diff"
14
+ require_relative "specdiff/differ"
15
+ require_relative "specdiff/plugin"
16
+ require_relative "specdiff/plugins"
data/specdiff.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/specdiff/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "specdiff"
7
+ spec.version = Specdiff::VERSION
8
+ spec.authors = ["Odin Heggvold Bekkelund"]
9
+ spec.email = ["odinhb@protonmail.com"]
10
+
11
+ spec.summary = "Improved request body diffs for webmock"
12
+ spec.homepage = "https://github.com/odinhb/specdiff"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
19
+
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
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "hashdiff", "~> 1.0"
30
+ spec.add_dependency "diff-lcs", "~> 1.5"
31
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: specdiff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Odin Heggvold Bekkelund
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashdiff
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: diff-lcs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ description:
42
+ email:
43
+ - odinhb@protonmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - ".rubocop.yml"
50
+ - ".tool-versions"
51
+ - CHANGELOG.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - examples/webmock/Gemfile
58
+ - examples/webmock/Gemfile.lock
59
+ - examples/webmock/json.rb
60
+ - examples/webmock/text.rb
61
+ - glossary.txt
62
+ - lib/specdiff.rb
63
+ - lib/specdiff/colorize.rb
64
+ - lib/specdiff/config.rb
65
+ - lib/specdiff/diff.rb
66
+ - lib/specdiff/differ.rb
67
+ - lib/specdiff/differ/hashdiff.rb
68
+ - lib/specdiff/differ/not_found.rb
69
+ - lib/specdiff/differ/text.rb
70
+ - lib/specdiff/plugin.rb
71
+ - lib/specdiff/plugins.rb
72
+ - lib/specdiff/plugins/json.rb
73
+ - lib/specdiff/threadlocal.rb
74
+ - lib/specdiff/version.rb
75
+ - lib/specdiff/webmock.rb
76
+ - lib/specdiff/webmock/request_body_diff.rb
77
+ - specdiff.gemspec
78
+ homepage: https://github.com/odinhb/specdiff
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/odinhb/specdiff
83
+ source_code_uri: https://github.com/odinhb/specdiff
84
+ changelog_uri: https://github.com/odinhb/specdiff/CHANGELOG.md
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 3.0.0
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.4.10
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Improved request body diffs for webmock
104
+ test_files: []