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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/release.yaml +24 -0
  4. data/.github/workflows/codeql-analysis.yml +39 -0
  5. data/.github/workflows/rubocop.yml +35 -0
  6. data/.github/workflows/test.yml +46 -0
  7. data/.rspec +2 -1
  8. data/.rubocop.yml +25 -0
  9. data/.rubocop_todo.yml +52 -0
  10. data/.simplecov_spawn.rb +16 -0
  11. data/CHANGELOG.md +287 -0
  12. data/Gemfile +28 -2
  13. data/README.md +267 -28
  14. data/Rakefile +8 -4
  15. data/bin/console +4 -3
  16. data/lib/rspec/openapi/components_updater.rb +98 -0
  17. data/lib/rspec/openapi/default_schema.rb +14 -2
  18. data/lib/rspec/openapi/extractors/hanami.rb +110 -0
  19. data/lib/rspec/openapi/extractors/rack.rb +31 -0
  20. data/lib/rspec/openapi/extractors/rails.rb +66 -0
  21. data/lib/rspec/openapi/extractors.rb +5 -0
  22. data/lib/rspec/openapi/hash_helper.rb +43 -0
  23. data/lib/rspec/openapi/key_transformer.rb +25 -0
  24. data/lib/rspec/openapi/minitest_hooks.rb +50 -0
  25. data/lib/rspec/openapi/record.rb +13 -3
  26. data/lib/rspec/openapi/record_builder.rb +65 -21
  27. data/lib/rspec/openapi/result_recorder.rb +63 -0
  28. data/lib/rspec/openapi/rspec_hooks.rb +21 -0
  29. data/lib/rspec/openapi/schema_builder.rb +166 -41
  30. data/lib/rspec/openapi/schema_cleaner.rb +129 -0
  31. data/lib/rspec/openapi/schema_file.rb +25 -3
  32. data/lib/rspec/openapi/schema_merger.rb +91 -21
  33. data/lib/rspec/openapi/schema_sorter.rb +35 -0
  34. data/lib/rspec/openapi/shared_hooks.rb +15 -0
  35. data/lib/rspec/openapi/version.rb +3 -1
  36. data/lib/rspec/openapi.rb +88 -2
  37. data/rspec-openapi.gemspec +19 -11
  38. data/scripts/rspec +11 -0
  39. data/scripts/rspec_with_simplecov +48 -0
  40. data/test.png +0 -0
  41. metadata +69 -15
  42. data/.travis.yml +0 -6
  43. data/lib/rspec/openapi/hooks.rb +0 -24
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # rspec-openapi
1
+ # rspec-openapi [![Gem Version](https://badge.fury.io/rb/rspec-openapi.svg)](https://rubygems.org/gems/rspec-openapi) [![test](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml/badge.svg)](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/exoego/rspec-openapi/branch/master/graph/badge.svg?token=egYm6AlxkD)](https://codecov.io/gh/exoego/rspec-openapi) [![Ruby-toolbox](https://img.shields.io/badge/ruby-toolbox-a61414?cacheSeconds=31536000)](https://www.ruby-toolbox.com/projects/rspec-openapi)
2
2
 
3
- Generate OpenAPI specs from RSpec request specs.
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](./spec/requests/table_spec.rb) like this:
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 be rspec spec/requests/tables_spec.rb
55
+ OPENAPI=1 rspec spec/requests/tables_spec.rb
56
56
  ```
57
57
 
58
- It will generate [`doc/openapi.yaml` file](./spec/railsapp/doc/openapi.yaml) like:
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: tables#index
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
- ![Redoc example](./spec/railsapp/doc/screenshot.png)
100
+ ![Redoc example](./spec/apps/rails/doc/screenshot.png)
97
101
 
98
102
 
99
103
  ### Configuration
100
104
 
101
- If you want to change the path to generate a spec from `doc/openapi.yaml`, use:
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 keep manual modifications as much as possible when generating specs.
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
- ## Project status
340
+ ## Customizations
131
341
 
132
- PoC / Experimental
342
+ Some examples' attributes can be overwritten via RSpec metadata options. Example:
133
343
 
134
- This worked for some of my Rails apps, but this may raise a basic error for your app.
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
- ### Current limitations
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
- * Generating a JSON file is not supported yet
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
- ### Other missing features with notes
386
+ Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
143
387
 
144
- * Delete obsoleted endpoints
145
- * Give up, or at least make the feature optional?
146
- * Running all to detect obsoleted endpoints is sometimes not realistic anyway.
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
- This gem was heavily inspired by the following gem:
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
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
3
2
 
4
- RSpec::Core::RakeTask.new(:spec)
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
- task :default => :spec
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 "bundler/setup"
4
- require "rspec/openapi"
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 "irb"
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
- }.freeze
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