json_spectacular 1.0.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
+ SHA1:
3
+ metadata.gz: aa60f6a80d45be14af65ac6a7d9098745fc554a8
4
+ data.tar.gz: 5084d6e054706dc3a3f0b85175a0df65c6b4c38f
5
+ SHA512:
6
+ metadata.gz: 486c82eddc6cfad05df2d4643163c117ebf23abf7e7de09b28aadbfb17c476708f5abee7f44c2dca7652a78f6bf90b37126cb6de16228a68e39c802c025efe59
7
+ data.tar.gz: 701b3e8789ca02a87176461e31a6c54cb80bf304992045593156da4e4235d776c45d6fac2e1704a458e9bf1e73207f0a0a3ab73ac1de73c0897bd8e930f41dfd
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,40 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.5
3
+
4
+ Metrics/LineLength:
5
+ Max: 120
6
+
7
+ Metrics/AbcSize:
8
+ Enabled: false
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Style/ClassAndModuleChildren:
14
+ Enabled: false
15
+
16
+ Style/RescueModifier:
17
+ Enabled: false
18
+
19
+ Layout/MultilineOperationIndentation:
20
+ Enabled: false
21
+
22
+ Layout/TrailingWhitespace:
23
+ Enabled: false
24
+
25
+ Metrics/PerceivedComplexity:
26
+ Max: 20
27
+
28
+ Metrics/CyclomaticComplexity:
29
+ Max: 20
30
+
31
+ Metrics/MethodLength:
32
+ CountComments: false
33
+ Max: 20
34
+
35
+ Metrics/BlockLength:
36
+ ExcludedMethods:
37
+ - describe
38
+ - context
39
+ - it
40
+ - let
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ script:
5
+ - bundle exec rspec
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ - README LICENSE
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+
6
+ dsl = Guard::RSpec::Dsl.new(self)
7
+
8
+ last_run_spec = nil
9
+
10
+ watch(%r{^lib/(.+)\.rb$}) do |match|
11
+ file_path =
12
+ if match[1] == 'lib'
13
+ "spec/lib/#{match[2]}_spec.rb"
14
+ else
15
+ "spec/#{match[2]}_spec.rb"
16
+ end
17
+
18
+ if File.exist?(file_path)
19
+ file_path
20
+ else
21
+ last_run_spec
22
+ end
23
+ end
24
+
25
+ # RSpec files
26
+ rspec = dsl.rspec
27
+
28
+ # noinspection RubyResolve
29
+ watch(rspec.spec_helper) { rspec.spec_dir }
30
+ # noinspection RubyResolve
31
+ watch(rspec.spec_support) { rspec.spec_dir }
32
+ # noinspection RubyResolve
33
+ watch(rspec.spec_files) do |spec|
34
+ # noinspection RubyUnusedLocalVariable
35
+ last_run_spec = spec[0]
36
+ end
37
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019 Aleck Greenham
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # JSONSpectacular
2
+
3
+ [![Gem](https://img.shields.io/gem/dt/json_spectacular.svg)]()
4
+ [![Build Status](https://travis-ci.org/greena13/json_spectacular.svg)](https://travis-ci.org/greena13/json_spectacular)
5
+ [![GitHub license](https://img.shields.io/github/license/greena13/json_spectacular.svg)](https://github.com/greena13/json_spectacular/blob/master/LICENSE)
6
+
7
+ JSON assertions with noise-free reports on complex nested structures.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ group :test do
15
+ gem 'json_spectacular', require: false
16
+ end
17
+ ```
18
+
19
+ Add `json_spectacular` to your `spec/rails_helper.rb`
20
+
21
+ ```ruby
22
+
23
+ require 'json_spectacular'
24
+
25
+ # ...
26
+
27
+ RSpec.configure do |config|
28
+ # ...
29
+
30
+ %i[request controller].each do |spec_type|
31
+ config.include JSONSpectacular::RSpec, type: spec_type
32
+ end
33
+ end
34
+ ```
35
+
36
+ And then execute:
37
+
38
+ ```bash
39
+ bundle install
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### Asserting JSON responses
45
+
46
+ ```ruby
47
+ RSpec.describe 'making some valid request', type: :request do
48
+ context 'some important context' do
49
+ it 'should return some complicated JSON' do
50
+ perform_request
51
+
52
+ expect(json_response).to eql_json([
53
+ {
54
+ "a" => [
55
+ 1, 2, 3
56
+ ],
57
+ "c" => { "d" => "d'"}
58
+ },
59
+ {
60
+ "b" => [
61
+ 1, 2, 3
62
+ ],
63
+ "c" => { "d" => "d'"}
64
+ }
65
+ ])
66
+ end
67
+ end
68
+ end
69
+ ```
70
+
71
+ The `json_response` helper automatically parses the last response object as json, and the assertion `eql_json` reports failures in a format that is much clearer than anything provided by RSpec.
72
+
73
+ The full `expected` and `actual` values are still reported, but below is a separate report that only includes the paths to the failed nested values and their differences, removing the need to manually compare the two complete objects to find the difference.
74
+
75
+ ## Test suite
76
+
77
+ `json_spectacular` comes with close-to-complete test coverage. You can run the test suite as follows:
78
+
79
+ ```bash
80
+ rspec
81
+ ```
82
+
83
+ ## Contributing
84
+
85
+ 1. Fork it ( https://github.com/greena13/json_spectacular/fork )
86
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
87
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
88
+ 4. Push to the branch (`git push origin my-new-feature`)
89
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'json_spectacular/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'json_spectacular'
9
+ spec.version = JSONSpectacular::VERSION
10
+ spec.authors = ['Aleck Greenham']
11
+ spec.email = ['greenhama13@gmail.com']
12
+ spec.summary = 'JSON assertions with noise-free reports on complex nested structures'
13
+ spec.description = 'JSON assertions that help you find exactly what has changed with ' \
14
+ 'your JSON API without having to manually diff large objects'
15
+ spec.homepage = 'https://github.com/greena13/json_spectacular'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(spec)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'hashdiff', '~> 0'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'guard', '~> 2.1'
27
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
28
+ spec.add_development_dependency 'rake', '~> 0'
29
+ spec.add_development_dependency 'rspec', '>= 3.5.0'
30
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_spectacular/version'
4
+
5
+ module JSONSpectacular
6
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hashdiff'
4
+
5
+ module JSONSpectacular
6
+ module DiffDescriptions
7
+ def self.included(base) # rubocop:disable Metrics/MethodLength
8
+ base.class_eval do # rubocop:disable Metrics/BlockLength
9
+ # Adds diff descriptions to the failure message until the all the nodes of the
10
+ # expected and actual values have been compared, and all the differences (and the
11
+ # paths to them) have been included.
12
+ #
13
+ # For Hashes and Arrays, it recursively calls itself to compare all nodes and
14
+ # elements.
15
+ #
16
+ # @param [String, Number, Boolean, Array, Hash] actual_value current node of the
17
+ # actual value being compared to the corresponding node of the expected
18
+ # value
19
+ # @param [String, Number, Boolean, Array, Hash] expected_value current node of
20
+ # the expected value being compared to the corresponding node of the
21
+ # actual value
22
+ # @param [String] path path to the current nodes being compared, relative to the
23
+ # root full objects
24
+ # @return void Diff descriptions are appended directly to message
25
+ def add_diff_to_message(actual_value, expected_value, path = '')
26
+ diffs_sorted_by_name = HashDiff
27
+ .diff(actual_value, expected_value)
28
+ .sort_by { |a| a[1] }
29
+
30
+ diffs_grouped_by_name =
31
+ diffs_sorted_by_name.each_with_object({}) do |diff, memo|
32
+ operator, name, value = diff
33
+ memo[name] ||= {}
34
+ memo[name][operator] = value
35
+ end
36
+
37
+ diffs_grouped_by_name.each do |name, difference|
38
+ resolve_and_append_diff_to(
39
+ path, name, expected_value, actual_value, difference
40
+ )
41
+ end
42
+ end
43
+
44
+ def resolve_and_append_diff_to(
45
+ path,
46
+ name,
47
+ expected_value,
48
+ actual_value,
49
+ difference
50
+ )
51
+ extra_value, missing_value, different_value =
52
+ resolve_changes(difference, expected_value, actual_value, name)
53
+
54
+ full_path = !path.empty? ? "#{path}.#{name}" : name
55
+
56
+ if non_empty_hash?(missing_value) && non_empty_hash?(extra_value)
57
+ add_diff_to_message(missing_value, extra_value, full_path)
58
+ elsif non_empty_array?(missing_value) && non_empty_array?(extra_value)
59
+ [missing_value.length, extra_value.length].max.times do |i|
60
+ add_diff_to_message(missing_value[i], extra_value[i], full_path)
61
+ end
62
+ elsif difference.key?('~')
63
+ value = value_at_path(expected_value, name)
64
+ append_diff_to_message(full_path, value, different_value)
65
+ else
66
+ append_diff_to_message(full_path, extra_value, missing_value)
67
+ end
68
+ end
69
+
70
+ def resolve_changes(difference, expected_value, actual_value, name)
71
+ missing_value = difference['-'] || value_at_path(actual_value, name)
72
+ extra_value = difference['+'] || value_at_path(expected_value, name)
73
+ different_value = difference['~']
74
+
75
+ [extra_value, missing_value, different_value]
76
+ end
77
+
78
+ def append_diff_to_message(path, expected, actual)
79
+ append_to_message(
80
+ path,
81
+ get_diff(path, expected: expected, actual: actual)
82
+ )
83
+ end
84
+
85
+ def non_empty_hash?(target)
86
+ target.is_a?(Hash) && target.any?
87
+ end
88
+
89
+ def non_empty_array?(target)
90
+ target.is_a?(Array) && target.any?
91
+ end
92
+
93
+ def append_to_message(attribute, diff_description)
94
+ return if already_reported_difference?(attribute)
95
+
96
+ @message += diff_description
97
+ @reported_differences[attribute] = true
98
+ end
99
+
100
+ def already_reported_difference?(attribute)
101
+ @reported_differences.key?(attribute)
102
+ end
103
+
104
+ def value_at_path(target, attribute_path)
105
+ keys = attribute_path.split(/[\[\].]/)
106
+
107
+ keys = keys.map do |key|
108
+ if key.to_i.zero? && key != '0'
109
+ key
110
+ else
111
+ key.to_i
112
+ end
113
+ end
114
+
115
+ result = target
116
+
117
+ keys.each do |key|
118
+ result = result[key] unless key == ''
119
+ end
120
+
121
+ result
122
+ end
123
+
124
+ def get_diff(attribute, options = {})
125
+ diff_description = ''
126
+ diff_description += "#{attribute}\n"
127
+ diff_description += "Expected: #{format_value(options[:expected])}\n"
128
+ diff_description + "Actual: #{format_value(options[:actual])}\n\n"
129
+ end
130
+
131
+ def format_value(value)
132
+ if value.is_a?(String)
133
+ "'#{value}'"
134
+ else
135
+ value
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_spectacular/diff_descriptions'
4
+
5
+ module JSONSpectacular
6
+ # Backing class for the eql_json RSpec matcher. Used for matching Ruby representations
7
+ # of JSON response bodies. Provides clear diff representations for simple and complex
8
+ # or nested JSON objects, highlighting only the values that are different, and where
9
+ # they are in the larger JSON object.
10
+ #
11
+ # Expected to be used as part of a RSpec test suite and with
12
+ # {JSONSpectacular::RSpec#json_response}.
13
+ #
14
+ # @see JSONSpectacular::RSpec#json_response
15
+ # @see JSONSpectacular::RSpec#eql_json
16
+ #
17
+ # @author Aleck Greenham
18
+ class Matcher
19
+ include DiffDescriptions
20
+
21
+ # Creates a new JSONSpectacular::Matcher object.
22
+ #
23
+ # @see JSONSpectacular::RSpec#eql_json
24
+ #
25
+ # @param [String, Number, Boolean, Array, Hash] expected The expected value that will
26
+ # be compared with the actual value
27
+ # @return [JSONSpectacular::Matcher] New matcher object
28
+ def initialize(expected)
29
+ @expected = expected
30
+ @message = ''
31
+ @reported_differences = {}
32
+ end
33
+
34
+ # Declares that RSpec should not attempt to diff the actual and expected values
35
+ # to put in the failure message. This class takes care of diffing and presenting
36
+ # the differences, itself.
37
+ # @return [false] Always false
38
+ def diffable?
39
+ false
40
+ end
41
+
42
+ # Whether the actual value and the expected value are considered equal.
43
+ # @param [String, Number, Boolean, Array, Hash] actual The value to be compared to
44
+ # <tt>expected</tt> for equality
45
+ # @return [Boolean] True when <tt>actual</tt> equals <tt>expected</tt>.
46
+ def matches?(actual)
47
+ @actual = actual
48
+ @expected.eql?(@actual)
49
+ end
50
+
51
+ # Message to display to StdOut by RSpec if the equality check fails. Includes a
52
+ # complete serialisation of <tt>expected</tt> and <tt>actual</tt> values and is
53
+ # then followed by a description of only the (possibly deeply nested) attributes
54
+ # that are different
55
+ # @return [String] message full failure message with explanation of why actual
56
+ # failed the equality check with expected
57
+ def failure_message
58
+ @message += "Expected: #{@expected}\n\n"
59
+ @message += "Actual: #{@actual}\n\n"
60
+ @message += "Differences\n\n"
61
+
62
+ add_diff_to_message(@actual, @expected)
63
+
64
+ @message
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_spectacular/matcher'
4
+
5
+ module JSONSpectacular
6
+ # Module containing JSON helper methods that can be mixed into RSpec test scope
7
+ #
8
+ # @author Aleck Greenham
9
+ module RSpec
10
+ # Parses the last response body in a Rails RSpec controller or request test as JSON
11
+ #
12
+ # @return [Hash{String => Boolean, String, Number, Hash, Array}] Ruby representation
13
+ # of the JSON response body.
14
+ def json_response
15
+ JSON.parse(response.body)
16
+ rescue JSON::ParserError
17
+ '< INVALID JSON RESPONSE >'
18
+ end
19
+
20
+ # Creates a new JSONSpectacular::Matcher instance so it can be passed to RSpec to
21
+ # match <tt>expected</tt> against an actual value.
22
+ #
23
+ # @see JSONSpectacular::Matcher
24
+ #
25
+ # @example Use the eql_json expectation
26
+ # expect(actual).to eql_json(expected)
27
+ #
28
+ # @example Use the eql_json expectation with json_response
29
+ # expect(json_response).to eql_json(expected)
30
+ #
31
+ # @param [Boolean, Hash, String, Number, Array] expected The expected value the RSpec
32
+ # matcher should match against.
33
+ # @return [JSONSpectacular::Matcher] New matcher object
34
+ def eql_json(expected)
35
+ JSONSpectacular::Matcher.new(expected)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONSpectacular
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_spectacular/rspec'
4
+
5
+ RSpec.describe 'eql_json' do
6
+ include JSONSpectacular::RSpec
7
+
8
+ context 'when comparing strings' do
9
+ let(:expected) { 'a' }
10
+ let(:actual) { 'b' }
11
+
12
+ it 'then correctly reports any differences' do
13
+ expect(actual).to eql_json(actual)
14
+
15
+ expect(actual).to_not eql(expected)
16
+
17
+ expect do
18
+ expect(actual).to eql_json(expected)
19
+ end.to raise_error.with_message(error_message(expected, actual,
20
+ '' => {
21
+ expected: "'#{expected}'",
22
+ actual: "'#{actual}'"
23
+ }))
24
+ end
25
+ end
26
+
27
+ context 'when comparing integers' do
28
+ let(:expected) { 2 }
29
+ let(:actual) { 1 }
30
+
31
+ it 'then correctly reports any differences' do
32
+ expect(actual).to eql_json(actual)
33
+
34
+ expect(actual).to_not eql(expected)
35
+
36
+ expect do
37
+ expect(actual).to eql_json(expected)
38
+ end.to raise_error.with_message(error_message(expected, actual,
39
+ '' => {
40
+ expected: expected,
41
+ actual: actual
42
+ }))
43
+ end
44
+ end
45
+
46
+ context 'when comparing nil' do
47
+ let(:expected) { 2 }
48
+ let(:actual) { nil }
49
+
50
+ it 'then correctly reports any differences' do
51
+ expect(actual).to eql_json(actual)
52
+
53
+ expect(actual).to_not eql(expected)
54
+
55
+ expect do
56
+ expect(actual).to eql_json(expected)
57
+ end.to raise_error.with_message(error_message(expected, actual,
58
+ '' => {
59
+ expected: expected,
60
+ actual: actual
61
+ }))
62
+ end
63
+ end
64
+
65
+ context 'when comparing arrays' do
66
+ let(:expected) { [1, 3, 4, 5] }
67
+ let(:actual) { [1, 2, 3] }
68
+
69
+ it 'then correctly reports the elements that have changed' do
70
+ expect(actual).to eql(actual)
71
+
72
+ expect(actual).to_not eql(expected)
73
+
74
+ begin
75
+ expect(actual).to eql_json(expected)
76
+ rescue RSpec::Expectations::ExpectationNotMetError => e
77
+ expect(e.message).to eql(error_message(expected, actual,
78
+ '[1]' => {
79
+ expected: 3,
80
+ actual: 2
81
+ },
82
+ '[2]' => {
83
+ expected: 4,
84
+ actual: 3
85
+ },
86
+ '[3]' => {
87
+ expected: 5,
88
+ actual: ''
89
+ }))
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'when comparing arrays of objects' do
95
+ let(:expected) do
96
+ {
97
+ 'alpha' => 'alpha',
98
+ 'beta' => [1, 2, 3],
99
+ 'gamma' => [
100
+ { 'i' => 'a', 'j' => 'b' },
101
+ { 'i' => 'c', 'j' => 'd' },
102
+ { 'i' => 'e', 'j' => 'f' }
103
+ ]
104
+ }
105
+ end
106
+
107
+ let(:actual) do
108
+ {
109
+ 'alpha' => 'alpha',
110
+ 'beta' => [1, 2, 3],
111
+ 'gamma' => [
112
+ { 'j' => 'b' },
113
+ { 'i' => 'c', 'j' => 'D' },
114
+ { 'i' => 'e', 'j' => 'f', 'k' => 'k' }
115
+ ]
116
+ }
117
+ end
118
+
119
+ it 'then correctly reports the elements that have changed' do
120
+ expect(actual).to eql(actual)
121
+
122
+ expect(actual).to_not eql(expected)
123
+
124
+ begin
125
+ expect(actual).to eql_json(expected)
126
+ rescue RSpec::Expectations::ExpectationNotMetError => e
127
+ expect(e.message).to eql(error_message(expected, actual,
128
+ 'gamma[0].i' => {
129
+ expected: "'a'",
130
+ actual: ''
131
+ },
132
+ 'gamma[1].j' => {
133
+ expected: "'d'",
134
+ actual: "'D'"
135
+ },
136
+ 'gamma[2].k' => {
137
+ expected: '',
138
+ actual: "'k'"
139
+ }))
140
+ end
141
+ end
142
+ end
143
+
144
+ context 'when comparing objects' do
145
+ let(:expected) do
146
+ {
147
+ 'a' => 'a',
148
+ 'c' => 'd',
149
+ 'e' => 'e'
150
+ }
151
+ end
152
+
153
+ let(:actual) do
154
+ {
155
+ 'a' => 'a',
156
+ 'b' => 'b',
157
+ 'c' => 'c'
158
+ }
159
+ end
160
+
161
+ it 'then correctly reports the elements that have changed' do
162
+ expect(actual).to eql(actual)
163
+
164
+ expect(actual).to_not eql(expected)
165
+
166
+ begin
167
+ expect(actual).to eql_json(expected)
168
+ rescue RSpec::Expectations::ExpectationNotMetError => e
169
+ expect(e.message).to eql(error_message(expected, actual,
170
+ 'b' => {
171
+ expected: '',
172
+ actual: "'b'"
173
+ },
174
+ 'c' => {
175
+ expected: "'d'",
176
+ actual: "'c'"
177
+ },
178
+ 'e' => {
179
+ expected: "'e'",
180
+ actual: ''
181
+ }))
182
+ end
183
+ end
184
+ end
185
+
186
+ context 'when comparing nested objects' do
187
+ let(:actual) do
188
+ {
189
+ 'a' => 'a',
190
+ 'b' => {
191
+ 'b' => 'b'
192
+ },
193
+ 'c' => {
194
+ 'd' => 'd',
195
+ 'e' => 'e',
196
+ 'f' => {
197
+ 'g' => 'g'
198
+ },
199
+ 'h' => [1, 2, 3]
200
+ },
201
+ 'i' => {
202
+ 'j' => 'j',
203
+ 'k' => 'k'
204
+ }
205
+ }
206
+ end
207
+
208
+ let(:expected) do
209
+ {
210
+ 'a' => 'a',
211
+ 'c' => {
212
+ 'e' => 'e2',
213
+ 'f' => {
214
+ 'g2' => 'g2'
215
+ },
216
+ 'h' => [1, 2, 4]
217
+ },
218
+ 'i' => {
219
+ 'j' => 'j'
220
+ }
221
+ }
222
+ end
223
+
224
+ it 'then correctly reports the elements that have changed' do
225
+ expect(actual).to eql(actual)
226
+
227
+ expect(actual).to_not eql(expected)
228
+
229
+ begin
230
+ expect(actual).to eql_json(expected)
231
+ rescue RSpec::Expectations::ExpectationNotMetError => e
232
+ expect(e.message).to eql(error_message(expected, actual,
233
+ 'b' => {
234
+ expected: '',
235
+ actual: '{"b"=>"b"}'
236
+ },
237
+ 'c.d' => {
238
+ expected: '',
239
+ actual: "'d'"
240
+ },
241
+ 'c.e' => {
242
+ expected: "'e2'",
243
+ actual: "'e'"
244
+ },
245
+ 'c.f.g' => {
246
+ expected: '',
247
+ actual: "'g'"
248
+ },
249
+ 'c.f.g2' => {
250
+ expected: "'g2'",
251
+ actual: ''
252
+ },
253
+ 'c.h[2]' => {
254
+ expected: '4',
255
+ actual: '3'
256
+ },
257
+ 'i.k' => {
258
+ expected: '',
259
+ actual: "'k'"
260
+ }))
261
+ end
262
+ end
263
+ end
264
+
265
+ context 'when comparing complicated objects' do
266
+ let(:expected) do
267
+ {
268
+ 'a' => 'aa',
269
+ 'b' => 'bb',
270
+ 'c' => {
271
+ 'd' => 2,
272
+ 'e' => 'ee',
273
+ 'f' => [{
274
+ 'g' => 'gg',
275
+ 'h' => 'hh'
276
+ },
277
+ {
278
+ 'g' => 'g1',
279
+ 'h' => 'h1'
280
+ }],
281
+ 'i' => {
282
+ 'j' => 'jj',
283
+ 'k' => 'kk',
284
+ 'l' => [],
285
+ 'm' => {
286
+ 'n' => 1,
287
+ 'o' => 'oo',
288
+ 'p' => {
289
+ 'q' => 'qq'
290
+ },
291
+ 'r' => []
292
+ }
293
+ },
294
+ 's' => [
295
+ {
296
+ 't' => 179,
297
+ 'u' => 'UU'
298
+ }
299
+ ]
300
+ }
301
+ }
302
+ end
303
+
304
+ let(:actual) do
305
+ {
306
+ 'a' => 'aa',
307
+ 'b' => 'bb',
308
+ 'c' => {
309
+ 'd' => 3,
310
+ 'e' => 'ee',
311
+ 'f' => [{
312
+ 'g' => 'g1',
313
+ 'h' => 'hh'
314
+ },
315
+ {
316
+ 'g' => 'g1',
317
+ 'h' => 'h1',
318
+ 'h2' => 'h2'
319
+ }],
320
+ 'i' => {
321
+ 'j' => 'j2',
322
+ 'k' => 'kk',
323
+ 'l' => [2],
324
+ 'm' => {
325
+ 'o' => 'oo',
326
+ 'p' => {
327
+ 'q' => 'qq'
328
+ },
329
+ 'r' => []
330
+ }
331
+ },
332
+ 's' => [
333
+ {
334
+ 't' => 179,
335
+ 'u' => 'UU'
336
+ }
337
+ ]
338
+ }
339
+ }
340
+ end
341
+
342
+ it 'then correctly reports the elements that have changed' do
343
+ expect(actual).to eql(actual)
344
+
345
+ expect(actual).to_not eql(expected)
346
+
347
+ begin
348
+ expect(actual).to eql_json(expected)
349
+ rescue RSpec::Expectations::ExpectationNotMetError => e
350
+ expect(e.message).to eql(error_message(expected, actual,
351
+ 'c.d' => {
352
+ expected: 2,
353
+ actual: 3
354
+ },
355
+ 'c.f[0].g' => {
356
+ expected: "'gg'",
357
+ actual: "'g1'"
358
+ },
359
+ 'c.f[1].h2' => {
360
+ expected: nil,
361
+ actual: "'h2'"
362
+ },
363
+ 'c.i.j' => {
364
+ expected: "'jj'",
365
+ actual: "'j2'"
366
+ },
367
+ 'c.i.l[0]' => {
368
+ expected: nil,
369
+ actual: 2
370
+ },
371
+ 'c.i.m.n' => {
372
+ expected: 1,
373
+ actual: nil
374
+ }))
375
+ end
376
+ end
377
+ end
378
+
379
+ private
380
+
381
+ def error_message(expected, actual, differences)
382
+ message_lines = [
383
+ "Expected: #{expected}\n\n",
384
+ "Actual: #{actual}\n\n",
385
+ "Differences\n\n"
386
+ ]
387
+
388
+ differences.each do |attribute_name, difference|
389
+ message_lines.push("#{attribute_name}\n")
390
+ message_lines.push("Expected: #{difference[:expected]}\n")
391
+ message_lines.push("Actual: #{difference[:actual]}\n\n")
392
+ end
393
+
394
+ message_lines.join
395
+ end
396
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.filter_run_including focus: true
5
+ # noinspection RubyResolve
6
+ config.run_all_when_everything_filtered = true
7
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_spectacular
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Aleck Greenham
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-13 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: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: guard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.5.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.5.0
97
+ description: JSON assertions that help you find exactly what has changed with your
98
+ JSON API without having to manually diff large objects
99
+ email:
100
+ - greenhama13@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".rubocop.yml"
108
+ - ".travis.yml"
109
+ - ".yardopts"
110
+ - Gemfile
111
+ - Guardfile
112
+ - LICENSE.txt
113
+ - README.md
114
+ - Rakefile
115
+ - json_spectacular.gemspec
116
+ - lib/json_spectacular.rb
117
+ - lib/json_spectacular/diff_descriptions.rb
118
+ - lib/json_spectacular/matcher.rb
119
+ - lib/json_spectacular/rspec.rb
120
+ - lib/json_spectacular/version.rb
121
+ - spec/eql_json_spec.rb
122
+ - spec/spec_helper.rb
123
+ homepage: https://github.com/greena13/json_spectacular
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.5.2.1
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: JSON assertions with noise-free reports on complex nested structures
147
+ test_files:
148
+ - spec/eql_json_spec.rb
149
+ - spec/spec_helper.rb