rspec-json_api 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: ab0bec99f7a2855d51112412fb0aced653ad442808ce16921c651bab808457ac
4
+ data.tar.gz: 2d8707cc7ea15377f0239dc235c86fc6ca7566667751823e430e3cc88d05ea1a
5
+ SHA512:
6
+ metadata.gz: f40f7bca528dfb1aac30052c0b83d9e515b3b88115a7ed7c60d7b6c77e56db0dd3077e8f6c0dac43187d97d67ef392f26bd2eff4e5cb1ca3f29cd34c2bd8bfe2
7
+ data.tar.gz: e6c9ea96e7ad17af2c1ebaa0d6f73e335f929d4ec50f3355e051b4b3f419bb73441169b406d47f5b058594317ca099d711b9a718e0e5e266b357f5a9c8c4070b
@@ -0,0 +1,20 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.1
14
+ - name: Bundle gems
15
+ run: |
16
+ gem install bundler -v 2.2.15
17
+ bundle install
18
+ - name: Run rspec
19
+ run: |
20
+ bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0.1
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-08-24
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at m.gajowiak@nomtek.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
+
6
+ # Specify your gem's dependencies in rspec-json_api.gemspec
7
+ gemspec
8
+
9
+ gem "activesupport", "~> 6.1", ">= 6.1.4.1"
10
+ gem "rake", "~> 13.0", ">= 13.0.6"
11
+ gem "rspec-rails", "~> 5.0", ">= 5.0.2"
12
+
13
+
data/Gemfile.lock ADDED
@@ -0,0 +1,165 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rspec-json_api (1.0.0)
5
+ activesupport (~> 6.1, >= 6.1.4.1)
6
+ rails (~> 6.1, >= 6.1.4.1)
7
+ rspec-rails (~> 5.0, >= 5.0.2)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actioncable (6.1.4.1)
13
+ actionpack (= 6.1.4.1)
14
+ activesupport (= 6.1.4.1)
15
+ nio4r (~> 2.0)
16
+ websocket-driver (>= 0.6.1)
17
+ actionmailbox (6.1.4.1)
18
+ actionpack (= 6.1.4.1)
19
+ activejob (= 6.1.4.1)
20
+ activerecord (= 6.1.4.1)
21
+ activestorage (= 6.1.4.1)
22
+ activesupport (= 6.1.4.1)
23
+ mail (>= 2.7.1)
24
+ actionmailer (6.1.4.1)
25
+ actionpack (= 6.1.4.1)
26
+ actionview (= 6.1.4.1)
27
+ activejob (= 6.1.4.1)
28
+ activesupport (= 6.1.4.1)
29
+ mail (~> 2.5, >= 2.5.4)
30
+ rails-dom-testing (~> 2.0)
31
+ actionpack (6.1.4.1)
32
+ actionview (= 6.1.4.1)
33
+ activesupport (= 6.1.4.1)
34
+ rack (~> 2.0, >= 2.0.9)
35
+ rack-test (>= 0.6.3)
36
+ rails-dom-testing (~> 2.0)
37
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
38
+ actiontext (6.1.4.1)
39
+ actionpack (= 6.1.4.1)
40
+ activerecord (= 6.1.4.1)
41
+ activestorage (= 6.1.4.1)
42
+ activesupport (= 6.1.4.1)
43
+ nokogiri (>= 1.8.5)
44
+ actionview (6.1.4.1)
45
+ activesupport (= 6.1.4.1)
46
+ builder (~> 3.1)
47
+ erubi (~> 1.4)
48
+ rails-dom-testing (~> 2.0)
49
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
50
+ activejob (6.1.4.1)
51
+ activesupport (= 6.1.4.1)
52
+ globalid (>= 0.3.6)
53
+ activemodel (6.1.4.1)
54
+ activesupport (= 6.1.4.1)
55
+ activerecord (6.1.4.1)
56
+ activemodel (= 6.1.4.1)
57
+ activesupport (= 6.1.4.1)
58
+ activestorage (6.1.4.1)
59
+ actionpack (= 6.1.4.1)
60
+ activejob (= 6.1.4.1)
61
+ activerecord (= 6.1.4.1)
62
+ activesupport (= 6.1.4.1)
63
+ marcel (~> 1.0.0)
64
+ mini_mime (>= 1.1.0)
65
+ activesupport (6.1.4.1)
66
+ concurrent-ruby (~> 1.0, >= 1.0.2)
67
+ i18n (>= 1.6, < 2)
68
+ minitest (>= 5.1)
69
+ tzinfo (~> 2.0)
70
+ zeitwerk (~> 2.3)
71
+ builder (3.2.4)
72
+ concurrent-ruby (1.1.9)
73
+ crass (1.0.6)
74
+ diff-lcs (1.4.4)
75
+ erubi (1.10.0)
76
+ globalid (0.5.2)
77
+ activesupport (>= 5.0)
78
+ i18n (1.8.10)
79
+ concurrent-ruby (~> 1.0)
80
+ loofah (2.12.0)
81
+ crass (~> 1.0.2)
82
+ nokogiri (>= 1.5.9)
83
+ mail (2.7.1)
84
+ mini_mime (>= 0.1.1)
85
+ marcel (1.0.2)
86
+ method_source (1.0.0)
87
+ mini_mime (1.1.2)
88
+ minitest (5.14.4)
89
+ nio4r (2.5.8)
90
+ nokogiri (1.12.4-x86_64-darwin)
91
+ racc (~> 1.4)
92
+ racc (1.5.2)
93
+ rack (2.2.3)
94
+ rack-test (1.1.0)
95
+ rack (>= 1.0, < 3)
96
+ rails (6.1.4.1)
97
+ actioncable (= 6.1.4.1)
98
+ actionmailbox (= 6.1.4.1)
99
+ actionmailer (= 6.1.4.1)
100
+ actionpack (= 6.1.4.1)
101
+ actiontext (= 6.1.4.1)
102
+ actionview (= 6.1.4.1)
103
+ activejob (= 6.1.4.1)
104
+ activemodel (= 6.1.4.1)
105
+ activerecord (= 6.1.4.1)
106
+ activestorage (= 6.1.4.1)
107
+ activesupport (= 6.1.4.1)
108
+ bundler (>= 1.15.0)
109
+ railties (= 6.1.4.1)
110
+ sprockets-rails (>= 2.0.0)
111
+ rails-dom-testing (2.0.3)
112
+ activesupport (>= 4.2.0)
113
+ nokogiri (>= 1.6)
114
+ rails-html-sanitizer (1.4.2)
115
+ loofah (~> 2.3)
116
+ railties (6.1.4.1)
117
+ actionpack (= 6.1.4.1)
118
+ activesupport (= 6.1.4.1)
119
+ method_source
120
+ rake (>= 0.13)
121
+ thor (~> 1.0)
122
+ rake (13.0.6)
123
+ rspec-core (3.10.1)
124
+ rspec-support (~> 3.10.0)
125
+ rspec-expectations (3.10.1)
126
+ diff-lcs (>= 1.2.0, < 2.0)
127
+ rspec-support (~> 3.10.0)
128
+ rspec-mocks (3.10.2)
129
+ diff-lcs (>= 1.2.0, < 2.0)
130
+ rspec-support (~> 3.10.0)
131
+ rspec-rails (5.0.2)
132
+ actionpack (>= 5.2)
133
+ activesupport (>= 5.2)
134
+ railties (>= 5.2)
135
+ rspec-core (~> 3.10)
136
+ rspec-expectations (~> 3.10)
137
+ rspec-mocks (~> 3.10)
138
+ rspec-support (~> 3.10)
139
+ rspec-support (3.10.2)
140
+ sprockets (4.0.2)
141
+ concurrent-ruby (~> 1.0)
142
+ rack (> 1, < 3)
143
+ sprockets-rails (3.2.2)
144
+ actionpack (>= 4.0)
145
+ activesupport (>= 4.0)
146
+ sprockets (>= 3.0.0)
147
+ thor (1.1.0)
148
+ tzinfo (2.0.4)
149
+ concurrent-ruby (~> 1.0)
150
+ websocket-driver (0.7.5)
151
+ websocket-extensions (>= 0.1.0)
152
+ websocket-extensions (0.1.5)
153
+ zeitwerk (2.4.2)
154
+
155
+ PLATFORMS
156
+ x86_64-darwin-20
157
+
158
+ DEPENDENCIES
159
+ activesupport (~> 6.1, >= 6.1.4.1)
160
+ rake (~> 13.0, >= 13.0.6)
161
+ rspec-json_api!
162
+ rspec-rails (~> 5.0, >= 5.0.2)
163
+
164
+ BUNDLED WITH
165
+ 2.2.19
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Michal Gajowiak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # RSpec::JsonApi
2
+
3
+ [RSpec:JsonAPI](https://github.com/nomtek/rspec-json_api)
4
+ is an extension for [RSpec](https://github.com/rspec)
5
+ to easily allow testing JSON API responses.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rspec-json_api'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rspec-json_api
22
+
23
+ Generate directory tree:
24
+
25
+ rails generate rspec:json_api:install
26
+
27
+ Require gem assets in your `spec_helper.rb`
28
+ ```ruby
29
+ Dir[File.join(__dir__, 'rspec', 'json_api', '**', '*.rb')].each { |file| require file }
30
+ ```
31
+
32
+ ## Generators
33
+
34
+ Using build-in generators it's possible to create custom interface and type.
35
+
36
+ Generate new template:
37
+
38
+ rails generate rspec:json_api:interface interface-name
39
+
40
+ Generate new type:
41
+
42
+ rails generate rspec:json_api:types type-name
43
+
44
+
45
+ ## Example usage
46
+
47
+ ```ruby
48
+ # spec/controllers/users_controller_spec.rb
49
+
50
+ RSpec.describe UsersController, type: :controller do
51
+ describe '#index' do
52
+ let(:expected_schema) do
53
+ Array[{
54
+ id: RSpec::JsonApi::Types::UUID,
55
+ name: String,
56
+ age: Integer,
57
+ favouriteColorHex: /^\#([a-fA-F]|[0-9]){3,6}$/,
58
+ number: -> { { type: Integer, min: 10, max: 20, lambda: ->(actual) { actual.even? } } }
59
+ }]
60
+ end
61
+
62
+ it 'matches API response' do
63
+ get :index
64
+
65
+ expect(response.body).to match_json_schema(expected_schema)
66
+ end
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Built-in matchers
72
+ - ### match_json_schema
73
+ ```
74
+ expect(response.body).to match_json_schema(expected_schema)
75
+ ```
76
+
77
+ ## Interfaces
78
+ The gem introduces interfaces to reuse them during test matches.
79
+
80
+ ```ruby
81
+ # spec/rspec/json_api/interfaces/example_interface.rb
82
+
83
+ module RSpec
84
+ module JsonApi
85
+ module Interfaces
86
+ EXAMPLE_INTERFACE = {
87
+ id: Types::UUID,
88
+ name: String,
89
+ number: Integer,
90
+ color: -> { { inclusion: %w[black red white], allow_blank: true } }
91
+ }.freeze
92
+ end
93
+ end
94
+ end
95
+ ```
96
+ _Note: You can either generate file on your own or use generator._
97
+ ## Types
98
+
99
+ The gem allow users either to user build-in types or define owns.
100
+ ### Build-in types
101
+ - #### EMAIL
102
+ ```ruby
103
+ RSpec::JsonApi::Types::EMAIL
104
+ ```
105
+ - #### URI
106
+ ```ruby
107
+ RSpec::JsonApi::Types::URI
108
+ ```
109
+ - #### UUID
110
+ ```ruby
111
+ RSpec::JsonApi::Types::UUID
112
+ ```
113
+
114
+
115
+ Custom type example:
116
+ ```ruby
117
+ # spec/rspec/json_api/types/color_hex.rb
118
+
119
+ module RSpec
120
+ module JsonApi
121
+ module Types
122
+ COLOR_HEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/
123
+ end
124
+ end
125
+ end
126
+
127
+ RSpec::JsonApi::Types::COLOR_HEX
128
+ ```
129
+
130
+ _Note: You can either generate file on your own or use generator._
131
+ ## Matching methods
132
+ The gem offers variety of possible matching methods.
133
+
134
+ ### Presumptions
135
+ - `match_json_schema` always require full keys match.
136
+
137
+ Failure Example:
138
+ ```ruby
139
+ let(:expected) do
140
+ {
141
+ id: RSpec::JsonApi::Types::UUID,
142
+ name: String,
143
+ age: Integer
144
+ }
145
+ end
146
+
147
+ let(:actual) do
148
+ {
149
+ id: "0a2f911f-3767-4cc7-9c19-049f4350e38c",
150
+ name: "Mikel",
151
+ }
152
+ end
153
+ ```
154
+
155
+ Success Example:
156
+ ```ruby
157
+ let(:expected) do
158
+ {
159
+ id: RSpec::JsonApi::Types::UUID,
160
+ name: String,
161
+ age: Integer
162
+ }
163
+ end
164
+
165
+ let(:actual) do
166
+ {
167
+ id: "0a2f911f-3767-4cc7-9c19-049f4350e38c",
168
+ name: "John",
169
+ age: 24
170
+ }
171
+ end
172
+ ```
173
+
174
+ ### Value match
175
+ ```ruby
176
+ let(:expected_schema) do
177
+ {
178
+ id: "e0067346-4d24-4aa6-b303-f927a410a001",
179
+ name: "John",
180
+ age: 24,
181
+ favouriteColorHex: "#FF5733"
182
+ }
183
+ end
184
+ ```
185
+
186
+ ### Class match
187
+ ```ruby
188
+ let(:expected_schema) do
189
+ {
190
+ id: Integer,
191
+ name: String,
192
+ age: Integer,
193
+ notes: Array[String]
194
+ }
195
+ end
196
+ ```
197
+
198
+ ### Type match
199
+ ```ruby
200
+ let(:expected_schema) do
201
+ {
202
+ id: RSpec::JsonApi::Types::UUID,
203
+ email: RSpec::JsonApi::Types::EMAIL,
204
+ }
205
+ end
206
+ ```
207
+
208
+ ### Regexp match
209
+ ```ruby
210
+ let(:expected_schema) do
211
+ {
212
+ color: /^\#([a-fA-F]|[0-9]){3,6}$/
213
+ }
214
+ end
215
+ ```
216
+
217
+ ### Interface match
218
+ ```ruby
219
+ let(:expected_schema) do
220
+ Array[RSpec::JsonApi::Interfaces::PERSON]
221
+ end
222
+ ```
223
+
224
+ ### Proc match
225
+ Proc match allows to customize schema accoring needs using lambda shorthand notation `->`
226
+
227
+ Supported options:
228
+ - #### type
229
+ ```ruby
230
+ let(:expected_schema) do
231
+ {
232
+ name: -> { { type: String } }
233
+ }
234
+ end
235
+ ```
236
+ - #### value
237
+ ```ruby
238
+ let(:expected_schema) do
239
+ {
240
+ name: -> { { value: "John" } }
241
+ }
242
+ end
243
+ ```
244
+ - #### min
245
+ ```ruby
246
+ let(:expected_schema) do
247
+ {
248
+ age: -> { { min: 15 } }
249
+ }
250
+ end
251
+ ```
252
+ - #### max
253
+ ```ruby
254
+ let(:expected_schema) do
255
+ {
256
+ age: -> { { max: 25 } }
257
+ }
258
+ end
259
+ ```
260
+ - #### inclusion
261
+ ```ruby
262
+ let(:expected_schema) do
263
+ {
264
+ letter: -> { { inclusion: %w[A B C] } }
265
+ }
266
+ end
267
+ ```
268
+ - #### regex
269
+ ```ruby
270
+ let(:expected_schema) do
271
+ {
272
+ hex: -> { { regex: /^\#([a-fA-F]|[0-9]){3,6}$/ } }
273
+ }
274
+ end
275
+ ```
276
+ - #### lambda
277
+ ```ruby
278
+ let(:expected) do
279
+ {
280
+ number: -> { { lambda: ->(actual) { actual.even? } } }
281
+ }
282
+ end
283
+ ```
284
+ - #### allow_blank
285
+
286
+ ```ruby
287
+ let(:expected_schema) do
288
+ {
289
+ name: -> { { type: String, allow_blank: true } }
290
+ }
291
+ end
292
+ ```
293
+ _Note: Default value is `false`_
294
+
295
+ ## Contributing
296
+
297
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nomtek/rspec-json_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nomtek/rspec-json_api/blob/master/CODE_OF_CONDUCT.md).
298
+
299
+ ## License
300
+
301
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
302
+
303
+ ## Code of Conduct
304
+
305
+ Everyone interacting in the RSpec::JsonApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nomtek/rspec-json_api/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "rspec/json_api"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extention methods for hash class
4
+ class Hash
5
+ def deep_keys
6
+ each_with_object([]) do |(k, v), keys|
7
+ keys << k
8
+ keys << v.deep_keys if v.respond_to?(:keys)
9
+ end
10
+ end
11
+
12
+ def deep_key_paths
13
+ stack = map { |k, v| [[k], v] }
14
+ key_map = []
15
+
16
+ until stack.empty?
17
+ key, value = stack.pop
18
+
19
+ key_map << key unless value.is_a? Hash
20
+
21
+ next unless value.is_a? Hash
22
+
23
+ value.map do |k, v|
24
+ stack.push [key.dup << k, v]
25
+ end
26
+ end
27
+
28
+ key_map.reverse
29
+ end
30
+
31
+ def sanitize!(keys)
32
+ keep_if do |k, _v|
33
+ keys.include?(k)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rspec
4
+ module JsonApi
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def create_dir_scaffold
10
+ directory "rspec", "spec/rspec"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rspec
4
+ module JsonApi
5
+ module Generators
6
+ class InterfaceGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def copy_interface_file
10
+ create_file "spec/rspec/json_api/interfaces/#{file_name}.rb", <<~FILE
11
+ module RSpec
12
+ module JsonApi
13
+ module Interfaces
14
+ #{file_name.upcase} = {
15
+ # name: String
16
+ }
17
+ end
18
+ end
19
+ end
20
+ FILE
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ <%= module RSpec %>
2
+ <%= module JsonApi %>
3
+ <%= module Interfaces %>
4
+ <%= const_set(file_name, {
5
+
6
+ }.freeze) %>
7
+ <%= end %>
8
+ <%= end %>
9
+ <%= end %>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rspec
4
+ module JsonApi
5
+ module Generators
6
+ class TypeGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def copy_interface_file
10
+ create_file "spec/rspec/json_api/types/#{file_name}.rb", <<~FILE
11
+ module RSpec
12
+ module JsonApi
13
+ module Types
14
+ #{file_name.upcase} = //
15
+ end
16
+ end
17
+ end
18
+ FILE
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module CompareArray
6
+ extend self
7
+
8
+ def compare(actual, expected)
9
+ if interface?(expected)
10
+ actual.all? do |actual_elem|
11
+ # Compare actual and expected schema
12
+ return false unless actual_elem.deep_keys == expected[0].deep_keys
13
+
14
+ CompareHash.compare(actual_elem, expected[0])
15
+ end
16
+ else
17
+ actual.each_with_index.all? do |actual_elem, index|
18
+ # Compare actual and expected schema
19
+ return false unless actual[index].deep_keys == expected[index].deep_keys
20
+
21
+ CompareHash.compare(actual_elem, expected[index])
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def interface?(expected_value)
29
+ expected_value.size == 1 && expected_value[0].is_a?(Hash)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module CompareHash
6
+ extend self
7
+
8
+ SUPPORTED_OPTIONS = %i[allow_blank type value min max inclusion regex lambda].freeze
9
+
10
+ def compare(actual, expected)
11
+ return false if actual.blank? && expected.present?
12
+
13
+ key_paths = actual.deep_key_paths
14
+
15
+ key_paths.all? do |key_path|
16
+ actual_value = actual.dig(*key_path)
17
+ expected_value = expected.dig(*key_path)
18
+
19
+ compare_hash_values(actual_value, expected_value)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def compare_hash_values(actual_value, expected_value)
26
+ case expected_value
27
+ when Class
28
+ compare_class(actual_value, expected_value)
29
+ when Regexp
30
+ compare_regexp(actual_value, expected_value)
31
+ when Proc
32
+ compare_proc(actual_value, expected_value)
33
+ when Array
34
+ compare_array(actual_value, expected_value)
35
+ else
36
+ compare_value(actual_value, expected_value)
37
+ end
38
+ end
39
+
40
+ def compare_class(actual_value, expected_value)
41
+ actual_value.instance_of?(expected_value)
42
+ end
43
+
44
+ def compare_regexp(actual_value, expected_value)
45
+ actual_value =~ expected_value
46
+ end
47
+
48
+ def compare_proc(actual_value, expected_value)
49
+ payload = expected_value.call
50
+ payload.sanitize!(SUPPORTED_OPTIONS)
51
+ payload = payload.sort_by { |k, _v| k == :allow_blank ? 0 : 1 }.to_h
52
+
53
+ payload.all? do |condition_key, condition_value|
54
+ case condition_key
55
+ when :allow_blank
56
+ return true if actual_value.blank? && condition_value
57
+
58
+ true
59
+ when :type
60
+ compare_class(actual_value, condition_value)
61
+ when :value
62
+ compare_value(actual_value, condition_value)
63
+ when :inclusion
64
+ condition_value.include?(actual_value)
65
+ when :min
66
+ return false if !condition_value.is_a?(Numeric) || !actual_value.is_a?(Numeric)
67
+
68
+ actual_value >= condition_value
69
+ when :max
70
+ return false if !condition_value.is_a?(Numeric) || !actual_value.is_a?(Numeric)
71
+
72
+ actual_value <= condition_value
73
+ when :regex
74
+ compare_regexp(actual_value, condition_value)
75
+ when :lambda
76
+ condition_value.call(actual_value)
77
+ end
78
+ end
79
+ end
80
+
81
+ def compare_array(actual_value, expected_value)
82
+ if simple_type?(expected_value)
83
+ type = expected_value[0]
84
+
85
+ actual_value.all? { |elem| compare_class(elem, type) }
86
+ elsif interface?(expected_value)
87
+ interface = expected_value[0]
88
+
89
+ actual_value.all? { |elem| compare(elem, interface) }
90
+ else
91
+ return false if actual_value.size != expected_value.size
92
+
93
+ actual_value.each_with_index.all? do |elem, index|
94
+ elem.is_a?(Hash) ? compare(elem, expected_value[index]) : compare_value(elem, expected_value[index])
95
+ end
96
+ end
97
+ end
98
+
99
+ def compare_value(actual_value, expected_value)
100
+ actual_value == expected_value
101
+ end
102
+
103
+ def simple_type?(expected_value)
104
+ expected_value.size == 1 && expected_value[0].instance_of?(Class)
105
+ end
106
+
107
+ def interface?(expected_value)
108
+ expected_value.size == 1 && expected_value[0].is_a?(Hash)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Interfaces
6
+ EXAMPLE_INTERFACE = {
7
+ id: Types::UUID,
8
+ name: String,
9
+ number: Integer,
10
+ color: -> { { inclusion: %w[black red white], allow_blank: true } }
11
+ }.freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Matchers
6
+ class MatchJsonSchema
7
+ attr_reader :expected
8
+
9
+ def initialize(expected)
10
+ @expected = expected
11
+ end
12
+
13
+ def matches?(actual)
14
+ actual = JSON.parse(actual, symbolize_names: true)
15
+
16
+ # Compare types
17
+ return false unless actual.instance_of?(expected.class)
18
+
19
+ if expected.instance_of?(Array)
20
+ RSpec::JsonApi::CompareArray.compare(actual, expected)
21
+ else
22
+ # Compare actual and expected schema
23
+ return false unless actual.deep_keys == expected.deep_keys
24
+
25
+ RSpec::JsonApi::CompareHash.compare(actual, expected)
26
+ end
27
+ end
28
+
29
+ def failure_message
30
+ self
31
+ end
32
+
33
+ def failure_message_when_negated
34
+ self
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Matchers
5
+ def match_json_schema(expected)
6
+ RSpec::JsonApi::Matchers::MatchJsonSchema.new(expected)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Types
6
+ EMAIL = URI::MailTo::EMAIL_REGEXP
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Types
6
+ URI = URI::DEFAULT_PARSER.make_regexp
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Types
6
+ UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load 3th party libraries
4
+ require "json"
5
+ require "active_support/core_ext/object/blank"
6
+
7
+ # Load the json_api parts
8
+ require "rspec/json_api/version"
9
+ require "rspec/json_api/compare_hash"
10
+ require "rspec/json_api/compare_array"
11
+
12
+ # Load extentions
13
+ require "extentions/hash"
14
+
15
+ # Load matchers
16
+ require "rspec/json_api/matchers"
17
+ require "rspec/json_api/matchers/match_json_schema"
18
+
19
+ # Load defined types
20
+ require "rspec/json_api/types/email"
21
+ require "rspec/json_api/types/uri"
22
+ require "rspec/json_api/types/uuid"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rspec/json_api/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rspec-json_api"
7
+ spec.version = RSpec::JsonApi::VERSION
8
+ spec.authors = ["Michal Gajowiak"]
9
+ spec.email = ["m.gajowiak@nomtek.com"]
10
+
11
+ spec.summary = "RSpec extension to test JSON API response."
12
+ spec.homepage = "https://github.com/nomtek/rspec-json_api"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/nomtek/rspec-json_api"
18
+ spec.metadata["changelog_uri"] = "https://github.com/nomtek/rspec-json_api/blob/master/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ # Uncomment to register a new dependency of your gem
30
+ spec.add_dependency "activesupport", "~> 6.1", ">= 6.1.4.1"
31
+ spec.add_dependency "rails", "~> 6.1", ">= 6.1.4.1"
32
+ spec.add_dependency "rspec-rails", "~> 5.0", ">= 5.0.2"
33
+
34
+ # For more information and examples about making a new gem, checkout our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-json_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Gajowiak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.1.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '6.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.1.4.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: rails
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '6.1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 6.1.4.1
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '6.1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 6.1.4.1
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec-rails
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '5.0'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 5.0.2
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '5.0'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 5.0.2
73
+ description:
74
+ email:
75
+ - m.gajowiak@nomtek.com
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - ".github/workflows/main.yml"
81
+ - ".gitignore"
82
+ - ".rspec"
83
+ - ".rubocop.yml"
84
+ - CHANGELOG.md
85
+ - CODE_OF_CONDUCT.md
86
+ - Gemfile
87
+ - Gemfile.lock
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - bin/console
92
+ - bin/setup
93
+ - lib/extentions/hash.rb
94
+ - lib/generators/rspec/json_api/install/install_generator.rb
95
+ - lib/generators/rspec/json_api/install/templates/rspec/json_api/interfaces/.empty_directory
96
+ - lib/generators/rspec/json_api/install/templates/rspec/json_api/types/.empty_directory
97
+ - lib/generators/rspec/json_api/interface/interface_generator.rb
98
+ - lib/generators/rspec/json_api/interface/templates/interface.erb
99
+ - lib/generators/rspec/json_api/type/type_generator.rb
100
+ - lib/rspec/json_api.rb
101
+ - lib/rspec/json_api/compare_array.rb
102
+ - lib/rspec/json_api/compare_hash.rb
103
+ - lib/rspec/json_api/interfaces/example_interface.rb
104
+ - lib/rspec/json_api/matchers.rb
105
+ - lib/rspec/json_api/matchers/match_json_schema.rb
106
+ - lib/rspec/json_api/types/email.rb
107
+ - lib/rspec/json_api/types/uri.rb
108
+ - lib/rspec/json_api/types/uuid.rb
109
+ - lib/rspec/json_api/version.rb
110
+ - rspec-json_api.gemspec
111
+ homepage: https://github.com/nomtek/rspec-json_api
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ homepage_uri: https://github.com/nomtek/rspec-json_api
116
+ source_code_uri: https://github.com/nomtek/rspec-json_api
117
+ changelog_uri: https://github.com/nomtek/rspec-json_api/blob/master/CHANGELOG.md
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 3.0.0
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.2.16
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: RSpec extension to test JSON API response.
137
+ test_files: []