rspec-oj 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
+ 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: []