rspec-json_api 1.6.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0a09137349ac0fccf09529e902891d26148b10533b62c4d8f7731ba7529b5a3
4
- data.tar.gz: 79217a3491fb7c081f86b38305574ba201d7710572312f406edb55a939dee8fa
3
+ metadata.gz: 1af83b52a7a2a283b8f88e3d35920a6a5222fe10ec786a7a7f09f509ee923b75
4
+ data.tar.gz: 7772ea842f6e73370494969b5f5ad867ef0cb67f77449a95607b71190fd37545
5
5
  SHA512:
6
- metadata.gz: d50831bc5a1ea49bd7579524db9f61879f7568982953c1f449a4daa1a2a67b3d2a052a04e079c2bafc140832868497014bfded996d860aa60aa47b03a8f0db9e
7
- data.tar.gz: 2296ba594e1acf7f9cca856386ac963ec46b0a72b286d50a38a830f3f39ed12b585ee39274af1baf56863f138d84d250820be594ea9868caddbec0a65f139e32
6
+ metadata.gz: 7bd1537b2ea54021789bc2bf2d9ace9df346c2a75ee1d3183eafd6ca0aaebfa332b534f233047605bb252cc0afb76e35d9e7e26d97c921cbc44f321760ef1fe9
7
+ data.tar.gz: 6a2161022a3bff6d5af28398653289eaee7be1e3cc27cddeedd74cffaeb644a7e2548c6e5e04891e211c826a68b6c0d0de2cf7d98253459b0bc90d35e7ca126c
data/CHANGELOG.md CHANGED
@@ -1,4 +1,29 @@
1
- ## [Unreleased]
1
+ ## [2.0.0] - 2026-09-14
2
+
3
+ ### Fixed
4
+ - Exact arrays enforce full key structure for Hash elements. Extra null-valued keys and missing keys that happen to accept blank values no longer pass inside tuples.
5
+ - Nested key checks preserve each key's parent association. Objects with the same nested key names attached to different parents no longer match when the affected values are `null`.
6
+ - Regexp schemas require a String value instead of matching `to_s`. Numeric values and `null` no longer satisfy permissive regular expressions.
7
+
8
+ ### Changed
9
+ - Removed ActiveSupport blank/present core extensions in favor of a JSON-focused internal helper with the same relevant semantics.
10
+ - Runtime dependencies are now only `diffy` and `rspec-expectations`. Rails, Railties, ActiveSupport, and `rspec-rails` are no longer installed for matcher-only consumers. Projects using the optional generators must provide Rails themselves.
11
+ - The compatibility matrix tests matcher behavior without Rails on Ruby 3.2, 3.3, 3.4, and 4.0. Separate generator jobs exercise Rails 6.1 and Rails 8.1 at the supported boundaries.
12
+ - The README now documents strict key behavior, schema dispatch, errors, array shorthand, supported runtimes, and the fact that this is a general JSON shape matcher rather than a JSON:API implementation.
13
+ - CI runs `actions/checkout@v7`, up from v5, alongside `ruby/setup-ruby@v1`. Both track their major tag, so upstream patch releases arrive without a commit here and Dependabot opens a pull request for each new major. Checkout also runs with `persist-credentials: false`: no step needs git credentials, and bundler evaluates gemspec and native-extension code from the branch under test.
14
+ - The workflow runs once per change rather than twice. `push` is scoped to `master`, so a branch with an open pull request no longer builds under both events, and a `concurrency` group cancels superseded pull request runs. Runs on `master` are left alone, so every commit there keeps a result.
15
+ - `rubocop` and `bundler-audit` share one job definition instead of two byte-identical ones, and every job carries `timeout-minutes: 30` in place of the six-hour default.
16
+
17
+ ### Added
18
+ - Top-level Class and Regexp schemas now work for scalar JSON documents, using the same dispatch rules as nested values.
19
+ - `match_json_schema` exposes an RSpec description for one-line and documentation formatter output.
20
+ - Direct unit coverage for schema matching, traversal, constraints, and the Rails generators; randomized spec ordering; Ruby warnings; and a 90% SimpleCov floor.
21
+ - Contributor and security policy documents, included in the packaged gem.
22
+ - A weekly `schedule` trigger, so `bundler-audit` reports an advisory published against a lockfile nobody has touched. Only the audit runs on that trigger. `workflow_dispatch` runs it on demand and is also how the cron is re-armed, since GitHub disables scheduled workflows after 60 days without repository activity.
23
+
24
+ ### Removed
25
+ - The internal example interface fixture from the packaged library; it now lives under test support.
26
+ - Redundant Rails 7.1, 7.2, and 8.0 appraisal files after reducing generator compatibility checks to the supported boundaries.
2
27
 
