rspec-openapi 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +39 -0
- data/.github/workflows/rubocop.yml +35 -0
- data/.rubocop.yml +22 -0
- data/.rubocop_todo.yml +41 -0
- data/CHANGELOG.md +14 -1
- data/Gemfile +3 -0
- data/README.md +42 -3
- data/Rakefile +4 -2
- data/bin/console +4 -3
- data/lib/rspec/openapi/components_updater.rb +20 -9
- data/lib/rspec/openapi/default_schema.rb +12 -2
- data/lib/rspec/openapi/hash_helper.rb +17 -3
- data/lib/rspec/openapi/minitest_hooks.rb +46 -0
- data/lib/rspec/openapi/record.rb +4 -1
- data/lib/rspec/openapi/record_builder.rb +70 -62
- data/lib/rspec/openapi/result_recorder.rb +44 -0
- data/lib/rspec/openapi/rspec_hooks.rb +21 -0
- data/lib/rspec/openapi/schema_builder.rb +23 -14
- data/lib/rspec/openapi/schema_cleaner.rb +7 -6
- data/lib/rspec/openapi/schema_file.rb +5 -4
- data/lib/rspec/openapi/schema_merger.rb +19 -10
- data/lib/rspec/openapi/version.rb +3 -1
- data/lib/rspec/openapi.rb +21 -3
- data/rspec-openapi.gemspec +10 -7
- metadata +22 -14
- data/.github/FUNDING.yml +0 -1
- data/lib/rspec/openapi/hooks.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f835fe8be895164639ec2bfdbb6414b922734cc9598b30b100ac7da15d31b98f
|
4
|
+
data.tar.gz: 60ba1234561313d8e9fbe5c1f5667e6c759d477ad124867045e3e2e516174ece
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44c56d011d95c1350eef4ed83011ff00c749cf9e83ec3cb44f605a6e3ee50c05173117d618d3472ea44b20aeef8981c17ae35f8b91220ff8891382447c1a4679
|
7
|
+
data.tar.gz: c264b04decebbcd4389633eed77cd98a0241980fa283c320e99c5bdaf5a5b5be36e242367da23abcefbf0f31802dd27f1d123324005c8234657d11328e9e1e60
|
@@ -0,0 +1,39 @@
|
|
1
|
+
name: "CodeQL"
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ "master" ]
|
6
|
+
pull_request:
|
7
|
+
# The branches below must be a subset of the branches above
|
8
|
+
branches: [ "master" ]
|
9
|
+
schedule:
|
10
|
+
- cron: '20 22 * * 2'
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
analyze:
|
14
|
+
name: Analyze
|
15
|
+
runs-on: ubuntu-latest
|
16
|
+
permissions:
|
17
|
+
actions: read
|
18
|
+
contents: read
|
19
|
+
security-events: write
|
20
|
+
|
21
|
+
strategy:
|
22
|
+
fail-fast: false
|
23
|
+
matrix:
|
24
|
+
language: [ 'ruby' ]
|
25
|
+
|
26
|
+
steps:
|
27
|
+
- name: Checkout repository
|
28
|
+
uses: actions/checkout@v3
|
29
|
+
|
30
|
+
- name: Initialize CodeQL
|
31
|
+
uses: github/codeql-action/init@v2
|
32
|
+
with:
|
33
|
+
languages: ${{ matrix.language }}
|
34
|
+
|
35
|
+
- name: Autobuild
|
36
|
+
uses: github/codeql-action/autobuild@v2
|
37
|
+
|
38
|
+
- name: Perform CodeQL Analysis
|
39
|
+
uses: github/codeql-action/analyze@v2
|
@@ -0,0 +1,35 @@
|
|
1
|
+
name: "Rubocop"
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ "master" ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ "master" ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
rubocop:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
fail-fast: false
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- name: Checkout repository
|
17
|
+
uses: actions/checkout@v3
|
18
|
+
|
19
|
+
- name: Set up Ruby
|
20
|
+
uses: ruby/setup-ruby@v1
|
21
|
+
with:
|
22
|
+
ruby-version: 2.6
|
23
|
+
bundler-cache: true
|
24
|
+
|
25
|
+
- name: Rubocop run
|
26
|
+
run: |
|
27
|
+
bash -c "
|
28
|
+
bundle exec rubocop --require code_scanning --format CodeScanning::SarifFormatter -o rubocop.sarif
|
29
|
+
[[ $? -ne 2 ]]
|
30
|
+
"
|
31
|
+
|
32
|
+
- name: Upload Sarif output
|
33
|
+
uses: github/codeql-action/upload-sarif@v2
|
34
|
+
with:
|
35
|
+
sarif_file: rubocop.sarif
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
NewCops: enable
|
5
|
+
TargetRubyVersion: 2.5
|
6
|
+
Exclude:
|
7
|
+
- 'spec/rails/**/*'
|
8
|
+
- 'vendor/**/*'
|
9
|
+
|
10
|
+
Style/TrailingCommaInHashLiteral:
|
11
|
+
EnforcedStyleForMultiline: consistent_comma
|
12
|
+
Style/TrailingCommaInArguments:
|
13
|
+
EnforcedStyleForMultiline: consistent_comma
|
14
|
+
Style/TrailingCommaInArrayLiteral:
|
15
|
+
EnforcedStyleForMultiline: consistent_comma
|
16
|
+
Style/ClassAndModuleChildren:
|
17
|
+
EnforcedStyle: compact
|
18
|
+
Exclude:
|
19
|
+
- 'lib/rspec/openapi/version.rb'
|
20
|
+
Metrics/BlockLength:
|
21
|
+
Exclude:
|
22
|
+
- 'spec/**/*'
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2023-04-14 06:22:29 UTC using RuboCop version 1.49.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 10
|
10
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Max: 31
|
13
|
+
|
14
|
+
# Offense count: 4
|
15
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
16
|
+
Metrics/CyclomaticComplexity:
|
17
|
+
Max: 10
|
18
|
+
|
19
|
+
# Offense count: 13
|
20
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
21
|
+
Metrics/MethodLength:
|
22
|
+
Max: 30
|
23
|
+
|
24
|
+
# Offense count: 1
|
25
|
+
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
26
|
+
# SupportedStyles: snake_case, normalcase, non_integer
|
27
|
+
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
28
|
+
Naming/VariableNumber:
|
29
|
+
Exclude:
|
30
|
+
- 'spec/integration_tests/roda_test.rb'
|
31
|
+
|
32
|
+
# Offense count: 5
|
33
|
+
# Configuration parameters: AllowedConstants.
|
34
|
+
Style/Documentation:
|
35
|
+
Exclude:
|
36
|
+
- 'spec/**/*'
|
37
|
+
- 'test/**/*'
|
38
|
+
- 'lib/rspec/openapi.rb'
|
39
|
+
- 'lib/rspec/openapi/minitest_hooks.rb'
|
40
|
+
- 'lib/rspec/openapi/result_recorder.rb'
|
41
|
+
- 'lib/rspec/openapi/schema_file.rb'
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
|
+
## v0.8.0
|
2
|
+
- Set `required` in request body and response body
|
3
|
+
[#95](https://github.com/exoego/rspec-openapi/pull/95), [#98](https://github.com/exoego/rspec-openapi/pull/98)
|
4
|
+
- Generate OpenAPI with minitest instead of RSpec
|
5
|
+
[#90](https://github.com/exoego/rspec-openapi/pull/90)
|
6
|
+
- Generate security schemas via RSpec::OpenAPI.security_schemes
|
7
|
+
[#84](https://github.com/exoego/rspec-openapi/pull/84)
|
8
|
+
- Bunch of refactorings
|
9
|
+
|
10
|
+
## v0.7.2
|
11
|
+
- $ref enhancements: Support $ref in arbitrary depth
|
12
|
+
[#82](https://github.com/k0kubun/rspec-openapi/pull/82)
|
13
|
+
|
1
14
|
## v0.7.1
|
2
|
-
- $ref enhancements:
|
15
|
+
- $ref enhancements: Auto-generate components referenced in "items"
|
3
16
|
[#80](https://github.com/k0kubun/rspec-openapi/pull/80)
|
4
17
|
- Administration
|
5
18
|
- Setup RuboCop
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -146,6 +146,16 @@ RSpec::OpenAPI.response_headers = %w[X-Cursor]
|
|
146
146
|
# Set `servers` - generate servers of a schema file
|
147
147
|
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
|
148
148
|
|
149
|
+
# Set `security_schemes` - generate security schemes
|
150
|
+
RSpec::OpenAPI.security_schemes = {
|
151
|
+
'MyToken' => {
|
152
|
+
description: 'Authenticate API requests via a JWT',
|
153
|
+
type: 'http',
|
154
|
+
scheme: 'bearer',
|
155
|
+
bearerFormat: 'JWT',
|
156
|
+
},
|
157
|
+
}
|
158
|
+
|
149
159
|
# Generate a comment on top of a schema file
|
150
160
|
RSpec::OpenAPI.comment = <<~EOS
|
151
161
|
This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
|
@@ -189,7 +199,7 @@ paths:
|
|
189
199
|
application/json:
|
190
200
|
schema:
|
191
201
|
$ref: "#/components/schemas/User"
|
192
|
-
# Note) #/components/schamas is not needed to be defined.
|
202
|
+
# Note) #/components/schamas is not needed to be defined.
|
193
203
|
```
|
194
204
|
|
195
205
|
3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.
|
@@ -298,6 +308,7 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example
|
|
298
308
|
summary: 'list all posts',
|
299
309
|
description: 'list all posts ordered by pub_date',
|
300
310
|
tags: %w[v1 posts],
|
311
|
+
security: [{"MyToken" => []}],
|
301
312
|
} do
|
302
313
|
# ...
|
303
314
|
end
|
@@ -305,6 +316,34 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example
|
|
305
316
|
|
306
317
|
**NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
|
307
318
|
|
319
|
+
## Experimental minitest support
|
320
|
+
|
321
|
+
Even if you are not using `rspec` this gem might help you with its experimental support for `minitest`.
|
322
|
+
|
323
|
+
Example:
|
324
|
+
|
325
|
+
```rb
|
326
|
+
class TablesTest < ActionDispatch::IntegrationTest
|
327
|
+
openapi!
|
328
|
+
|
329
|
+
test "GET /index returns a list of tables" do
|
330
|
+
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
|
331
|
+
assert_response :success
|
332
|
+
end
|
333
|
+
|
334
|
+
test "GET /index does not return tables if unauthorized" do
|
335
|
+
get '/tables'
|
336
|
+
assert_response :unauthorized
|
337
|
+
end
|
338
|
+
|
339
|
+
# ...
|
340
|
+
end
|
341
|
+
```
|
342
|
+
|
343
|
+
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.
|
344
|
+
|
345
|
+
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.
|
346
|
+
|
308
347
|
## Links
|
309
348
|
|
310
349
|
Existing RSpec plugins which have OpenAPI integration:
|
@@ -315,9 +354,9 @@ Existing RSpec plugins which have OpenAPI integration:
|
|
315
354
|
|
316
355
|
## Acknowledgements
|
317
356
|
|
318
|
-
|
357
|
+
* Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
|
358
|
+
* Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.
|
319
359
|
|
320
|
-
* [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
|
321
360
|
|
322
361
|
## License
|
323
362
|
|
data/Rakefile
CHANGED
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__)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'hash_helper'
|
2
4
|
|
3
5
|
class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
@@ -7,15 +9,22 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
7
9
|
# Top-level schema: Used as the body of request or response
|
8
10
|
top_level_refs = paths_to_top_level_refs(base)
|
9
11
|
return if top_level_refs.empty?
|
12
|
+
|
10
13
|
fresh_schemas = build_fresh_schemas(top_level_refs, base, fresh)
|
11
14
|
|
12
15
|
# Nested schema: References in Top-level schemas. May contain some top-level schema.
|
13
16
|
generated_schema_names = fresh_schemas.keys
|
14
17
|
nested_refs = find_non_top_level_nested_refs(base, generated_schema_names)
|
15
18
|
nested_refs.each do |paths|
|
16
|
-
|
17
|
-
|
18
|
-
|
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.slice(2, paths.size - 3)
|
27
|
+
nested_schema = fresh_schemas.dig(*needle)
|
19
28
|
|
20
29
|
# Skip if the property using $ref is not found in the parent schema. The property may be removed.
|
21
30
|
next if nested_schema.nil?
|
@@ -25,7 +34,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
25
34
|
RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
|
26
35
|
end
|
27
36
|
|
28
|
-
RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas }})
|
37
|
+
RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas } })
|
29
38
|
RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { 'components' => { 'schemas' => fresh_schemas } })
|
30
39
|
end
|
31
40
|
|
@@ -33,7 +42,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
33
42
|
|
34
43
|
def build_fresh_schemas(references, base, fresh)
|
35
44
|
references.inject({}) do |acc, paths|
|
36
|
-
ref_link = dig_schema(base, paths)
|
45
|
+
ref_link = dig_schema(base, paths)['$ref']
|
37
46
|
schema_name = ref_link.gsub('#/components/schemas/', '')
|
38
47
|
schema_body = dig_schema(fresh, paths)
|
39
48
|
RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
|
@@ -45,16 +54,18 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
45
54
|
end
|
46
55
|
|
47
56
|
def paths_to_top_level_refs(base)
|
48
|
-
request_bodies = RSpec::OpenAPI::HashHelper
|
49
|
-
responses = RSpec::OpenAPI::HashHelper
|
57
|
+
request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
|
58
|
+
responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
|
50
59
|
(request_bodies + responses).select do |paths|
|
51
60
|
dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
|
52
61
|
end
|
53
62
|
end
|
54
63
|
|
55
64
|
def find_non_top_level_nested_refs(base, generated_names)
|
56
|
-
nested_refs =
|
57
|
-
|
65
|
+
nested_refs = [
|
66
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
|
67
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
|
68
|
+
]
|
58
69
|
# Reject already-generated schemas to reduce unnecessary loop
|
59
70
|
nested_refs.reject do |paths|
|
60
71
|
ref_link = base.dig(*paths)
|
@@ -1,6 +1,8 @@
|
|
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,
|
@@ -8,6 +10,14 @@ class << RSpec::OpenAPI::DefaultSchema = Object.new
|
|
8
10
|
},
|
9
11
|
servers: RSpec::OpenAPI.servers,
|
10
12
|
paths: {},
|
11
|
-
}
|
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
|
12
22
|
end
|
13
23
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class << RSpec::OpenAPI::HashHelper = Object.new
|
2
4
|
def paths_to_all_fields(obj)
|
3
5
|
case obj
|
@@ -13,11 +15,23 @@ class << RSpec::OpenAPI::HashHelper = Object.new
|
|
13
15
|
|
14
16
|
def matched_paths(obj, selector)
|
15
17
|
selector_parts = selector.split('.').map(&:to_s)
|
16
|
-
|
18
|
+
paths_to_all_fields(obj).select do |key_parts|
|
17
19
|
key_parts.size == selector_parts.size && key_parts.zip(selector_parts).all? do |kp, sp|
|
18
|
-
kp == sp || (sp == '*' && kp
|
20
|
+
kp == sp || (sp == '*' && !kp.nil?)
|
19
21
|
end
|
20
22
|
end
|
21
|
-
|
23
|
+
end
|
24
|
+
|
25
|
+
def matched_paths_deeply_nested(obj, begin_selector, end_selector)
|
26
|
+
path_depth_sizes = paths_to_all_fields(obj).map(&:size).uniq
|
27
|
+
path_depth_sizes.map do |depth|
|
28
|
+
diff = depth - begin_selector.count('.') - end_selector.count('.')
|
29
|
+
if diff >= 0
|
30
|
+
selector = "#{begin_selector}.#{'*.' * diff}#{end_selector}"
|
31
|
+
matched_paths(obj, selector)
|
32
|
+
else
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
end.flatten(1)
|
22
36
|
end
|
23
37
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest'
|
4
|
+
|
5
|
+
module RSpec::OpenAPI::Minitest
|
6
|
+
Example = Struct.new(:context, :description, :metadata, :file_path)
|
7
|
+
|
8
|
+
module TestPatch
|
9
|
+
def self.prepended(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(*args)
|
14
|
+
result = super
|
15
|
+
if ENV['OPENAPI'] && self.class.openapi?
|
16
|
+
file_path = method(name).source_location.first
|
17
|
+
human_name = name.sub(/^test_/, '').gsub(/_/, ' ')
|
18
|
+
example = Example.new(self, human_name, {}, file_path)
|
19
|
+
path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
|
20
|
+
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
|
21
|
+
RSpec::OpenAPI.path_records[path] << record if record
|
22
|
+
end
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def openapi?
|
28
|
+
@openapi
|
29
|
+
end
|
30
|
+
|
31
|
+
def openapi!
|
32
|
+
@openapi = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Minitest::Test.prepend RSpec::OpenAPI::Minitest::TestPatch
|
39
|
+
|
40
|
+
Minitest.after_run do
|
41
|
+
if ENV['OPENAPI']
|
42
|
+
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
|
43
|
+
result_recorder.record_results!
|
44
|
+
puts result_record.error_message if result_recorder.errors?
|
45
|
+
end
|
46
|
+
end
|
data/lib/rspec/openapi/record.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec::OpenAPI::Record = Struct.new(
|
2
|
-
:
|
4
|
+
:http_method, # @param [String] - "GET"
|
3
5
|
:path, # @param [String] - "/v1/status/:id"
|
4
6
|
:path_params, # @param [Hash] - {:controller=>"v1/statuses", :action=>"create", :id=>"1"}
|
5
7
|
:query_params, # @param [Hash] - {:query=>"string"}
|
@@ -9,6 +11,7 @@ RSpec::OpenAPI::Record = Struct.new(
|
|
9
11
|
:summary, # @param [String] - "v1/statuses #show"
|
10
12
|
:tags, # @param [Array] - ["Status"]
|
11
13
|
:description, # @param [String] - "returns a status"
|
14
|
+
:security, # @param [Array] - [{securityScheme1: []}]
|
12
15
|
:status, # @param [Integer] - 200
|
13
16
|
:response_body, # @param [Object] - {"status" => "ok"}
|
14
17
|
:response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'action_dispatch'
|
2
4
|
require 'rspec/openapi/record'
|
3
5
|
|
@@ -6,71 +8,90 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
|
|
6
8
|
# @param [RSpec::Core::Example] example
|
7
9
|
# @return [RSpec::OpenAPI::Record,nil]
|
8
10
|
def build(context, example:)
|
9
|
-
|
10
|
-
request = ActionDispatch::Request.new(context.last_request.env)
|
11
|
-
request.body.rewind if request.body.respond_to?(:rewind)
|
12
|
-
response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
|
13
|
-
else
|
14
|
-
request = context.request
|
15
|
-
response = context.response
|
16
|
-
end
|
11
|
+
request, response = extract_request_response(context)
|
17
12
|
return if request.nil?
|
18
13
|
|
19
|
-
|
20
|
-
if rails?
|
21
|
-
route = find_rails_route(request)
|
22
|
-
path = route.path.spec.to_s.delete_suffix('(.:format)')
|
23
|
-
summary = route.requirements[:action] || "#{request.method} #{path}"
|
24
|
-
tags = [route.requirements[:controller]&.classify].compact
|
25
|
-
else
|
26
|
-
path = request.path
|
27
|
-
summary = "#{request.method} #{request.path}"
|
28
|
-
end
|
14
|
+
path, summary, tags, raw_path_params, description, security = extract_request_attributes(request, example)
|
29
15
|
|
30
|
-
|
31
|
-
begin
|
32
|
-
response.parsed_body
|
33
|
-
rescue JSON::ParserError
|
34
|
-
nil
|
35
|
-
end
|
36
|
-
|
37
|
-
request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
|
38
|
-
header_key = header.gsub(/-/, '_').upcase
|
39
|
-
header_value = request.get_header(['HTTP', header_key].join('_')) || request.get_header(header_key)
|
40
|
-
headers_arr << [header, header_value] if header_value
|
41
|
-
end
|
42
|
-
|
43
|
-
metadata_options = example.metadata[:openapi] || {}
|
44
|
-
|
45
|
-
response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
|
46
|
-
header_key = header
|
47
|
-
header_value = response.headers[header_key]
|
48
|
-
headers_arr << [header_key, header_value] if header_value
|
49
|
-
end
|
16
|
+
request_headers, response_headers = extract_headers(request, response)
|
50
17
|
|
51
18
|
RSpec::OpenAPI::Record.new(
|
52
|
-
|
19
|
+
http_method: request.method,
|
53
20
|
path: path,
|
54
|
-
path_params: raw_path_params
|
21
|
+
path_params: raw_path_params,
|
55
22
|
query_params: request.query_parameters,
|
56
23
|
request_params: raw_request_params(request),
|
57
24
|
request_content_type: request.media_type,
|
58
25
|
request_headers: request_headers,
|
59
|
-
summary:
|
60
|
-
tags:
|
61
|
-
description:
|
26
|
+
summary: summary,
|
27
|
+
tags: tags,
|
28
|
+
description: description,
|
29
|
+
security: security,
|
62
30
|
status: response.status,
|
63
|
-
response_body:
|
31
|
+
response_body: safe_parse_body(response),
|
64
32
|
response_headers: response_headers,
|
65
33
|
response_content_type: response.media_type,
|
66
|
-
response_content_disposition: response.header[
|
34
|
+
response_content_disposition: response.header['Content-Disposition'],
|
67
35
|
).freeze
|
68
36
|
end
|
69
37
|
|
70
38
|
private
|
71
39
|
|
40
|
+
def safe_parse_body(response)
|
41
|
+
response.parsed_body
|
42
|
+
rescue JSON::ParserError
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_headers(request, response)
|
47
|
+
request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
|
48
|
+
header_key = header.gsub(/-/, '_').upcase
|
49
|
+
header_value = request.get_header(['HTTP', header_key].join('_')) || request.get_header(header_key)
|
50
|
+
headers_arr << [header, header_value] if header_value
|
51
|
+
end
|
52
|
+
response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
|
53
|
+
header_key = header
|
54
|
+
header_value = response.headers[header_key]
|
55
|
+
headers_arr << [header_key, header_value] if header_value
|
56
|
+
end
|
57
|
+
[request_headers, response_headers]
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_request_attributes(request, example)
|
61
|
+
metadata = example.metadata[:openapi] || {}
|
62
|
+
summary = metadata[:summary]
|
63
|
+
tags = metadata[:tags]
|
64
|
+
security = metadata[:security]
|
65
|
+
description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
|
66
|
+
raw_path_params = request.path_parameters
|
67
|
+
path = request.path
|
68
|
+
if rails?
|
69
|
+
route = find_rails_route(request)
|
70
|
+
path = route.path.spec.to_s.delete_suffix('(.:format)')
|
71
|
+
summary ||= route.requirements[:action]
|
72
|
+
tags ||= [route.requirements[:controller]&.classify].compact
|
73
|
+
# :controller and :action always exist. :format is added when routes is configured as such.
|
74
|
+
# TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
|
75
|
+
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - %i[controller action format]))
|
76
|
+
end
|
77
|
+
summary ||= "#{request.method} #{path}"
|
78
|
+
[path, summary, tags, raw_path_params, description, security]
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_request_response(context)
|
82
|
+
if rack_test?(context)
|
83
|
+
request = ActionDispatch::Request.new(context.last_request.env)
|
84
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
85
|
+
response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
|
86
|
+
else
|
87
|
+
request = context.request
|
88
|
+
response = context.response
|
89
|
+
end
|
90
|
+
[request, response]
|
91
|
+
end
|
92
|
+
|
72
93
|
def rails?
|
73
|
-
defined?(Rails) && Rails.application
|
94
|
+
defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
74
95
|
end
|
75
96
|
|
76
97
|
def rack_test?(context)
|
@@ -87,25 +108,12 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
|
|
87
108
|
|
88
109
|
app.routes.router.recognize(request) do |route|
|
89
110
|
if route.app.matches?(request)
|
90
|
-
if route.app.engine?
|
91
|
-
return find_rails_route(request, app: route.app.app, fix_path: false)
|
92
|
-
else
|
93
|
-
return route
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
raise "No route matched for #{request.request_method} #{request.path_info}"
|
98
|
-
end
|
111
|
+
return find_rails_route(request, app: route.app.app, fix_path: false) if route.app.engine?
|
99
112
|
|
100
|
-
|
101
|
-
def raw_path_params(request)
|
102
|
-
if rails?
|
103
|
-
request.path_parameters.reject do |key, _value|
|
104
|
-
%i[controller action format].include?(key)
|
113
|
+
return route
|
105
114
|
end
|
106
|
-
else
|
107
|
-
request.path_parameters
|
108
115
|
end
|
116
|
+
raise "No route matched for #{request.request_method} #{request.path_info}"
|
109
117
|
end
|
110
118
|
|
111
119
|
# workaround to get real request parameters
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RSpec::OpenAPI::ResultRecorder
|
4
|
+
def initialize(path_records)
|
5
|
+
@path_records = path_records
|
6
|
+
@error_records = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def record_results!
|
10
|
+
title = File.basename(Dir.pwd)
|
11
|
+
@path_records.each do |path, records|
|
12
|
+
RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
|
13
|
+
schema = RSpec::OpenAPI::DefaultSchema.build(title)
|
14
|
+
schema[:info].merge!(RSpec::OpenAPI.info)
|
15
|
+
RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
|
16
|
+
new_from_zero = {}
|
17
|
+
records.each do |record|
|
18
|
+
File.open('/tmp/records', 'a') { |f| f.puts record.to_yaml }
|
19
|
+
begin
|
20
|
+
record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
|
21
|
+
RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
|
22
|
+
RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
|
23
|
+
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
|
24
|
+
@error_records[e] = record # Avoid failing the build
|
25
|
+
end
|
26
|
+
end
|
27
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
|
28
|
+
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def errors?
|
34
|
+
@error_records.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
def error_message
|
38
|
+
<<~ERR_MSG
|
39
|
+
RSpec::OpenAPI got errors building #{@error_records.size} requests
|
40
|
+
|
41
|
+
#{@error_records.map { |e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
|
42
|
+
ERR_MSG
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
|
5
|
+
RSpec.configuration.after(:each) do |example|
|
6
|
+
if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
|
7
|
+
path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
|
8
|
+
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
|
9
|
+
RSpec::OpenAPI.path_records[path] << record if record
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec.configuration.after(:suite) do
|
14
|
+
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
|
15
|
+
result_recorder.record_results!
|
16
|
+
if result_recorder.errors?
|
17
|
+
error_message = result_recorder.error_message
|
18
|
+
colorizer = RSpec::Core::Formatters::ConsoleCodes
|
19
|
+
RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
|
20
|
+
end
|
21
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
2
4
|
# @param [RSpec::OpenAPI::Record] record
|
3
5
|
# @return [Hash]
|
@@ -22,9 +24,10 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
22
24
|
{
|
23
25
|
paths: {
|
24
26
|
normalize_path(record.path) => {
|
25
|
-
record.
|
27
|
+
record.http_method.downcase => {
|
26
28
|
summary: record.summary,
|
27
29
|
tags: record.tags,
|
30
|
+
security: record.security,
|
28
31
|
parameters: build_parameters(record),
|
29
32
|
requestBody: build_request_body(record),
|
30
33
|
responses: {
|
@@ -38,6 +41,11 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
38
41
|
|
39
42
|
private
|
40
43
|
|
44
|
+
def enrich_with_required_keys(obj)
|
45
|
+
obj[:required] = obj[:properties]&.keys
|
46
|
+
obj
|
47
|
+
end
|
48
|
+
|
41
49
|
def response_example(record, disposition:)
|
42
50
|
return nil if !example_enabled? || disposition
|
43
51
|
|
@@ -81,6 +89,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
81
89
|
end
|
82
90
|
|
83
91
|
return nil if parameters.empty?
|
92
|
+
|
84
93
|
parameters
|
85
94
|
end
|
86
95
|
|
@@ -115,8 +124,8 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
115
124
|
normalize_content_type(record.request_content_type) => {
|
116
125
|
schema: build_property(record.request_params),
|
117
126
|
example: (build_example(record.request_params) if example_enabled?),
|
118
|
-
}.compact
|
119
|
-
}
|
127
|
+
}.compact,
|
128
|
+
},
|
120
129
|
}
|
121
130
|
end
|
122
131
|
|
@@ -125,17 +134,18 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
125
134
|
|
126
135
|
case value
|
127
136
|
when Array
|
128
|
-
if value.empty?
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
137
|
+
property[:items] = if value.empty?
|
138
|
+
{} # unknown
|
139
|
+
else
|
140
|
+
build_property(value.first)
|
141
|
+
end
|
133
142
|
when Hash
|
134
143
|
property[:properties] = {}.tap do |properties|
|
135
144
|
value.each do |key, v|
|
136
145
|
properties[key] = build_property(v)
|
137
146
|
end
|
138
147
|
end
|
148
|
+
property = enrich_with_required_keys(property)
|
139
149
|
end
|
140
150
|
property
|
141
151
|
end
|
@@ -167,15 +177,14 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
167
177
|
|
168
178
|
# Convert an always-String param to an appropriate type
|
169
179
|
def try_cast(value)
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
value
|
174
|
-
end
|
180
|
+
Integer(value)
|
181
|
+
rescue TypeError, ArgumentError
|
182
|
+
value
|
175
183
|
end
|
176
184
|
|
177
185
|
def build_example(value)
|
178
186
|
return nil if value.nil?
|
187
|
+
|
179
188
|
value = value.dup
|
180
189
|
adjust_params(value)
|
181
190
|
end
|
@@ -191,7 +200,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
191
200
|
end
|
192
201
|
|
193
202
|
def normalize_path(path)
|
194
|
-
path.gsub(%r
|
203
|
+
path.gsub(%r{/:([^:/]+)}, '/{\1}')
|
195
204
|
end
|
196
205
|
|
197
206
|
def normalize_content_type(content_type)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# For Ruby 3.0+
|
2
4
|
require 'set'
|
3
5
|
|
@@ -44,15 +46,14 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
44
46
|
Marshal.dump(slice(obj, fields_for_identity))
|
45
47
|
end
|
46
48
|
|
47
|
-
RSpec::OpenAPI::HashHelper
|
49
|
+
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
48
50
|
target_array = base.dig(*paths)
|
49
51
|
spec_array = spec.dig(*paths)
|
50
|
-
unless target_array.is_a?(Array) && spec_array.is_a?(Array)
|
51
|
-
|
52
|
-
end
|
52
|
+
next unless target_array.is_a?(Array) && spec_array.is_a?(Array)
|
53
|
+
|
53
54
|
spec_identities = Set.new(spec_array.map(&marshal))
|
54
55
|
target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
|
55
|
-
target_array.sort_by! { |param| fields_for_identity.map {|f| param[f] }.join('-') }
|
56
|
+
target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
|
56
57
|
# Keep the last duplicate to produce the result stably
|
57
58
|
deduplicated = target_array.reverse.uniq { |param| slice(param, fields_for_identity) }.reverse
|
58
59
|
target_array.replace(deduplicated)
|
@@ -61,7 +62,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
61
62
|
end
|
62
63
|
|
63
64
|
def cleanup_hash!(base, spec, selector)
|
64
|
-
RSpec::OpenAPI::HashHelper
|
65
|
+
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
65
66
|
exist_in_base = !base.dig(*paths).nil?
|
66
67
|
not_in_spec = spec.dig(*paths).nil?
|
67
68
|
if exist_in_base && not_in_spec
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
require 'yaml'
|
3
5
|
require 'json'
|
@@ -21,7 +23,8 @@ class RSpec::OpenAPI::SchemaFile
|
|
21
23
|
# @return [Hash]
|
22
24
|
def read
|
23
25
|
return {} unless File.exist?(@path)
|
24
|
-
|
26
|
+
|
27
|
+
YAML.safe_load(File.read(@path)) # this can also parse JSON
|
25
28
|
end
|
26
29
|
|
27
30
|
# @param [Hash] spec
|
@@ -40,9 +43,7 @@ class RSpec::OpenAPI::SchemaFile
|
|
40
43
|
return content if RSpec::OpenAPI.comment.nil?
|
41
44
|
|
42
45
|
comment = RSpec::OpenAPI.comment.dup
|
43
|
-
unless comment.end_with?("\n")
|
44
|
-
comment << "\n"
|
45
|
-
end
|
46
|
+
comment << "\n" unless comment.end_with?("\n")
|
46
47
|
"#{comment.gsub(/^/, '# ').gsub(/^# \n/, "#\n")}#{content}"
|
47
48
|
end
|
48
49
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class << RSpec::OpenAPI::SchemaMerger = Object.new
|
2
4
|
# @param [Hash] base
|
3
5
|
# @param [Hash] spec
|
@@ -22,28 +24,35 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
22
24
|
end
|
23
25
|
|
24
26
|
# Not doing `base.replace(deep_merge(base, spec))` to preserve key orders.
|
25
|
-
# Also this needs to be aware of OpenAPI details because a Hash-like structure
|
27
|
+
# Also this needs to be aware of OpenAPI details because a Hash-like structure
|
26
28
|
# may be an array whose Hash elements have a key name.
|
27
29
|
#
|
28
30
|
# TODO: Should we probably force-merge `summary` regardless of manual modifications?
|
29
31
|
def merge_schema!(base, spec)
|
30
32
|
spec.each do |key, value|
|
31
33
|
if base[key].is_a?(Hash) && value.is_a?(Hash)
|
32
|
-
|
33
|
-
merge_schema!(base[key], value)
|
34
|
-
end
|
34
|
+
merge_schema!(base[key], value) unless base[key].key?('$ref')
|
35
35
|
elsif base[key].is_a?(Array) && value.is_a?(Array)
|
36
36
|
# parameters need to be merged as if `name` and `in` were the Hash keys.
|
37
|
-
|
38
|
-
base[key] = value | base[key]
|
39
|
-
base[key].uniq! { |param| param.slice('name', 'in') }
|
40
|
-
else
|
41
|
-
base[key] = value
|
42
|
-
end
|
37
|
+
merge_arrays(base, key, value)
|
43
38
|
else
|
44
39
|
base[key] = value
|
45
40
|
end
|
46
41
|
end
|
47
42
|
base
|
48
43
|
end
|
44
|
+
|
45
|
+
def merge_arrays(base, key, value)
|
46
|
+
case key
|
47
|
+
when 'parameters'
|
48
|
+
base[key] = value | base[key]
|
49
|
+
base[key].uniq! { |param| param.slice('name', 'in') }
|
50
|
+
when 'required'
|
51
|
+
# Preserve properties that appears in all test cases
|
52
|
+
base[key] = value & base[key]
|
53
|
+
else
|
54
|
+
# last one wins
|
55
|
+
base[key] = value
|
56
|
+
end
|
57
|
+
end
|
49
58
|
end
|
data/lib/rspec/openapi.rb
CHANGED
@@ -1,17 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rspec/openapi/version'
|
2
|
-
require 'rspec/openapi/
|
4
|
+
require 'rspec/openapi/components_updater'
|
5
|
+
require 'rspec/openapi/default_schema'
|
6
|
+
require 'rspec/openapi/record_builder'
|
7
|
+
require 'rspec/openapi/result_recorder'
|
8
|
+
require 'rspec/openapi/schema_builder'
|
9
|
+
require 'rspec/openapi/schema_file'
|
10
|
+
require 'rspec/openapi/schema_merger'
|
11
|
+
require 'rspec/openapi/schema_cleaner'
|
12
|
+
|
13
|
+
if ENV['OPENAPI']
|
14
|
+
require 'rspec/openapi/minitest_hooks'
|
15
|
+
require 'rspec/openapi/rspec_hooks'
|
16
|
+
end
|
3
17
|
|
4
18
|
module RSpec::OpenAPI
|
5
19
|
@path = 'doc/openapi.yaml'
|
6
20
|
@comment = nil
|
7
21
|
@enable_example = true
|
8
|
-
@description_builder = ->
|
22
|
+
@description_builder = ->(example) { example.description }
|
9
23
|
@info = {}
|
10
24
|
@application_version = '1.0.0'
|
11
25
|
@request_headers = []
|
12
26
|
@servers = []
|
27
|
+
@security_schemes = []
|
13
28
|
@example_types = %i[request]
|
14
29
|
@response_headers = []
|
30
|
+
@path_records = Hash.new { |h, k| h[k] = [] }
|
15
31
|
|
16
32
|
class << self
|
17
33
|
attr_accessor :path,
|
@@ -22,7 +38,9 @@ module RSpec::OpenAPI
|
|
22
38
|
:application_version,
|
23
39
|
:request_headers,
|
24
40
|
:servers,
|
41
|
+
:security_schemes,
|
25
42
|
:example_types,
|
26
|
-
:response_headers
|
43
|
+
:response_headers,
|
44
|
+
:path_records
|
27
45
|
end
|
28
46
|
end
|
data/rspec-openapi.gemspec
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'lib/rspec/openapi/version'
|
2
4
|
|
3
5
|
Gem::Specification.new do |spec|
|
4
6
|
spec.name = 'rspec-openapi'
|
5
7
|
spec.version = RSpec::OpenAPI::VERSION
|
6
|
-
spec.authors = ['Takashi Kokubun']
|
7
|
-
spec.email = ['takashikkbn@gmail.com']
|
8
|
+
spec.authors = ['Takashi Kokubun', 'TATSUNO Yasuhiro']
|
9
|
+
spec.email = ['takashikkbn@gmail.com', 'ytatsuno.jp@gmail.com']
|
8
10
|
|
9
|
-
spec.summary =
|
10
|
-
spec.description =
|
11
|
-
spec.homepage = 'https://github.com/
|
11
|
+
spec.summary = 'Generate OpenAPI schema from RSpec request specs'
|
12
|
+
spec.description = 'Generate OpenAPI from RSpec request specs'
|
13
|
+
spec.homepage = 'https://github.com/exoego/rspec-openapi'
|
12
14
|
spec.license = 'MIT'
|
13
15
|
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
14
16
|
|
@@ -16,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
16
18
|
spec.metadata['source_code_uri'] = spec.homepage
|
17
19
|
spec.metadata['changelog_uri'] = File.join(spec.homepage, 'blob/master/CHANGELOG.md')
|
18
20
|
|
19
|
-
spec.files
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
20
22
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
23
|
end
|
22
24
|
spec.bindir = 'exe'
|
@@ -24,5 +26,6 @@ Gem::Specification.new do |spec|
|
|
24
26
|
spec.require_paths = ['lib']
|
25
27
|
|
26
28
|
spec.add_dependency 'actionpack', '>= 5.2.0'
|
27
|
-
spec.add_dependency 'rspec'
|
29
|
+
spec.add_dependency 'rspec-core'
|
30
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
28
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-openapi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Takashi Kokubun
|
8
|
-
|
8
|
+
- TATSUNO Yasuhiro
|
9
|
+
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2023-04-20 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: actionpack
|
@@ -25,7 +26,7 @@ dependencies:
|
|
25
26
|
- !ruby/object:Gem::Version
|
26
27
|
version: 5.2.0
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: rspec
|
29
|
+
name: rspec-core
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
32
|
- - ">="
|
@@ -38,17 +39,21 @@ dependencies:
|
|
38
39
|
- - ">="
|
39
40
|
- !ruby/object:Gem::Version
|
40
41
|
version: '0'
|
41
|
-
description: Generate OpenAPI
|
42
|
+
description: Generate OpenAPI from RSpec request specs
|
42
43
|
email:
|
43
44
|
- takashikkbn@gmail.com
|
45
|
+
- ytatsuno.jp@gmail.com
|
44
46
|
executables: []
|
45
47
|
extensions: []
|
46
48
|
extra_rdoc_files: []
|
47
49
|
files:
|
48
|
-
- ".github/
|
50
|
+
- ".github/workflows/codeql-analysis.yml"
|
51
|
+
- ".github/workflows/rubocop.yml"
|
49
52
|
- ".github/workflows/test.yml"
|
50
53
|
- ".gitignore"
|
51
54
|
- ".rspec"
|
55
|
+
- ".rubocop.yml"
|
56
|
+
- ".rubocop_todo.yml"
|
52
57
|
- CHANGELOG.md
|
53
58
|
- Gemfile
|
54
59
|
- LICENSE.txt
|
@@ -60,9 +65,11 @@ files:
|
|
60
65
|
- lib/rspec/openapi/components_updater.rb
|
61
66
|
- lib/rspec/openapi/default_schema.rb
|
62
67
|
- lib/rspec/openapi/hash_helper.rb
|
63
|
-
- lib/rspec/openapi/
|
68
|
+
- lib/rspec/openapi/minitest_hooks.rb
|
64
69
|
- lib/rspec/openapi/record.rb
|
65
70
|
- lib/rspec/openapi/record_builder.rb
|
71
|
+
- lib/rspec/openapi/result_recorder.rb
|
72
|
+
- lib/rspec/openapi/rspec_hooks.rb
|
66
73
|
- lib/rspec/openapi/schema_builder.rb
|
67
74
|
- lib/rspec/openapi/schema_cleaner.rb
|
68
75
|
- lib/rspec/openapi/schema_file.rb
|
@@ -70,14 +77,15 @@ files:
|
|
70
77
|
- lib/rspec/openapi/version.rb
|
71
78
|
- rspec-openapi.gemspec
|
72
79
|
- test.png
|
73
|
-
homepage: https://github.com/
|
80
|
+
homepage: https://github.com/exoego/rspec-openapi
|
74
81
|
licenses:
|
75
82
|
- MIT
|
76
83
|
metadata:
|
77
|
-
homepage_uri: https://github.com/
|
78
|
-
source_code_uri: https://github.com/
|
79
|
-
changelog_uri: https://github.com/
|
80
|
-
|
84
|
+
homepage_uri: https://github.com/exoego/rspec-openapi
|
85
|
+
source_code_uri: https://github.com/exoego/rspec-openapi
|
86
|
+
changelog_uri: https://github.com/exoego/rspec-openapi/blob/master/CHANGELOG.md
|
87
|
+
rubygems_mfa_required: 'true'
|
88
|
+
post_install_message:
|
81
89
|
rdoc_options: []
|
82
90
|
require_paths:
|
83
91
|
- lib
|
@@ -92,8 +100,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
100
|
- !ruby/object:Gem::Version
|
93
101
|
version: '0'
|
94
102
|
requirements: []
|
95
|
-
rubygems_version: 3.
|
96
|
-
signing_key:
|
103
|
+
rubygems_version: 3.4.6
|
104
|
+
signing_key:
|
97
105
|
specification_version: 4
|
98
106
|
summary: Generate OpenAPI schema from RSpec request specs
|
99
107
|
test_files: []
|
data/.github/FUNDING.yml
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
github: k0kubun
|
data/lib/rspec/openapi/hooks.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'rspec'
|
2
|
-
require 'rspec/openapi/components_updater'
|
3
|
-
require 'rspec/openapi/default_schema'
|
4
|
-
require 'rspec/openapi/record_builder'
|
5
|
-
require 'rspec/openapi/schema_builder'
|
6
|
-
require 'rspec/openapi/schema_file'
|
7
|
-
require 'rspec/openapi/schema_merger'
|
8
|
-
require 'rspec/openapi/schema_cleaner'
|
9
|
-
|
10
|
-
path_records = Hash.new { |h, k| h[k] = [] }
|
11
|
-
error_records = {}
|
12
|
-
|
13
|
-
RSpec.configuration.after(:each) do |example|
|
14
|
-
if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
|
15
|
-
path = RSpec::OpenAPI.path.yield_self { |path| path.is_a?(Proc) ? path.call(example) : path }
|
16
|
-
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
|
17
|
-
path_records[path] << record if record
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
RSpec.configuration.after(:suite) do
|
22
|
-
title = File.basename(Dir.pwd)
|
23
|
-
path_records.each do |path, records|
|
24
|
-
RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
|
25
|
-
schema = RSpec::OpenAPI::DefaultSchema.build(title)
|
26
|
-
schema[:info].merge!(RSpec::OpenAPI.info)
|
27
|
-
RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
|
28
|
-
new_from_zero = {}
|
29
|
-
records.each do |record|
|
30
|
-
begin
|
31
|
-
record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
|
32
|
-
RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
|
33
|
-
RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
|
34
|
-
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
|
35
|
-
error_records[e] = record # Avoid failing the build
|
36
|
-
end
|
37
|
-
end
|
38
|
-
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
|
39
|
-
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
if error_records.any?
|
43
|
-
error_message = <<~EOS
|
44
|
-
RSpec::OpenAPI got errors building #{error_records.size} requests
|
45
|
-
|
46
|
-
#{error_records.map {|e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
|
47
|
-
EOS
|
48
|
-
colorizer = ::RSpec::Core::Formatters::ConsoleCodes
|
49
|
-
RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
|
50
|
-
end
|
51
|
-
end
|