rspec-openapi 0.1.0 → 0.18.4
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 +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/release.yaml +24 -0
- data/.github/workflows/codeql-analysis.yml +39 -0
- data/.github/workflows/rubocop.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.rspec +2 -1
- data/.rubocop.yml +25 -0
- data/.rubocop_todo.yml +52 -0
- data/.simplecov_spawn.rb +16 -0
- data/CHANGELOG.md +287 -0
- data/Gemfile +28 -2
- data/README.md +267 -28
- data/Rakefile +8 -4
- data/bin/console +4 -3
- data/lib/rspec/openapi/components_updater.rb +98 -0
- data/lib/rspec/openapi/default_schema.rb +14 -2
- data/lib/rspec/openapi/extractors/hanami.rb +110 -0
- data/lib/rspec/openapi/extractors/rack.rb +31 -0
- data/lib/rspec/openapi/extractors/rails.rb +66 -0
- data/lib/rspec/openapi/extractors.rb +5 -0
- data/lib/rspec/openapi/hash_helper.rb +43 -0
- data/lib/rspec/openapi/key_transformer.rb +25 -0
- data/lib/rspec/openapi/minitest_hooks.rb +50 -0
- data/lib/rspec/openapi/record.rb +13 -3
- data/lib/rspec/openapi/record_builder.rb +65 -21
- data/lib/rspec/openapi/result_recorder.rb +63 -0
- data/lib/rspec/openapi/rspec_hooks.rb +21 -0
- data/lib/rspec/openapi/schema_builder.rb +166 -41
- data/lib/rspec/openapi/schema_cleaner.rb +129 -0
- data/lib/rspec/openapi/schema_file.rb +25 -3
- data/lib/rspec/openapi/schema_merger.rb +91 -21
- data/lib/rspec/openapi/schema_sorter.rb +35 -0
- data/lib/rspec/openapi/shared_hooks.rb +15 -0
- data/lib/rspec/openapi/version.rb +3 -1
- data/lib/rspec/openapi.rb +88 -2
- data/rspec-openapi.gemspec +19 -11
- data/scripts/rspec +11 -0
- data/scripts/rspec_with_simplecov +48 -0
- data/test.png +0 -0
- metadata +69 -15
- data/.travis.yml +0 -6
- data/lib/rspec/openapi/hooks.rb +0 -24
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# rspec-openapi
|
|
1
|
+
# rspec-openapi [](https://rubygems.org/gems/rspec-openapi) [](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [](https://codecov.io/gh/exoego/rspec-openapi) [](https://www.ruby-toolbox.com/projects/rspec-openapi)
|
|
2
2
|
|
|
3
|
-
Generate OpenAPI
|
|
3
|
+
Generate OpenAPI schema from RSpec request specs.
|
|
4
4
|
|
|
5
5
|
## What's this?
|
|
6
6
|
|
|
@@ -24,12 +24,12 @@ gem 'rspec-openapi', group: :test
|
|
|
24
24
|
Run rspec with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
$ OPENAPI=1 rspec
|
|
27
|
+
$ OPENAPI=1 bundle exec rspec
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
### Example
|
|
31
31
|
|
|
32
|
-
Let's say you have [a request spec](
|
|
32
|
+
Let's say you have [a request spec](https://github.com/exoego/rspec-openapi/blob/24e5c567c2e90945c7a41f19f71634ac028cc314/spec/requests/rails_spec.rb#L38) like this:
|
|
33
33
|
|
|
34
34
|
```rb
|
|
35
35
|
RSpec.describe 'Tables', type: :request do
|
|
@@ -52,10 +52,10 @@ end
|
|
|
52
52
|
If you run the spec with `OPENAPI=1`,
|
|
53
53
|
|
|
54
54
|
```
|
|
55
|
-
OPENAPI=1
|
|
55
|
+
OPENAPI=1 rspec spec/requests/tables_spec.rb
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
It will generate [`doc/openapi.yaml` file](./spec/
|
|
58
|
+
It will generate [`doc/openapi.yaml` file](./spec/rails/doc/openapi.yaml) like:
|
|
59
59
|
|
|
60
60
|
```yml
|
|
61
61
|
openapi: 3.0.3
|
|
@@ -64,16 +64,20 @@ info:
|
|
|
64
64
|
paths:
|
|
65
65
|
"/tables":
|
|
66
66
|
get:
|
|
67
|
-
summary:
|
|
67
|
+
summary: index
|
|
68
|
+
tags:
|
|
69
|
+
- Table
|
|
68
70
|
parameters:
|
|
69
71
|
- name: page
|
|
70
72
|
in: query
|
|
71
73
|
schema:
|
|
72
74
|
type: integer
|
|
75
|
+
example: 1
|
|
73
76
|
- name: per
|
|
74
77
|
in: query
|
|
75
78
|
schema:
|
|
76
79
|
type: integer
|
|
80
|
+
example: 10
|
|
77
81
|
responses:
|
|
78
82
|
'200':
|
|
79
83
|
description: returns a list of tables
|
|
@@ -93,20 +97,226 @@ paths:
|
|
|
93
97
|
|
|
94
98
|
and the schema file can be used as an input of [Swagger UI](https://github.com/swagger-api/swagger-ui) or [Redoc](https://github.com/Redocly/redoc).
|
|
95
99
|
|
|
96
|
-

|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
### Configuration
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
The following configurations are optional.
|
|
102
106
|
|
|
103
107
|
```rb
|
|
108
|
+
require 'rspec/openapi'
|
|
109
|
+
|
|
110
|
+
# Change the path to generate schema from `doc/openapi.yaml`
|
|
104
111
|
RSpec::OpenAPI.path = 'doc/schema.yaml'
|
|
112
|
+
|
|
113
|
+
# Change the output type to JSON
|
|
114
|
+
RSpec::OpenAPI.path = 'doc/schema.json'
|
|
115
|
+
|
|
116
|
+
# Or generate multiple partial schema files, given an RSpec example
|
|
117
|
+
RSpec::OpenAPI.path = -> (example) {
|
|
118
|
+
case example.file_path
|
|
119
|
+
when %r[spec/requests/api/v1/] then 'doc/openapi/v1.yaml'
|
|
120
|
+
when %r[spec/requests/api/v2/] then 'doc/openapi/v2.yaml'
|
|
121
|
+
else 'doc/openapi.yaml'
|
|
122
|
+
end
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Change the default title of the generated schema
|
|
126
|
+
RSpec::OpenAPI.title = 'OpenAPI Documentation'
|
|
127
|
+
|
|
128
|
+
# Or generate individual titles for your partial schema files, given an RSpec example
|
|
129
|
+
RSpec::OpenAPI.title = -> (example) {
|
|
130
|
+
case example.file_path
|
|
131
|
+
when %r[spec/requests/api/v1/] then 'API v1 Documentation'
|
|
132
|
+
when %r[spec/requests/api/v2/] then 'API v2 Documentation'
|
|
133
|
+
else 'OpenAPI Documentation'
|
|
134
|
+
end
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Disable generating `example`
|
|
138
|
+
RSpec::OpenAPI.enable_example = false
|
|
139
|
+
|
|
140
|
+
# Change `info.version`
|
|
141
|
+
RSpec::OpenAPI.application_version = '1.0.0'
|
|
142
|
+
|
|
143
|
+
# Set the info header details
|
|
144
|
+
RSpec::OpenAPI.info = {
|
|
145
|
+
description: 'My beautiful API',
|
|
146
|
+
license: {
|
|
147
|
+
'name': 'Apache 2.0',
|
|
148
|
+
'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Set request `headers` - generate parameters with headers for a request
|
|
153
|
+
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
|
|
154
|
+
|
|
155
|
+
# Set response `headers` - generate parameters with headers for a response
|
|
156
|
+
RSpec::OpenAPI.response_headers = %w[X-Cursor]
|
|
157
|
+
|
|
158
|
+
# Set `servers` - generate servers of a schema file
|
|
159
|
+
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
|
|
160
|
+
|
|
161
|
+
# Set `security_schemes` - generate security schemes
|
|
162
|
+
RSpec::OpenAPI.security_schemes = {
|
|
163
|
+
'MyToken' => {
|
|
164
|
+
description: 'Authenticate API requests via a JWT',
|
|
165
|
+
type: 'http',
|
|
166
|
+
scheme: 'bearer',
|
|
167
|
+
bearerFormat: 'JWT',
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Generate a comment on top of a schema file
|
|
172
|
+
RSpec::OpenAPI.comment = <<~EOS
|
|
173
|
+
This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
|
|
174
|
+
|
|
175
|
+
When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
|
|
176
|
+
update this file automatically. You can also manually edit this file.
|
|
177
|
+
EOS
|
|
178
|
+
|
|
179
|
+
# Generate a custom description, given an RSpec example
|
|
180
|
+
RSpec::OpenAPI.description_builder = -> (example) { example.description }
|
|
181
|
+
|
|
182
|
+
# Generate a custom summary, given an RSpec example
|
|
183
|
+
# This example uses the summary from the example_group.
|
|
184
|
+
RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_group, :openapi, :summary) }
|
|
185
|
+
|
|
186
|
+
# Generate a custom tags, given an RSpec example
|
|
187
|
+
# This example uses the tags from the parent_example_group
|
|
188
|
+
RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
|
|
189
|
+
|
|
190
|
+
# Change the example type(s) that will generate schema
|
|
191
|
+
RSpec::OpenAPI.example_types = %i[request]
|
|
192
|
+
|
|
193
|
+
# Configure which path params to ignore
|
|
194
|
+
# :controller and :action always exist. :format is added when routes is configured as such.
|
|
195
|
+
RSpec::OpenAPI.ignored_path_params = %i[controller action format]
|
|
196
|
+
|
|
197
|
+
# Configure which paths to ignore.
|
|
198
|
+
# You can exclude some specs via `openapi: false`.
|
|
199
|
+
# But, in a complex API usage scenario, you may need to include spec itself, but exclude some private paths.
|
|
200
|
+
# In that case, you can specify the paths to ignore.
|
|
201
|
+
# String or Regexp is acceptable.
|
|
202
|
+
RSpec::OpenAPI.ignored_paths = ["/admin/full/path/", Regexp.new("^/_internal/")]
|
|
203
|
+
|
|
204
|
+
# Your custom post-processing hook (like unrandomizing IDs)
|
|
205
|
+
RSpec::OpenAPI.post_process_hook = -> (path, records, spec) do
|
|
206
|
+
RSpec::OpenAPI::HashHelper.matched_paths(spec, 'paths.*.*.responses.*.content.*.*.*.id').each do |paths|
|
|
207
|
+
spec.dig(*paths[0..-2]).merge!(id: '123')
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Can I use rspec-openapi with `$ref` to minimize duplication of schema?
|
|
213
|
+
|
|
214
|
+
Yes, rspec-openapi v0.7.0+ supports [`$ref` mechanism](https://swagger.io/docs/specification/using-ref/) and generates
|
|
215
|
+
schemas under `#/components/schemas` with some manual steps.
|
|
216
|
+
|
|
217
|
+
1. First, generate plain OpenAPI file.
|
|
218
|
+
2. Then, manually replace the duplications with `$ref`.
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
paths:
|
|
222
|
+
"/users":
|
|
223
|
+
get:
|
|
224
|
+
responses:
|
|
225
|
+
'200':
|
|
226
|
+
content:
|
|
227
|
+
application/json:
|
|
228
|
+
schema:
|
|
229
|
+
type: array
|
|
230
|
+
items:
|
|
231
|
+
$ref: "#/components/schemas/User"
|
|
232
|
+
"/users/{id}":
|
|
233
|
+
get:
|
|
234
|
+
responses:
|
|
235
|
+
'200':
|
|
236
|
+
content:
|
|
237
|
+
application/json:
|
|
238
|
+
schema:
|
|
239
|
+
$ref: "#/components/schemas/User"
|
|
240
|
+
# Note) #/components/schemas is not needed to be defined.
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
paths:
|
|
247
|
+
"/users":
|
|
248
|
+
get:
|
|
249
|
+
responses:
|
|
250
|
+
'200':
|
|
251
|
+
content:
|
|
252
|
+
application/json:
|
|
253
|
+
schema:
|
|
254
|
+
type: array
|
|
255
|
+
items:
|
|
256
|
+
$ref: "#/components/schemas/User"
|
|
257
|
+
"/users/{id}":
|
|
258
|
+
get:
|
|
259
|
+
responses:
|
|
260
|
+
'200':
|
|
261
|
+
content:
|
|
262
|
+
application/json:
|
|
263
|
+
schema:
|
|
264
|
+
$ref: "#/components/schemas/User"
|
|
265
|
+
components:
|
|
266
|
+
schemas:
|
|
267
|
+
User:
|
|
268
|
+
type: object
|
|
269
|
+
properties:
|
|
270
|
+
id:
|
|
271
|
+
type: string
|
|
272
|
+
name:
|
|
273
|
+
type: string
|
|
274
|
+
role:
|
|
275
|
+
type: array
|
|
276
|
+
items:
|
|
277
|
+
type: string
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
rspec-openapi also supports `$ref` in `properties` of schemas. Example)
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
paths:
|
|
284
|
+
"/locations":
|
|
285
|
+
get:
|
|
286
|
+
responses:
|
|
287
|
+
'200':
|
|
288
|
+
content:
|
|
289
|
+
application/json:
|
|
290
|
+
schema:
|
|
291
|
+
type: array
|
|
292
|
+
items:
|
|
293
|
+
$ref: "#/components/schemas/Location"
|
|
294
|
+
components:
|
|
295
|
+
schemas:
|
|
296
|
+
Location:
|
|
297
|
+
type: object
|
|
298
|
+
properties:
|
|
299
|
+
id:
|
|
300
|
+
type: string
|
|
301
|
+
name:
|
|
302
|
+
type: string
|
|
303
|
+
Coordinate:
|
|
304
|
+
"$ref": "#/components/schemas/Coordinate"
|
|
305
|
+
Coordinate:
|
|
306
|
+
type: object
|
|
307
|
+
properties:
|
|
308
|
+
lat:
|
|
309
|
+
type: string
|
|
310
|
+
lon:
|
|
311
|
+
type: string
|
|
105
312
|
```
|
|
106
313
|
|
|
314
|
+
Note that automatic `schemas` update feature is still new and may not work in complex scenario.
|
|
315
|
+
If you find a room for improvement, open an issue.
|
|
316
|
+
|
|
107
317
|
### How can I add information which can't be generated from RSpec?
|
|
108
318
|
|
|
109
|
-
rspec-openapi tries to
|
|
319
|
+
rspec-openapi tries to preserve manual modifications as much as possible when generating specs.
|
|
110
320
|
You can directly edit `doc/openapi.yaml` as you like without spoiling the automatic generation capability.
|
|
111
321
|
|
|
112
322
|
### Can I exclude specific specs from OpenAPI generation?
|
|
@@ -127,28 +337,57 @@ RSpec.describe '/resources', type: :request do
|
|
|
127
337
|
end
|
|
128
338
|
```
|
|
129
339
|
|
|
130
|
-
##
|
|
340
|
+
## Customizations
|
|
131
341
|
|
|
132
|
-
|
|
342
|
+
Some examples' attributes can be overwritten via RSpec metadata options. Example:
|
|
133
343
|
|
|
134
|
-
|
|
344
|
+
```rb
|
|
345
|
+
describe 'GET /api/v1/posts', openapi: {
|
|
346
|
+
summary: 'list all posts',
|
|
347
|
+
description: 'list all posts ordered by pub_date',
|
|
348
|
+
tags: %w[v1 posts],
|
|
349
|
+
required_request_params: %w[limit],
|
|
350
|
+
security: [{"MyToken" => []}],
|
|
351
|
+
} do
|
|
352
|
+
# ...
|
|
353
|
+
end
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
|
|
357
|
+
|
|
358
|
+
## Experimental minitest support
|
|
359
|
+
|
|
360
|
+
Even if you are not using `rspec` this gem might help you with its experimental support for `minitest`.
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
|
|
364
|
+
```rb
|
|
365
|
+
class TablesTest < ActionDispatch::IntegrationTest
|
|
366
|
+
openapi!
|
|
135
367
|
|
|
136
|
-
|
|
368
|
+
test "GET /index returns a list of tables" do
|
|
369
|
+
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
|
|
370
|
+
assert_response :success
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
test "GET /index does not return tables if unauthorized" do
|
|
374
|
+
get '/tables'
|
|
375
|
+
assert_response :unauthorized
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# ...
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
It should work with both classes inheriting from `ActionDispatch::IntegrationTest` and with classes using `Rack::Test` directly, as long as you call `openapi!` in your test class.
|
|
137
383
|
|
|
138
|
-
|
|
139
|
-
* This only works for RSpec request specs
|
|
140
|
-
* Only Rails is supported for looking up a request route
|
|
384
|
+
Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per test case metadata is not supported. A custom `description_builder` will not work either.
|
|
141
385
|
|
|
142
|
-
|
|
386
|
+
Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
|
|
143
387
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
* Intelligent merges
|
|
148
|
-
* To maintain both automated changes and manual edits, the schema merge needs to be intelligent.
|
|
149
|
-
* We'll just deep-reverse-merge schema for now, but if there's a $ref for example, modifications
|
|
150
|
-
there should be rerouted to the referenced object.
|
|
151
|
-
* A type could be an array of all possible types when merged.
|
|
388
|
+
```bash
|
|
389
|
+
$ OPENAPI=1 bundle exec rails t
|
|
390
|
+
```
|
|
152
391
|
|
|
153
392
|
## Links
|
|
154
393
|
|
|
@@ -160,9 +399,9 @@ Existing RSpec plugins which have OpenAPI integration:
|
|
|
160
399
|
|
|
161
400
|
## Acknowledgements
|
|
162
401
|
|
|
163
|
-
|
|
402
|
+
* Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
|
|
403
|
+
* Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.
|
|
164
404
|
|
|
165
|
-
* [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
|
|
166
405
|
|
|
167
406
|
## License
|
|
168
407
|
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
require "rspec/core/rake_task"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
7
|
+
t.pattern = 'spec/rspec/openapi/**/*_spec.rb'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
task default: :spec
|
data/bin/console
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'rspec/openapi'
|
|
5
6
|
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
|
@@ -10,5 +11,5 @@ require "rspec/openapi"
|
|
|
10
11
|
# require "pry"
|
|
11
12
|
# Pry.start
|
|
12
13
|
|
|
13
|
-
require
|
|
14
|
+
require 'irb'
|
|
14
15
|
IRB.start(__FILE__)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hash_helper'
|
|
4
|
+
|
|
5
|
+
class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
6
|
+
# @param [Hash] base
|
|
7
|
+
# @param [Hash] fresh
|
|
8
|
+
def update!(base, fresh)
|
|
9
|
+
# Top-level schema: Used as the body of request or response
|
|
10
|
+
top_level_refs = paths_to_top_level_refs(base)
|
|
11
|
+
return if top_level_refs.empty?
|
|
12
|
+
|
|
13
|
+
fresh_schemas = build_fresh_schemas(top_level_refs, base, fresh)
|
|
14
|
+
|
|
15
|
+
# Nested schema: References in Top-level schemas. May contain some top-level schema.
|
|
16
|
+
generated_schema_names = fresh_schemas.keys
|
|
17
|
+
nested_refs = find_non_top_level_nested_refs(base, generated_schema_names)
|
|
18
|
+
nested_refs.each do |paths|
|
|
19
|
+
# Slice between the parent name and the element before "$ref"
|
|
20
|
+
# ["components", "schema", "Table", "properties", "database", "$ref"]
|
|
21
|
+
# 0 1 2 ^....................^
|
|
22
|
+
# ["components", "schema", "Table", "properties", "columns", "items", "$ref"]
|
|
23
|
+
# 0 1 2 ^...............................^
|
|
24
|
+
# ["components", "schema", "Table", "properties", "owner", "properties", "company", "$ref"]
|
|
25
|
+
# 0 1 2 ^...........................................^
|
|
26
|
+
needle = paths.reject { |path| path.is_a?(Integer) || path == :oneOf }
|
|
27
|
+
needle = needle.slice(2, needle.size - 3)
|
|
28
|
+
nested_schema = fresh_schemas.dig(*needle)
|
|
29
|
+
|
|
30
|
+
# Skip if the property using $ref is not found in the parent schema. The property may be removed.
|
|
31
|
+
next if nested_schema.nil?
|
|
32
|
+
|
|
33
|
+
schema_name = base.dig(*paths)&.gsub('#/components/schemas/', '')&.to_sym
|
|
34
|
+
fresh_schemas[schema_name] ||= {}
|
|
35
|
+
RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
RSpec::OpenAPI::SchemaMerger.merge!(base, { components: { schemas: fresh_schemas } })
|
|
39
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { components: { schemas: fresh_schemas } })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_fresh_schemas(references, base, fresh)
|
|
45
|
+
references.inject({}) do |acc, paths|
|
|
46
|
+
ref_link = dig_schema(base, paths)[:$ref]
|
|
47
|
+
schema_name = ref_link.to_s.gsub('#/components/schemas/', '')
|
|
48
|
+
schema_body = dig_schema(fresh, paths.reject { |path| path.is_a?(Integer) })
|
|
49
|
+
|
|
50
|
+
RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dig_schema(obj, paths)
|
|
55
|
+
# Response code can be an integer
|
|
56
|
+
paths = paths.map { |path| path.is_a?(Integer) ? path : path.to_sym }
|
|
57
|
+
item_schema = obj.dig(*paths, :schema, :items)
|
|
58
|
+
object_schema = obj.dig(*paths, :schema)
|
|
59
|
+
one_of_schema = obj.dig(*paths.take(paths.size - 1), :schema, :oneOf, paths.last)
|
|
60
|
+
|
|
61
|
+
item_schema || object_schema || one_of_schema
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def paths_to_top_level_refs(base)
|
|
65
|
+
request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
|
|
66
|
+
responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
|
|
67
|
+
(request_bodies + responses).flat_map do |paths|
|
|
68
|
+
object_paths = find_object_refs(base, paths)
|
|
69
|
+
one_of_paths = find_one_of_refs(base, paths)
|
|
70
|
+
|
|
71
|
+
object_paths || one_of_paths || []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find_non_top_level_nested_refs(base, generated_names)
|
|
76
|
+
nested_refs = [
|
|
77
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
|
|
78
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
|
|
79
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'oneOf.*.$ref'),
|
|
80
|
+
]
|
|
81
|
+
# Reject already-generated schemas to reduce unnecessary loop
|
|
82
|
+
nested_refs.reject do |paths|
|
|
83
|
+
ref_link = base.dig(*paths)
|
|
84
|
+
schema_name = ref_link.gsub('#/components/schemas/', '')
|
|
85
|
+
generated_names.include?(schema_name)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def find_one_of_refs(base, paths)
|
|
90
|
+
dig_schema(base, paths)&.dig(:oneOf)&.map&.with_index do |schema, index|
|
|
91
|
+
paths + [index] if schema&.dig(:$ref)&.start_with?('#/components/schemas/')
|
|
92
|
+
end&.compact
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def find_object_refs(base, paths)
|
|
96
|
+
[paths] if dig_schema(base, paths)&.dig(:$ref)&.start_with?('#/components/schemas/')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
class << RSpec::OpenAPI::DefaultSchema = Object.new
|
|
2
4
|
def build(title)
|
|
3
|
-
{
|
|
5
|
+
spec = {
|
|
4
6
|
openapi: '3.0.3',
|
|
5
7
|
info: {
|
|
6
8
|
title: title,
|
|
9
|
+
version: RSpec::OpenAPI.application_version,
|
|
7
10
|
},
|
|
11
|
+
servers: RSpec::OpenAPI.servers,
|
|
8
12
|
paths: {},
|
|
9
|
-
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if RSpec::OpenAPI.security_schemes.present?
|
|
16
|
+
spec[:components] = {
|
|
17
|
+
securitySchemes: RSpec::OpenAPI.security_schemes,
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
spec.freeze
|
|
10
22
|
end
|
|
11
23
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/inflector'
|
|
4
|
+
require 'hanami'
|
|
5
|
+
|
|
6
|
+
# https://github.com/hanami/router/blob/97f75b8529574bd4ff23165460e82a6587bc323c/lib/hanami/router/inspector.rb#L13
|
|
7
|
+
class Inspector
|
|
8
|
+
attr_accessor :routes, :inflector
|
|
9
|
+
|
|
10
|
+
def initialize(routes: [])
|
|
11
|
+
@routes = routes
|
|
12
|
+
@inflector = Dry::Inflector.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_route(route)
|
|
16
|
+
routes.push(route)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(verb, path)
|
|
20
|
+
route = routes.find { |r| r.http_method == verb && r.path == path }
|
|
21
|
+
|
|
22
|
+
if route.to.is_a?(Proc)
|
|
23
|
+
{
|
|
24
|
+
tags: [],
|
|
25
|
+
summary: "#{verb} #{path}",
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
data = route.to.split('.')
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
tags: [inflector.classify(data[0])],
|
|
32
|
+
summary: data[1],
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
InspectorAnalyzer = Inspector.new
|
|
39
|
+
|
|
40
|
+
# Add default parameter to load inspector before test cases run
|
|
41
|
+
module InspectorAnalyzerPrepender
|
|
42
|
+
def router(inspector: InspectorAnalyzer)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Hanami::Slice::ClassMethods.prepend(InspectorAnalyzerPrepender)
|
|
48
|
+
|
|
49
|
+
# Extractor for hanami
|
|
50
|
+
class << RSpec::OpenAPI::Extractors::Hanami = Object.new
|
|
51
|
+
# @param [ActionDispatch::Request] request
|
|
52
|
+
# @param [RSpec::Core::Example] example
|
|
53
|
+
# @return Array
|
|
54
|
+
def request_attributes(request, example)
|
|
55
|
+
route = Hanami.app.router.recognize(Rack::MockRequest.env_for(request.path, method: request.method))
|
|
56
|
+
|
|
57
|
+
return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable?
|
|
58
|
+
|
|
59
|
+
metadata = example.metadata[:openapi] || {}
|
|
60
|
+
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
|
|
61
|
+
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
|
|
62
|
+
operation_id = metadata[:operation_id]
|
|
63
|
+
required_request_params = metadata[:required_request_params] || []
|
|
64
|
+
security = metadata[:security]
|
|
65
|
+
description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
|
|
66
|
+
deprecated = metadata[:deprecated]
|
|
67
|
+
path = request.path
|
|
68
|
+
|
|
69
|
+
raw_path_params = route.params
|
|
70
|
+
|
|
71
|
+
result = InspectorAnalyzer.call(request.method, add_id(path, route))
|
|
72
|
+
|
|
73
|
+
summary ||= result[:summary]
|
|
74
|
+
tags ||= result[:tags]
|
|
75
|
+
path = add_openapi_id(path, route)
|
|
76
|
+
|
|
77
|
+
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
|
|
78
|
+
|
|
79
|
+
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param [RSpec::ExampleGroups::*] context
|
|
83
|
+
def request_response(context)
|
|
84
|
+
request = ActionDispatch::Request.new(context.last_request.env)
|
|
85
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
86
|
+
response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
|
|
87
|
+
|
|
88
|
+
[request, response]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_id(path, route)
|
|
92
|
+
return path if route.params.empty?
|
|
93
|
+
|
|
94
|
+
route.params.each_pair do |key, value|
|
|
95
|
+
path = path.sub("/#{value}", "/:#{key}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
path
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_openapi_id(path, route)
|
|
102
|
+
return path if route.params.empty?
|
|
103
|
+
|
|
104
|
+
route.params.each_pair do |key, value|
|
|
105
|
+
path = path.sub("/#{value}", "/{#{key}}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
path
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Extractor for rack
|
|
4
|
+
class << RSpec::OpenAPI::Extractors::Rack = Object.new
|
|
5
|
+
# @param [ActionDispatch::Request] request
|
|
6
|
+
# @param [RSpec::Core::Example] example
|
|
7
|
+
# @return Array
|
|
8
|
+
def request_attributes(request, example)
|
|
9
|
+
metadata = example.metadata[:openapi] || {}
|
|
10
|
+
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
|
|
11
|
+
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
|
|
12
|
+
operation_id = metadata[:operation_id]
|
|
13
|
+
required_request_params = metadata[:required_request_params] || []
|
|
14
|
+
security = metadata[:security]
|
|
15
|
+
description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
|
|
16
|
+
deprecated = metadata[:deprecated]
|
|
17
|
+
raw_path_params = request.path_parameters
|
|
18
|
+
path = request.path
|
|
19
|
+
summary ||= "#{request.method} #{path}"
|
|
20
|
+
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param [RSpec::ExampleGroups::*] context
|
|
24
|
+
def request_response(context)
|
|
25
|
+
request = ActionDispatch::Request.new(context.last_request.env)
|
|
26
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
27
|
+
response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
|
|
28
|
+
|
|
29
|
+
[request, response]
|
|
30
|
+
end
|
|
31
|
+
end
|