3
28
  ## [1.6.0] - 2026-09-03
4
29
 
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,24 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome.
4
+
5
+ ## Setup
6
+
7
+ Use the Ruby version declared in `.ruby-version` and Bundler 4.0.4:
8
+
9
+ ```sh
10
+ gem install bundler -v 4.0.4
11
+ bundle install
12
+ ```
13
+
14
+ Run the local checks before opening a pull request:
15
+
16
+ ```sh
17
+ bundle exec rspec
18
+ bundle exec rubocop
19
+ bundle exec bundle-audit check --update
20
+ ```
21
+
22
+ Matcher changes should include focused examples under `spec/rspec/json_api`. Generator changes should include examples under `spec/generators` and be checked against both appraisal gemfiles.
23
+
24
+ Keep pull requests focused and explain user-visible behavior changes in `CHANGELOG.md`. By participating, you agree to follow the [code of conduct](CODE_OF_CONDUCT.md).
data/README.md CHANGED
@@ -1,135 +1,168 @@
1
1
  # RSpec::JsonApi
2
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.
3
+ `rspec-json_api` adds RSpec matchers for checking JSON values against compact Ruby schemas. Despite the name, it validates general JSON response shapes; it does not implement the [JSON:API specification](https://jsonapi.org/).
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 3.2 or newer
8
+ - RSpec 3
9
+ - Rails 6.1 or newer only when using the optional generators
10
+
11
+ The CI suite covers matcher behavior on Ruby 3.2, 3.3, 3.4, and 4.0. Generator integration is exercised at the supported Rails boundaries: Rails 6.1 on Ruby 3.2 and Rails 8.1 on Ruby 3.4.
6
12
 
7
13
  ## Installation
8
14
 
9
- Add this line to your application's Gemfile:
15
+ Add the gem to your test group:
10
16
 
11
17
  ```ruby
12
- gem 'rspec-json_api'
18
+ group :test do
19
+ gem "rspec-json_api"
20
+ end
13
21
  ```
14
22
 
15
- And then execute:
23
+ Then run:
16
24
 
17
- $ bundle install
25
+ ```sh
26
+ bundle install
27
+ ```
28
+
29
+ Load the matchers from `spec/spec_helper.rb` (or your equivalent RSpec setup file):
18
30
 
19
- Or install it yourself as:
31
+ ```ruby
32
+ require "rspec/json_api"
33
+ ```
20
34
 
21
- $ gem install rspec-json_api
35
+ Rails projects can generate the definition directories:
22
36
 
23
- Generate directory tree:
37
+ ```sh
38
+ rails generate rspec:json_api:install
39
+ ```
24
40
 
25
- rails generate rspec:json_api:install
41
+ Load custom types before interfaces in `rails_helper.rb`, because interfaces may reference types:
26
42
 
27
- Require gem assets in your `rails_helper.rb`
28
43
  ```ruby
29
- Dir[File.join(__dir__, 'rspec', 'json_api', 'types', '*.rb')].each { |file| require file }
30
- Dir[File.join(__dir__, 'rspec', 'json_api', 'interfaces', '*.rb')].each { |file| require file }
44
+ Dir[File.join(__dir__, "rspec", "json_api", "types", "*.rb")].each { |file| require file }
45
+ Dir[File.join(__dir__, "rspec", "json_api", "interfaces", "*.rb")].each { |file| require file }
31
46
  ```
32
47
 
33
- ## Generators
48
+ The matchers themselves do not depend on Rails, ActiveSupport, or `rspec-rails`.
34
49
 
35
- Using build-in generators it's possible to create custom interface and type.
50
+ ## Matchers
36
51
 
37
- Generate new template:
52
+ ### `match_json_schema`
38
53
 
39
- rails generate rspec:json_api:interface interface-name
54
+ Pass the matcher a JSON String and describe the parsed value with Ruby values, classes, regular expressions, arrays, hashes, or constraint Procs:
40
55
 
41
- Generate new type:
56
+ ```ruby
57
+ schema = {
58
+ id: RSpec::JsonApi::Types::UUID,
59
+ name: String,
60
+ age: -> { { type: Integer, min: 18 } },
61
+ tags: [String]
62
+ }
42
63
 
43
- rails generate rspec:json_api:type type-name
64
+ expect(response.body).to match_json_schema(schema)
65
+ ```
44
66
 
67
+ The actual value must be a JSON String. Invalid JSON and non-String inputs fail the match. Schema keys must be symbols because JSON object keys are symbolized while parsing.
45
68
 
46
- ## Example usage
69
+ Object schemas are strict at every nesting level: every expected key must be present and unexpected keys fail the match. `allow_blank` permits a blank value; it does not make a key optional.
70
+
71
+ Root schemas may describe objects, arrays, or scalar JSON values:
47
72
 
48
73
  ```ruby
49
- # spec/controllers/users_controller_spec.rb
74
+ expect('"ready"').to match_json_schema(String)
75
+ expect('"ready"').to match_json_schema(/\Aready\z/)
76
+ expect("42").to match_json_schema(42)
77
+ ```
50
78
 
51
- RSpec.describe UsersController, type: :controller do
52
- describe '#index' do
53
- let(:expected_schema) do
54
- [{
55
- id: RSpec::JsonApi::Types::UUID,
56
- name: String,
57
- age: Integer,
58
- favoriteColorHex: /\A\#([a-fA-F]|[0-9]){3,6}\z/,
59
- number: -> { { type: Integer, min: 10, max: 20, lambda: lambda(&:even?) } }
60
- }]
61
- end
79
+ ### `have_no_content`
62
80
 
63
- it 'matches API response' do
64
- get :index
81
+ `have_no_content` matches only an empty String:
65
82
 
66
- expect(response.body).to match_json_schema(expected_schema)
67
- end
68
- end
69
-
70
- describe '#update' do
71
- it 'matches API response' do
72
- put :update, params: { name: 'John', age: 35 }
73
-
74
- expect(response.body).to have_no_content
75
- end
76
- end
77
- end
83
+ ```ruby
84
+ expect(response.body).to have_no_content
78
85
  ```
79
86
 
80
- ## Built-in matchers
81
- - ### match_json_schema
82
- ```
83
- expect(response.body).to match_json_schema(expected_schema)
84
- ```
87
+ JSON objects, JSON arrays, and whitespace-only bodies are considered content.
85
88
 
86
- - ### have_no_content
87
- ```
88
- expect(response.body).to have_no_content
89
+ ## Schema Values
90
+
91
+ ### Exact values
92
+
93
+ ```ruby
94
+ schema = { status: "ready", count: 2 }
89
95
  ```
90
96
 
91
- ## Interfaces
92
- The gem introduces interfaces to reuse them during test matches.
97
+ ### Classes
98
+
99
+ Classes use `instance_of?`, so subclasses do not match:
93
100
 
94
101
  ```ruby
95
- # spec/rspec/json_api/interfaces/example_interface.rb
102
+ schema = { id: Integer, name: String }
103
+ ```
96
104
 
97
- module RSpec
98
- module JsonApi
99
- module Interfaces
100
- EXAMPLE_INTERFACE = {
101
- id: Types::UUID,
102
- name: String,
103
- number: Integer,
104
- color: -> { { inclusion: %w[black red white], allow_blank: true } }
105
- }.freeze
106
- end
107
- end
108
- end
105
+ ### Regular expressions
106
+
107
+ A regular expression matches only a JSON String. Numbers, booleans, and `null` do not match after conversion:
108
+
109
+ ```ruby
110
+ schema = { color: /\A#[0-9a-fA-F]{6}\z/ }
109
111
  ```
110
- _Note: You can either generate file on your own or use generator._
111
- ## Types
112
112
 
113
- The gem allow users either to user build-in types or define owns.
114
- ### Build-in types
115
- - #### EMAIL
113
+ Use `\A` and `\z` for whole-string validation. Ruby's `^` and `$` are line anchors and may accept a matching line inside a multiline value.
114
+
115
+ ### Arrays
116
+
117
+ Array schemas have three forms:
118
+
116
119
  ```ruby
117
- RSpec::JsonApi::Types::EMAIL
120
+ [String] # any-length list of Strings
121
+ [{ id: Integer, name: String }] # any-length list of this object shape
122
+ [Integer, String] # an exact two-element tuple
118
123
  ```
119
- - #### URI
124
+
125
+ The one-element shorthand applies only to a Class or Hash. For example, `[Types::UUID]` means an exact one-element array because `Types::UUID` is a Regexp.
126
+
127
+ ### Constraint Procs
128
+
129
+ A constraint Proc takes no arguments and returns an options Hash:
130
+
120
131
  ```ruby
121
- RSpec::JsonApi::Types::URI
132
+ schema = {
133
+ age: -> { { type: Integer, min: 18, max: 120 } },
134
+ role: -> { { inclusion: %w[admin member] } },
135
+ code: -> { { regex: /\A[A-Z]{3}\z/ } },
136
+ even: -> { { lambda: ->(value) { value.even? } } },
137
+ nickname: -> { { type: String, allow_blank: true } }
138
+ }
122
139
  ```
123
- - #### UUID
140
+
141
+ Supported options are `allow_blank`, `type`, `value`, `min`, `max`, `inclusion`, `regex`, and `lambda`. All supplied constraints must pass. Unknown options, a non-Hash return value, or a Proc that declares an argument raises `ArgumentError` with usage guidance.
142
+
143
+ `allow_blank: true` accepts `null`, `false`, empty strings, whitespace-only strings, empty arrays, and empty objects. The key itself remains required.
144
+
145
+ ## Built-in Types
146
+
147
+ The built-in types are anchored regular expressions:
148
+
124
149
  ```ruby
150
+ RSpec::JsonApi::Types::EMAIL
151
+ RSpec::JsonApi::Types::URI
125
152
  RSpec::JsonApi::Types::UUID
126
153
  ```
127
154
 
155
+ `URI` accepts schemes supported by Ruby's standard URI parser, not only HTTP and HTTPS.
128
156
 
129
- Custom type example:
130
- ```ruby
131
- # spec/rspec/json_api/types/color_hex.rb
157
+ Generate a custom type with Rails:
158
+
159
+ ```sh
160
+ rails generate rspec:json_api:type color_hex
161
+ ```
162
+
163
+ Or define one directly:
132
164
 
165
+ ```ruby
133
166
  module RSpec
134
167
  module JsonApi
135
168
  module Types
@@ -137,184 +170,57 @@ module RSpec
137
170
  end
138
171
  end
139
172
  end
140
-
141
- RSpec::JsonApi::Types::COLOR_HEX
142
173
  ```
143
174
 
144
- _Note: You can either generate file on your own or use generator._
145
- ## Matching methods
146
- The gem offers variety of possible matching methods.
175
+ ## Interfaces
147
176
 
148
- ### Presumptions
149
- - `match_json_schema` always require full keys match.
177
+ Interfaces are reusable strict object schemas:
150
178
 
151
- Failure Example:
152
- ```ruby
153
- let(:expected_schema) do
154
- {
155
- id: RSpec::JsonApi::Types::UUID,
179
+ ```ruby
180
+ module RSpec
181
+ module JsonApi
182
+ module Interfaces
183
+ PERSON = {
184
+ id: Types::UUID,
156
185
  name: String,
157
- age: Integer
158
- }
159
- end
160
-
161
- let(:actual) do
162
- {
163
- id: "0a2f911f-3767-4cc7-9c19-049f4350e38c",
164
- name: "Mikel",
165
- }
186
+ active: -> { { inclusion: [true, false] } }
187
+ }.freeze
166
188
  end
167
- ```
168
-
169
- Success Example:
170
- ```ruby
171
- let(:expected_schema) do
172
- {
173
- id: RSpec::JsonApi::Types::UUID,
174
- name: String,
175
- age: Integer
176
- }
177
189
  end
178
-
179
- let(:actual) do
180
- {
181
- id: "0a2f911f-3767-4cc7-9c19-049f4350e38c",
182
- name: "John",
183
- age: 24
184
- }
185
- end
186
- ```
187
-
188
- ### Value match
189
- ```ruby
190
- let(:expected_schema) do
191
- {
192
- id: "e0067346-4d24-4aa6-b303-f927a410a001",
193
- name: "John",
194
- age: 24,
195
- favoriteColorHex: "#FF5733"
196
- }
197
190
  end
198
191
  ```
199
192
 
200
- ### Class match
201
- ```ruby
202
- let(:expected_schema) do
203
- {
204
- id: Integer,
205
- name: String,
206
- age: Integer,
207
- notes: [String]
208
- }
209
- end
210
- ```
193
+ Generate one with:
211
194
 
212
- ### Type match
213
- ```ruby
214
- let(:expected_schema) do
215
- {
216
- id: RSpec::JsonApi::Types::UUID,
217
- email: RSpec::JsonApi::Types::EMAIL,
218
- }
219
- end
195
+ ```sh
196
+ rails generate rspec:json_api:interface person
220
197
  ```
221
198
 
222
- ### Regexp match
223
- ```ruby
224
- let(:expected_schema) do
225
- {
226
- color: /\A\#([a-fA-F]|[0-9]){3,6}\z/
227
- }
228
- end
229
- ```
230
- _Note: anchor with `\A` and `\z`, not `^` and `$`. `^` and `$` match at line boundaries, so `/^\#[0-9a-fA-F]{3}$/` also accepts `"not a color\n#FFF"` and the value only has to contain a matching line for the schema to pass. The built-in `EMAIL`, `URI` and `UUID` types are anchored this way._
199
+ Use an interface directly or as a homogeneous list schema:
231
200
 
232
- ### Interface match
233
201
  ```ruby
234
- let(:expected_schema) do
235
- [RSpec::JsonApi::Interfaces::PERSON]
236
- end
202
+ expect(response.body).to match_json_schema(RSpec::JsonApi::Interfaces::PERSON)
203
+ expect(response.body).to match_json_schema([RSpec::JsonApi::Interfaces::PERSON])
237
204
  ```
238
205
 
239
- ### Proc match
240
- Proc match allows to customize schema according needs using lambda shorthand notation `->`
206
+ ## Development
241
207
 
242
- Supported options:
243
- - #### type
244
- ```ruby
245
- let(:expected_schema) do
246
- {
247
- name: -> { { type: String } }
248
- }
249
- end
250
- ```
251
- - #### value
252
- ```ruby
253
- let(:expected_schema) do
254
- {
255
- name: -> { { value: "John" } }
256
- }
257
- end
258
- ```
259
- - #### min
260
- ```ruby
261
- let(:expected_schema) do
262
- {
263
- age: -> { { min: 15 } }
264
- }
265
- end
266
- ```
267
- - #### max
268
- ```ruby
269
- let(:expected_schema) do
270
- {
271
- age: -> { { max: 25 } }
272
- }
273
- end
274
- ```
275
- - #### inclusion
276
- ```ruby
277
- let(:expected_schema) do
278
- {
279
- letter: -> { { inclusion: %w[A B C] } }
280
- }
281
- end
282
- ```
283
- - #### regex
284
- ```ruby
285
- let(:expected_schema) do
286
- {
287
- hex: -> { { regex: /^\#([a-fA-F]|[0-9]){3,6}$/ } }
288
- }
289
- end
290
- ```
291
- - #### lambda
292
- ```ruby
293
- let(:expected_schema) do
294
- {
295
- number: -> { { lambda: lambda(&:even?) } }
296
- }
297
- end
298
- ```
299
- - #### allow_blank
208
+ Use the Ruby version in `.ruby-version` and Bundler 4.0.4:
300
209
 
301
- ```ruby
302
- let(:expected_schema) do
303
- {
304
- name: -> { { type: String, allow_blank: true } }
305
- }
306
- end
210
+ ```sh
211
+ gem install bundler -v 4.0.4
212
+ bundle install
213
+ bundle exec rspec
214
+ bundle exec rubocop
215
+ bundle exec bundle-audit check --update
307
216
  ```
308
- _Note: Default value is `false`_
309
-
310
- ## Contributing
311
217
 
312
- 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).
218
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for compatibility and contribution guidance. Please report vulnerabilities using [GitHub's private security advisory form](https://github.com/nomtek/rspec-json_api/security/advisories/new), as described in [SECURITY.md](SECURITY.md).
313
219
 
314
220
  ## License
315
221
 
316
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
222
+ The gem is available under the terms of the [MIT License](LICENSE.txt).
317
223
 
318
224
  ## Code of Conduct
319
225
 
320
- 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).
226
+ Everyone participating in this project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
data/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are made on the latest released version.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please do not open a public issue for a suspected vulnerability. Use [GitHub's private security advisory form](https://github.com/nomtek/rspec-json_api/security/advisories/new) and include reproduction steps, affected versions, and the expected impact.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module JsonApi
5
+ module Blank
6
+ module_function
7
+
8
+ BLANK_STRING = /\A[[:space:]]*\z/
9
+
10
+ def blank?(value)
11
+ case value
12
+ when nil, false
13
+ true
14
+ when String
15
+ BLANK_STRING.match?(value)
16
+ else
17
+ value.respond_to?(:empty?) && value.empty?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -21,7 +21,7 @@ module RSpec
21
21
  def match(value, options)
22
22
  validate!(options)
23
23
 
24
- return true if value.blank? && options[:allow_blank]
24
+ return true if Blank.blank?(value) && options[:allow_blank]
25
25
 
26
26
  options.except(:allow_blank).all? do |option, condition|
27
27
  satisfies?(value, option, condition)
@@ -42,7 +42,7 @@ module RSpec
42
42
  when :type then value.instance_of?(condition)
43
43
  when :value then value == condition
44
44
  when :inclusion then condition.include?(value)
45
- when :regex then condition.match?(value.to_s)
45
+ when :regex then matches_regexp?(value, condition)
46
46
  when :lambda then condition.call(value)
47
47
  when :min, :max then within_bound?(value, option, condition)
48
48
  end
@@ -53,6 +53,10 @@ module RSpec
53
53
 
54
54
  option == :min ? value >= condition : value <= condition
55
55
  end
56
+
57
+ def matches_regexp?(value, condition)
58
+ value.is_a?(String) && condition.match?(value)
59
+ end
56
60
  end
57
61
  end
58
62
  end
@@ -45,6 +45,10 @@ module RSpec
45
45
  !matches?(actual) && !@type_error
46
46
  end
47
47
 
48
+ def description
49
+ "match JSON schema"
50
+ end
51
+
48
52
  # Provides a failure message for when the JSON data does not match the expected schema.
49
53
  # @return [String] A descriptive message detailing the mismatch between expected and actual JSON.
50
54
  def failure_message
@@ -10,31 +10,28 @@ module RSpec
10
10
  module SchemaMatch
11
11
  module_function
12
12
 
13
- # Top-level comparison. Applies the shape guards (class equality and, for
14
- # objects, key-set equality) before recursing.
13
+ # Top-level comparison. Applies shape guards to objects and collections,
14
+ # then uses the same value dispatch as nested schema values.
15
15
  def match(actual, expected)
16
- return false unless actual.instance_of?(expected.class)
17
-
18
16
  case expected
19
17
  when Array
20
18
  compare_array(actual, expected)
21
19
  when Hash
20
+ return false unless actual.is_a?(Hash)
22
21
  return false unless same_key_structure?(actual, expected)
23
22
 
24
23
  compare(actual, expected)
25
24
  else
26
- compare_simple_value(actual, expected)
25
+ compare_values(actual, expected)
27
26
  end
28
27
  end
29
28
 
30
29
  def same_key_structure?(actual, expected)
31
- Traversal.deep_sort(Traversal.deep_keys(actual)) ==
32
- Traversal.deep_sort(Traversal.deep_keys(expected))
30
+ Traversal.same_key_structure?(actual, expected)
33
31
  end
34
32
 
35
33
  def compare(actual, expected)
36
34
  return false unless actual.is_a?(Hash)
37
- return false if actual.blank? && expected.present?
38
35
 
39
36
  keys = Traversal.deep_key_paths(expected) | Traversal.deep_key_paths(actual)
40
37
 
@@ -77,7 +74,7 @@ module RSpec
77
74
  end
78
75
 
79
76
  def compare_regexp(actual_value, expected_value)
80
- expected_value.match?(actual_value.to_s)
77
+ actual_value.is_a?(String) && expected_value.match?(actual_value)
81
78
  end
82
79
 
83
80
  # A schema Proc describes the constraints for a value; it is called without
@@ -142,7 +139,7 @@ module RSpec
142
139
  return false if actual_value.size != expected_value.size
143
140
 
144
141
  expected_value.each_with_index.all? do |elem, index|
145
- elem.is_a?(Hash) ? compare(actual_value[index], elem) : compare_values(actual_value[index], elem)
142
+ elem.is_a?(Hash) ? match(actual_value[index], elem) : compare_values(actual_value[index], elem)
146
143
  end
147
144
  end
148
145
 
@@ -44,6 +44,22 @@ module RSpec
44
44
  .map { |element| element.is_a?(Array) ? deep_sort(element) : element }
45
45
  .sort_by { |element| element.is_a?(Array) ? element.first.to_s : element.to_s }
46
46
  end
47
+
48
+ # Whether two hashes have the same keys under the same parent objects.
49
+ def same_key_structure?(actual, expected)
50
+ same_keys?(actual, expected) && actual.all? do |key, actual_value|
51
+ same_nested_key_structure?(actual_value, expected[key])
52
+ end
53
+ end
54
+
55
+ def same_keys?(actual, expected)
56
+ actual.size == expected.size && actual.each_key.all? { |key| expected.key?(key) }
57
+ end
58
+
59
+ def same_nested_key_structure?(actual, expected)
60
+ nested = actual.is_a?(Hash) || expected.is_a?(Hash)
61
+ !nested || (actual.is_a?(Hash) && expected.is_a?(Hash) && same_key_structure?(actual, expected))
62
+ end
47
63
  end
48
64
  end
49
65
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module JsonApi
5
- VERSION = "1.6.0"
5
+ VERSION = "2.0.0"
6
6
  end
7
7
  end
@@ -4,10 +4,11 @@
4
4
  require "json"
5
5
  require "uri"
6
6
  require "diffy"
7
- require "active_support/core_ext/object/blank"
7
+ require "rspec/expectations"
8
8
 
9
9
  # Load the json_api parts
10
10
  require "rspec/json_api/version"
11
+ require "rspec/json_api/blank"
11
12
  require "rspec/json_api/traversal"
12
13
  require "rspec/json_api/constraints"
13
14
  require "rspec/json_api/schema_match"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-json_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Gajowiak
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: activesupport
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: 6.1.4.1
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: 6.1.4.1
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: diffy
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -38,33 +24,19 @@ dependencies:
38
24
  - !ruby/object:Gem::Version
39
25
  version: 3.4.2
40
26
  - !ruby/object:Gem::Dependency
41
- name: railties
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 6.1.4.1
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: 6.1.4.1
54
- - !ruby/object:Gem::Dependency
55
- name: rspec-rails
27
+ name: rspec-expectations
56
28
  requirement: !ruby/object:Gem::Requirement
57
29
  requirements:
58
- - - ">="
30
+ - - "~>"
59
31
  - !ruby/object:Gem::Version
60
- version: 5.0.2
32
+ version: '3.0'
61
33
  type: :runtime
62
34
  prerelease: false
63
35
  version_requirements: !ruby/object:Gem::Requirement
64
36
  requirements:
65
- - - ">="
37
+ - - "~>"
66
38
  - !ruby/object:Gem::Version
67
- version: 5.0.2
39
+ version: '3.0'
68
40
  email:
69
41
  - m.gajowiak@nomtek.com
70
42
  executables: []
@@ -72,16 +44,18 @@ extensions: []
72
44
  extra_rdoc_files: []
73
45
  files:
74
46
  - CHANGELOG.md
47
+ - CONTRIBUTING.md
75
48
  - LICENSE.txt
76
49
  - README.md
50
+ - SECURITY.md
77
51
  - lib/generators/rspec/json_api/install/install_generator.rb
78
52
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/interfaces/.empty_directory
79
53
  - lib/generators/rspec/json_api/install/templates/rspec/json_api/types/.empty_directory
80
54
  - lib/generators/rspec/json_api/interface/interface_generator.rb
81
55
  - lib/generators/rspec/json_api/type/type_generator.rb
82
56
  - lib/rspec/json_api.rb
57
+ - lib/rspec/json_api/blank.rb
83
58
  - lib/rspec/json_api/constraints.rb
84
- - lib/rspec/json_api/interfaces/example_interface.rb
85
59
  - lib/rspec/json_api/matchers.rb
86
60
  - lib/rspec/json_api/matchers/have_no_content.rb
87
61
  - lib/rspec/json_api/matchers/match_json_schema.rb
@@ -113,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
87
  - !ruby/object:Gem::Version
114
88
  version: '0'
115
89
  requirements: []
116
- rubygems_version: 4.0.3
90
+ rubygems_version: 3.6.9
117
91
  specification_version: 4
118
92
  summary: RSpec extension to test JSON API response.
119
93
  test_files: []
@@ -1,14 +0,0 @@
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