rspec-oj 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 505fa0b6ec22748476d0b3aa748f36c4b7d1c226dc87403aedadd31442a89122
4
+ data.tar.gz: aa0aab2919a98dfc9ade03e6ccee638442c059f9068927a18707282cecf55de3
5
+ SHA512:
6
+ metadata.gz: ed0c980e4e0af0bf1b8174b01f5a2ebb241dcc44e5162cac15fdfe76b2720277c283ef2780f75108af9c283952c15184e4ebb9ff7e10b230374e3fb6681a69b3
7
+ data.tar.gz: 208ae97dc849e475b628976632e67cc52c2f341919c8186f7044cb75f7048ad23bd7613afeb1f2bf36c926b25d156750a6ce08f2bda2c4524284cece0e3a10be
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2020 Mikael Henriksson
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,341 @@
1
+ # rspec-oj
2
+
3
+ Easily handle JSON in RSpec and Cucumber
4
+
5
+ [![Gem Version](https://img.shields.io/gem/v/rspec-oj.svg?style=flat)](http://rubygems.org/gems/rspec-oj)
6
+ [![Build Status](https://img.shields.io/travis/mhenrixon/rspec-oj/master.svg?style=flat)](https://travis-ci.org/mhenrixon/rspec-oj)
7
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e05efd6949d820a0db09/maintainability)](https://codeclimate.com/github/mhenrixon/rspec-oj/maintainability)
8
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/e05efd6949d820a0db09/test_coverage)](https://codeclimate.com/github/mhenrixon/rspec-oj/test_coverage)
9
+
10
+ ## RSpec
11
+
12
+ rspec-oj defines five new RSpec matchers:
13
+
14
+ * `be_json_eql`
15
+ * `include_json`
16
+ * `have_json_path`
17
+ * `have_json_type`
18
+ * `have_json_size`
19
+
20
+ The new matchers could be used in RSpec as follows:
21
+
22
+ ```ruby
23
+ describe User do
24
+ let(:user){ User.create!(first_name: "Steve", last_name: "Richert") }
25
+
26
+ context "#to_json" do
27
+ it "includes names" do
28
+ names = { "first_name": "Steve", "last_name": "Richert" }
29
+ expect(user).to be_json_eql(names).excluding("friends")
30
+ end
31
+
32
+ it "includes the ID" do
33
+ expect(user).to have_json_path("id")
34
+ expect(user).to have_json_type(Integer).at_path("id")
35
+ end
36
+
37
+ it "includes friends" do
38
+ expect(user).to have_json_size(0).at_path("friends")
39
+
40
+ friend = User.create!(first_name: "Catie", last_name: "Richert")
41
+ user.friends << friend
42
+
43
+ expect(user).to have_json_size(1).at_path("friends")
44
+ expect(user).to include_json(friend)
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ rspec-oj also provides some useful helpers for RSpec tests:
51
+
52
+ * `parse_json`
53
+ * `normalize_json`
54
+ * `generate_normalized_json`
55
+ * `load_json`
56
+
57
+ To start using them add an include them in your RSpec configuration:
58
+
59
+ ```ruby
60
+ RSpec.configure do |config|
61
+ config.include RSpec::Oj::Helpers
62
+ end
63
+ ```
64
+
65
+ You can find usage examples for the helpers in [`spec/rspec/oj/helpers_spec.rb`](https://github.com/mhenrixon/rspec-oj/blob/master/spec/rspec/oj/helpers_spec.rb)
66
+
67
+ ### Exclusions
68
+
69
+ rspec-oj ignores certain hash keys by default when comparing JSON:
70
+
71
+ * `id`
72
+ * `created_at`
73
+ * `updated_at`
74
+
75
+ It's oftentimes helpful when evaluating JSON representations of newly-created ActiveRecord records
76
+ so that the new ID and timestamps don't have to be known. These exclusions are globally
77
+ customizeable:
78
+
79
+ ```ruby
80
+ RSpec::Oj.configure do
81
+ exclude_keys "created_at", "updated_at"
82
+ end
83
+ ```
84
+
85
+ Now, the `id` key will be included in rspec-oj's comparisons. Keys can also be excluded/included
86
+ per matcher by chaining the `excluding` or `including` methods (as shown above) which will add or
87
+ subtract from the globally excluded keys, respectively.
88
+
89
+ ### Paths
90
+
91
+ Each of rspec-oj's matchers deal with JSON "paths." These are simple strings of "/" separated
92
+ hash keys and array indexes. For instance, with the following JSON:
93
+
94
+ {
95
+ "first_name": "Steve",
96
+ "last_name": "Richert",
97
+ "friends": [
98
+ {
99
+ "first_name": "Catie",
100
+ "last_name": "Richert"
101
+ }
102
+ ]
103
+ }
104
+
105
+ We could access the first friend's first name with the path `"friends/0/first_name"`.
106
+
107
+ ## Cucumber
108
+
109
+ rspec-oj provides Cucumber steps that utilize its RSpec matchers and that's where rspec-oj really
110
+ shines. This is perfect for testing your app's JSON API.
111
+
112
+ In order to use the Cucumber steps, in your `env.rb` you must:
113
+
114
+ ```ruby
115
+ require "rspec/oj/cucumber"
116
+ ```
117
+
118
+ You also need to define a `last_json` method. If you're using Capybara, it could be as simple as:
119
+
120
+ ```ruby
121
+ def last_json
122
+ page.source
123
+ end
124
+ ```
125
+
126
+ Now, you can use the rspec-oj steps in your features:
127
+
128
+ ```cucumber
129
+ Feature: User API
130
+ Background:
131
+ Given the following users exist:
132
+ | id | first_name | last_name |
133
+ | 1 | Steve | Richert |
134
+ | 2 | Catie | Richert |
135
+ And "Steve Richert" is friends with "Catie Richert"
136
+
137
+ Scenario: Index action
138
+ When I visit "/users.json"
139
+ Then the JSON response should have 2 users
140
+ And the JSON response at "0/id" should be 1
141
+ And the JSON response at "1/id" should be 2
142
+
143
+ Scenario: Show action
144
+ When I visit "/users/1.json"
145
+ Then the JSON response at "first_name" should be "Steve"
146
+ And the JSON response at "last_name" should be "Richert"
147
+ And the JSON response should have "created_at"
148
+ And the JSON response at "created_at" should be a string
149
+ And the JSON response at "friends" should be:
150
+ """
151
+ [
152
+ {
153
+ "id": 2,
154
+ "first_name": "Catie",
155
+ "last_name": "Richert"
156
+ }
157
+ ]
158
+ """
159
+ ```
160
+
161
+ The background steps above aren't provided by rspec-oj and the "visit" steps are provided by
162
+ Capybara. The remaining steps, rspec-oj provides. They're versatile and can be used in plenty of
163
+ different formats:
164
+
165
+ ```cucumber
166
+ Then the JSON should be:
167
+ """
168
+ {
169
+ "key": "value"
170
+ }
171
+ """
172
+ Then the JSON at "path" should be:
173
+ """
174
+ [
175
+ "entry",
176
+ "entry"
177
+ ]
178
+ """
179
+
180
+ Then the JSON should be {"key":"value"}
181
+ Then the JSON at "path" should be {"key":"value"}
182
+ Then the JSON should be ["entry","entry"]
183
+ Then the JSON at "path" should be ["entry","entry"]
184
+ Then the JSON at "path" should be "string"
185
+ Then the JSON at "path" should be 10
186
+ Then the JSON at "path" should be 10.0
187
+ Then the JSON at "path" should be 1e+1
188
+ Then the JSON at "path" should be true
189
+ Then the JSON at "path" should be false
190
+ Then the JSON at "path" should be null
191
+
192
+ Then the JSON should include:
193
+ """
194
+ {
195
+ "key": "value"
196
+ }
197
+ """
198
+ Then the JSON at "path" should include:
199
+ """
200
+ [
201
+ "entry",
202
+ "entry"
203
+ ]
204
+ """
205
+
206
+ Then the JSON should include {"key":"value"}
207
+ Then the JSON at "path" should include {"key":"value"}
208
+ Then the JSON should include ["entry","entry"]
209
+ Then the JSON at "path" should include ["entry","entry"]
210
+ Then the JSON should include "string"
211
+ Then the JSON at "path" should include "string"
212
+ Then the JSON should include 10
213
+ Then the JSON at "path" should include 10
214
+ Then the JSON should include 10.0
215
+ Then the JSON at "path" should include 10.0
216
+ Then the JSON should include 1e+1
217
+ Then the JSON at "path" should include 1e+1
218
+ Then the JSON should include true
219
+ Then the JSON at "path" should include true
220
+ Then the JSON should include false
221
+ Then the JSON at "path" should include false
222
+ Then the JSON should include null
223
+ Then the JSON at "path" should include null
224
+
225
+ Then the JSON should have "path"
226
+
227
+ Then the JSON should be a hash
228
+ Then the JSON at "path" should be an array
229
+ Then the JSON at "path" should be a float
230
+
231
+ Then the JSON should have 1 entry
232
+ Then the JSON at "path" should have 2 entries
233
+ Then the JSON should have 3 keys
234
+ Then the JSON should have 4 whatevers
235
+ ```
236
+
237
+ _All instances of "should" above could be followed by "not" and all instances of "JSON" could be downcased and/or followed by "response."_
238
+
239
+ ### Table Format
240
+
241
+ Another step exists that uses Cucumber's table formatting and wraps two of the above steps:
242
+
243
+ ```cucumber
244
+ Then the JSON should have the following:
245
+ | path/0 | {"key":"value"} |
246
+ | path/1 | ["entry","entry"] |
247
+ ```
248
+
249
+ Any number of rows can be given. The step above is equivalent to:
250
+
251
+ ```cucumber
252
+ Then the JSON at "path/0" should be {"key":"value"}
253
+ And the JSON at "path/1" should be ["entry","entry"]
254
+ ```
255
+
256
+ If only one column is given:
257
+
258
+ ```cucumber
259
+ Then the JSON should have the following:
260
+ | path/0 |
261
+ | path/1 |
262
+ ```
263
+
264
+ This is equivalent to:
265
+
266
+ ```cucumber
267
+ Then the JSON should have "path/0"
268
+ And the JSON should have "path/1"
269
+ ```
270
+
271
+ ### JSON Memory
272
+
273
+ There's one more Cucumber step that rspec-oj provides which hasn't been used above. It's used to
274
+ memorize JSON for reuse in later steps. You can "keep" all or a portion of the JSON by giving a
275
+ name by which to remember it.
276
+
277
+ ```cucumber
278
+ Feature: User API
279
+ Scenario: Index action includes full user JSON
280
+ Given the following user exists:
281
+ | id | first_name | last_name |
282
+ | 1 | Steve | Richert |
283
+ And I visit "/users/1.json"
284
+ And I keep the JSON response as "USER_1"
285
+ When I visit "/users.json"
286
+ Then the JSON response should be:
287
+ """
288
+ [
289
+ %{USER_1}
290
+ ]
291
+ """
292
+ ```
293
+
294
+ You can memorize JSON at a path:
295
+
296
+ ```cucumber
297
+ Given I keep the JSON response at "first_name" as "FIRST_NAME"
298
+ ```
299
+
300
+ You can remember JSON at a path:
301
+
302
+ ```cucumber
303
+ Then the JSON response at "0/first_name" should be:
304
+ """
305
+ %{FIRST_NAME}
306
+ """
307
+ ```
308
+
309
+ You can also remember JSON inline:
310
+
311
+ ```cucumber
312
+ Then the JSON response at "0/first_name" should be %{FIRST_NAME}
313
+ ```
314
+
315
+ ### More
316
+
317
+ Check out the [specs](https://github.com/mhenrixon/rspec-oj/blob/master/spec)
318
+ and [features](https://github.com/mhenrixon/rspec-oj/blob/master/features) to see all the
319
+ various ways you can use rspec-oj.
320
+
321
+ ## Contributing
322
+
323
+ If you come across any issues, please [tell us](https://github.com/mhenrixon/rspec-oj/issues).
324
+ Pull requests (with tests) are appreciated. No pull request is too small. Please help with:
325
+
326
+ * Reporting bugs
327
+ * Suggesting features
328
+ * Writing or improving documentation
329
+ * Fixing typos
330
+ * Cleaning whitespace
331
+ * Refactoring code
332
+ * Adding tests
333
+ * Closing [issues](https://github.com/mhenrixon/rspec-oj/issues)
334
+
335
+ If you report a bug and don't include a fix, please include a failing test.
336
+
337
+ ## Copyright
338
+
339
+ Copyright © 2020 Steve Richert
340
+
341
+ See [LICENSE](https://github.com/mhenrixon/rspec-oj/blob/master/LICENSE) for details.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module RSpec
6
+ module Oj
7
+ module Configuration
8
+ DEFAULT_EXCLUDED_KEYS = %w[id created_at updated_at].freeze
9
+
10
+ attr_accessor :directory
11
+
12
+ def configure(&block)
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def excluded_keys
17
+ @excluded_keys ||= DEFAULT_EXCLUDED_KEYS
18
+ end
19
+
20
+ def excluded_keys=(keys)
21
+ @excluded_keys = keys.map(&:to_s).uniq
22
+ end
23
+
24
+ def exclude_keys(*keys)
25
+ self.excluded_keys = keys
26
+ end
27
+
28
+ def reset
29
+ instance_variables.each { |ivar| remove_instance_variable(ivar) }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec-oj'
4
+
5
+ World(RSpec::Oj::Helpers, RSpec::Oj::Matchers)
6
+
7
+ After do
8
+ RSpec::Oj.forget
9
+ end
10
+
11
+ When(/^(?:I )?keep the (?:JSON|json)(?: response)?(?: at "(.*)")? as "(.*)"$/) do |path, key|
12
+ RSpec::Oj.memorize(key, normalize_json(last_json, path))
13
+ end
14
+
15
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be:$/) do |path, negative, json|
16
+ if negative
17
+ expect(last_json).not_to be_json_eql(RSpec::Oj.remember(json)).at_path(path)
18
+ else
19
+ expect(last_json).to be_json_eql(RSpec::Oj.remember(json)).at_path(path)
20
+ end
21
+ end
22
+
23
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be file "(.+)"$/) do |path, negative, file_path|
24
+ if negative
25
+ expect(last_json).not_to be_json_eql.to_file(file_path).at_path(path)
26
+ else
27
+ expect(last_json).to be_json_eql.to_file(file_path).at_path(path)
28
+ end
29
+ end
30
+
31
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/) do |path, negative, value| # rubocop:disable Layout/LineLength
32
+ if negative
33
+ expect(last_json).not_to be_json_eql(RSpec::Oj.remember(value)).at_path(path)
34
+ else
35
+ expect(last_json).to be_json_eql(RSpec::Oj.remember(value)).at_path(path)
36
+ end
37
+ end
38
+
39
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include:$/) do |path, negative, json|
40
+ if negative
41
+ expect(last_json).not_to include_json(RSpec::Oj.remember(json)).at_path(path)
42
+ else
43
+ expect(last_json).to include_json(RSpec::Oj.remember(json)).at_path(path)
44
+ end
45
+ end
46
+
47
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include file "(.+)"$/) do |path, negative, file_path|
48
+ if negative
49
+ expect(last_json).not_to include_json.from_file(file_path).at_path(path)
50
+ else
51
+ expect(last_json).to include_json.from_file(file_path).at_path(path)
52
+ end
53
+ end
54
+
55
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/) do |path, negative, value| # rubocop:disable Layout/LineLength
56
+ if negative
57
+ expect(last_json).not_to include_json(RSpec::Oj.remember(value)).at_path(path)
58
+ else
59
+ expect(last_json).to include_json(RSpec::Oj.remember(value)).at_path(path)
60
+ end
61
+ end
62
+
63
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should have the following:$/) do |base, table|
64
+ table.raw.each do |path, value|
65
+ path = [base, path].compact.join('/')
66
+
67
+ if value
68
+ step %(the JSON at "#{path}" should be:), value
69
+ else
70
+ step %(the JSON should have "#{path}")
71
+ end
72
+ end
73
+ end
74
+
75
+ Then(/^the (?:JSON|json)(?: response)? should( not)? have "(.*)"$/) do |negative, path|
76
+ if negative
77
+ expect(last_json).not_to have_json_path(path)
78
+ else
79
+ expect(last_json).to have_json_path(path)
80
+ end
81
+ end
82
+
83
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be an? (.*)$/) do |path, negative, type|
84
+ if negative
85
+ expect(last_json).not_to have_json_type(type).at_path(path)
86
+ else
87
+ expect(last_json).to have_json_type(type).at_path(path)
88
+ end
89
+ end
90
+
91
+ Then(/^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? have (\d+)/) do |path, negative, size|
92
+ if negative
93
+ expect(last_json).not_to have_json_size(size.to_i).at_path(path)
94
+ else
95
+ expect(last_json).to have_json_size(size.to_i).at_path(path)
96
+ end
97
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ class Error < StandardError
6
+ end
7
+
8
+ class MissingPath < Error
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ def to_s
16
+ %(Missing JSON path "#{path}")
17
+ end
18
+ end
19
+
20
+ class MissingDirectory < Error
21
+ def to_s
22
+ 'No JsonSpec.directory set'
23
+ end
24
+ end
25
+
26
+ class MissingFile < Error
27
+ attr_reader :path
28
+
29
+ def initialize(path)
30
+ @path = path
31
+ end
32
+
33
+ def to_s
34
+ "No JSON file at #{path}"
35
+ end
36
+ end
37
+
38
+ class EnumerableExpected < Error
39
+ attr_reader :actual_value
40
+
41
+ def initialize(actual_value)
42
+ @actual_value = actual_value
43
+ end
44
+
45
+ def to_s
46
+ "Enumerable expected, got #{actual_value.inspect}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Exclusion
6
+ module_function
7
+
8
+ def exclude_keys(ruby)
9
+ case ruby
10
+ when Hash
11
+ ruby.sort.each_with_object({}) do |(key, value), hash|
12
+ hash[key] = exclude_keys(value) unless exclude_key?(key)
13
+ end
14
+ when Array
15
+ ruby.map { |v| exclude_keys(v) }
16
+ else ruby
17
+ end
18
+ end
19
+
20
+ def exclude_key?(key)
21
+ excluded_keys.include?(key)
22
+ end
23
+
24
+ def excluded_keys
25
+ @excluded_keys ||= Set.new(RSpec::Oj.excluded_keys)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+
5
+ module RSpec
6
+ module Oj
7
+ module Helpers
8
+ extend self
9
+
10
+ def parse_json(json, path = nil)
11
+ return parse_json(generate_normalized_json(json), path) unless json.is_a?(String)
12
+
13
+ ruby = ::Oj.load("[#{json}]", mode: :compat).first
14
+ value_at_json_path(ruby, path)
15
+ rescue EncodingError
16
+ begin
17
+ ::Oj.load(json)
18
+ rescue ::Oj::ParseError
19
+ json
20
+ end
21
+ end
22
+
23
+ def normalize_json(json, path = nil)
24
+ ruby = parse_json(json, path)
25
+ generate_normalized_json(ruby)
26
+ end
27
+
28
+ def generate_normalized_json(ruby)
29
+ case ruby
30
+ when Hash, Array
31
+ ::Oj.dump(ruby, mode: :compat)
32
+ else
33
+ ::Oj.to_json(ruby, mode: :compat)
34
+ end
35
+ end
36
+
37
+ def load_json(relative_path)
38
+ missing_json_directory! unless RSpec::Oj.directory
39
+ path = File.join(RSpec::Oj.directory, relative_path)
40
+ missing_json_file!(path) unless File.exist?(path)
41
+ File.read(path)
42
+ end
43
+
44
+ private
45
+
46
+ def value_at_json_path(ruby, path)
47
+ return ruby unless path
48
+
49
+ path.split('/').reduce(ruby) do |memo, key|
50
+ case memo
51
+ when Hash
52
+ memo.fetch(key) { missing_json_path!(path) }
53
+ when Array
54
+ missing_json_path!(path) unless /^\d+$/.match?(key)
55
+ memo.fetch(key.to_i) { missing_json_path!(path) }
56
+ else
57
+ missing_json_path!(path)
58
+ end
59
+ end
60
+ end
61
+
62
+ def missing_json_path!(path)
63
+ raise RSpec::Oj::MissingPath, path
64
+ end
65
+
66
+ def missing_json_directory!
67
+ raise RSpec::Oj::MissingDirectory
68
+ end
69
+
70
+ def missing_json_file!(path)
71
+ raise RSpec::Oj::MissingFile, path
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Matchers
6
+ class BeJsonEql
7
+ include RSpec::Oj::Helpers
8
+ include RSpec::Oj::Exclusion
9
+ include RSpec::Oj::Messages
10
+
11
+ attr_reader :expected, :actual
12
+
13
+ def diffable?
14
+ true
15
+ end
16
+
17
+ def initialize(expected_json = nil)
18
+ @expected_json = expected_json
19
+ @path = nil
20
+ end
21
+
22
+ def matches?(actual_json)
23
+ raise 'Expected equivalent JSON not provided' if @expected_json.nil?
24
+
25
+ @actual = scrub(actual_json, @path)
26
+ @expected = scrub(@expected_json)
27
+ @actual == @expected
28
+ end
29
+
30
+ def at_path(path)
31
+ @path = path
32
+ self
33
+ end
34
+
35
+ def to_file(path)
36
+ @expected_json = load_json(path)
37
+ self
38
+ end
39
+
40
+ def excluding(*keys)
41
+ excluded_keys.merge(keys.map(&:to_s))
42
+ self
43
+ end
44
+
45
+ def including(*keys)
46
+ excluded_keys.subtract(keys.map(&:to_s))
47
+ self
48
+ end
49
+
50
+ def failure_message
51
+ message_with_path('Expected equivalent JSON')
52
+ end
53
+ alias failure_message_for_should failure_message
54
+
55
+ def failure_message_when_negated
56
+ message_with_path('Expected inequivalent JSON')
57
+ end
58
+ alias failure_message_for_should_not failure_message_when_negated
59
+
60
+ def description
61
+ message_with_path('equal JSON')
62
+ end
63
+
64
+ private
65
+
66
+ def scrub(json, path = nil)
67
+ generate_normalized_json(exclude_keys(parse_json(json, path))).chomp + "\n"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Matchers
6
+ class HaveJsonPath
7
+ include RSpec::Oj::Helpers
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def matches?(json)
14
+ parse_json(json, @path)
15
+ true
16
+ rescue RSpec::Oj::MissingPath
17
+ false
18
+ end
19
+
20
+ def failure_message
21
+ %(Expected JSON path "#{@path}")
22
+ end
23
+ alias failure_message_for_should failure_message
24
+
25
+ def failure_message_when_negated
26
+ %(Expected no JSON path "#{@path}")
27
+ end
28
+ alias failure_message_for_should_not failure_message_when_negated
29
+
30
+ def description
31
+ %(have JSON path "#{@path}")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Matchers
6
+ class HaveJsonSize
7
+ include RSpec::Oj::Helpers
8
+ include RSpec::Oj::Messages
9
+
10
+ def initialize(size)
11
+ @expected = size
12
+ @path = nil
13
+ end
14
+
15
+ def matches?(json)
16
+ ruby = parse_json(json, @path)
17
+ raise EnumerableExpected, ruby unless Enumerable === ruby
18
+
19
+ @actual = ruby.size
20
+ @actual == @expected
21
+ end
22
+
23
+ def at_path(path)
24
+ @path = path
25
+ self
26
+ end
27
+
28
+ def failure_message
29
+ message_with_path("Expected JSON value size to be #{@expected}, got #{@actual}")
30
+ end
31
+
32
+ def failure_message_when_negated
33
+ message_with_path("Expected JSON value size to not be #{@expected}, got #{@actual}")
34
+ end
35
+
36
+ def description
37
+ message_with_path(%(have JSON size "#{@expected}"))
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Matchers
6
+ class HaveJsonType
7
+ include RSpec::Oj::Helpers
8
+ include RSpec::Oj::Messages
9
+
10
+ def initialize(type)
11
+ @classes = type_to_classes(type)
12
+ @path = nil
13
+ end
14
+
15
+ def matches?(json)
16
+ @ruby = parse_json(json, @path)
17
+ @classes.any? { |c| c === @ruby }
18
+ end
19
+
20
+ def at_path(path)
21
+ @path = path
22
+ self
23
+ end
24
+
25
+ def failure_message
26
+ message_with_path("Expected JSON value type to be #{@classes.join(', ')}, got #{@ruby.class}")
27
+ end
28
+
29
+ def failure_message_when_negated
30
+ message_with_path("Expected JSON value type to not be #{@classes.join(', ')}, got #{@ruby.class}")
31
+ end
32
+
33
+ def description
34
+ message_with_path(%(have JSON type "#{@classes.join(', ')}"))
35
+ end
36
+
37
+ private
38
+
39
+ def type_to_classes(type)
40
+ case type
41
+ when Class then [type]
42
+ when Array then type.map { |t| type_to_classes(t) }.flatten
43
+ else
44
+ case type.to_s.downcase
45
+ when 'boolean' then [TrueClass, FalseClass]
46
+ when 'object' then [Hash]
47
+ when 'nil', 'null' then [NilClass]
48
+ else [Module.const_get(type.to_s.capitalize)]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Matchers
6
+ class IncludeJson
7
+ include RSpec::Oj::Helpers
8
+ include RSpec::Oj::Exclusion
9
+ include RSpec::Oj::Messages
10
+
11
+ def initialize(expected_json = nil)
12
+ @expected_json = expected_json
13
+ @path = nil
14
+ end
15
+
16
+ def matches?(actual_json)
17
+ raise 'Expected included JSON not provided' if @expected_json.nil?
18
+
19
+ @actual_json = actual_json
20
+
21
+ actual = parse_json(actual_json, @path)
22
+ expected = exclude_keys(parse_json(@expected_json))
23
+ case actual
24
+ when Hash then actual.values.map { |v| exclude_keys(v) }.include?(expected)
25
+ when Array then actual.map { |e| exclude_keys(e) }.include?(expected)
26
+ when String then actual.include?(expected)
27
+ else false
28
+ end
29
+ end
30
+
31
+ def at_path(path)
32
+ @path = path
33
+ self
34
+ end
35
+
36
+ def from_file(path)
37
+ @expected_json = load_json(path)
38
+ self
39
+ end
40
+
41
+ def excluding(*keys)
42
+ excluded_keys.merge(keys.map(&:to_s))
43
+ self
44
+ end
45
+
46
+ def including(*keys)
47
+ excluded_keys.subtract(keys.map(&:to_s))
48
+ self
49
+ end
50
+
51
+ def failure_message
52
+ message_with_path("Expected #{@actual_json} to include #{@expected_json}")
53
+ end
54
+ alias failure_message_for_should failure_message
55
+
56
+ def failure_message_when_negated
57
+ message_with_path("Expected #{@actual_json} to not include #{@expected_json}")
58
+ end
59
+ alias failure_message_for_should_not failure_message_when_negated
60
+
61
+ def description
62
+ message_with_path('include JSON')
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/oj/matchers/be_json_eql'
4
+ require 'rspec/oj/matchers/include_json'
5
+ require 'rspec/oj/matchers/have_json_path'
6
+ require 'rspec/oj/matchers/have_json_type'
7
+ require 'rspec/oj/matchers/have_json_size'
8
+
9
+ module RSpec
10
+ module Oj
11
+ module Matchers
12
+ def be_json_eql(json = nil)
13
+ RSpec::Oj::Matchers::BeJsonEql.new(json)
14
+ end
15
+
16
+ def include_json(json = nil)
17
+ RSpec::Oj::Matchers::IncludeJson.new(json)
18
+ end
19
+
20
+ def have_json_path(path) # rubocop:disable Naming/PredicateName
21
+ RSpec::Oj::Matchers::HaveJsonPath.new(path)
22
+ end
23
+
24
+ def have_json_type(type) # rubocop:disable Naming/PredicateName
25
+ RSpec::Oj::Matchers::HaveJsonType.new(type)
26
+ end
27
+
28
+ def have_json_size(size) # rubocop:disable Naming/PredicateName
29
+ RSpec::Oj::Matchers::HaveJsonSize.new(size)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ RSpec.configure do |config|
36
+ config.include RSpec::Oj::Matchers
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Memory
6
+ def memory
7
+ @memory ||= {}
8
+ end
9
+
10
+ def memorize(key, value)
11
+ memory[key.to_sym] = value
12
+ end
13
+
14
+ def remember(json)
15
+ memory.empty? ? json : json % memory
16
+ end
17
+
18
+ def forget
19
+ memory.clear
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ module Messages
6
+ def message_with_path(message)
7
+ message = +message << %( at path "#{@path}") if @path
8
+ message
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Oj
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
data/lib/rspec-oj.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+ require 'rspec'
5
+ require 'rspec/oj/errors'
6
+ require 'rspec/oj/configuration'
7
+ require 'rspec/oj/exclusion'
8
+ require 'rspec/oj/helpers'
9
+ require 'rspec/oj/messages'
10
+ require 'rspec/oj/matchers'
11
+ require 'rspec/oj/memory'
12
+
13
+ module RSpec
14
+ module Oj
15
+ extend Configuration
16
+ extend Memory
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,206 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-oj
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mikael Henriksson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '5.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '3.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.1'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.1'
61
+ - !ruby/object:Gem::Dependency
62
+ name: gem-release
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.1'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: pry
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.12.2
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.12.2
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: reek
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '5.0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop-mhenrixon
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.80.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.80.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: simplecov
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.17.0
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 0.17.0
145
+ - !ruby/object:Gem::Dependency
146
+ name: simplecov-oj
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.18.0
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 0.18.0
159
+ description: RSpec matchers and Cucumber steps for testing JSON content
160
+ email:
161
+ - mikael@mhenrixon.com
162
+ executables: []
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - LICENSE.txt
167
+ - README.md
168
+ - lib/rspec-oj.rb
169
+ - lib/rspec/oj/configuration.rb
170
+ - lib/rspec/oj/cucumber.rb
171
+ - lib/rspec/oj/errors.rb
172
+ - lib/rspec/oj/exclusion.rb
173
+ - lib/rspec/oj/helpers.rb
174
+ - lib/rspec/oj/matchers.rb
175
+ - lib/rspec/oj/matchers/be_json_eql.rb
176
+ - lib/rspec/oj/matchers/have_json_path.rb
177
+ - lib/rspec/oj/matchers/have_json_size.rb
178
+ - lib/rspec/oj/matchers/have_json_type.rb
179
+ - lib/rspec/oj/matchers/include_json.rb
180
+ - lib/rspec/oj/memory.rb
181
+ - lib/rspec/oj/messages.rb
182
+ - lib/rspec/oj/version.rb
183
+ homepage: https://github.com/mhenrixon/rspec-oj
184
+ licenses:
185
+ - MIT
186
+ metadata: {}
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib/rspec
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubygems_version: 3.1.2
203
+ signing_key:
204
+ specification_version: 4
205
+ summary: Easily handle JSON in RSpec and Cucumber
206
+ test_files: []