rspec-openapi 0.7.2 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +1 -3
- data/.rubocop.yml +22 -0
- data/.rubocop_todo.yml +41 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +3 -1
- data/README.md +42 -3
- data/Rakefile +4 -2
- data/bin/console +4 -3
- data/lib/rspec/openapi/components_updater.rb +8 -5
- data/lib/rspec/openapi/default_schema.rb +12 -2
- data/lib/rspec/openapi/hash_helper.rb +4 -3
- data/lib/rspec/openapi/minitest_hooks.rb +46 -0
- data/lib/rspec/openapi/record.rb +3 -0
- data/lib/rspec/openapi/record_builder.rb +69 -61
- data/lib/rspec/openapi/result_recorder.rb +45 -0
- data/lib/rspec/openapi/rspec_hooks.rb +21 -0
- data/lib/rspec/openapi/schema_builder.rb +22 -13
- data/lib/rspec/openapi/schema_cleaner.rb +19 -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 +20 -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: 86927b142ea56ae6b4c2c4f70c1cf8d21159b85f614369d4af15c91489e56647
|
4
|
+
data.tar.gz: 3711fffd6052537911f4bfaafdb2d22fba78a1649f71c97f16325d79ccc3d427
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a54a6a78f142fc39b7a4ad5a5afd92e2486ba320e414e0782f0eb2c386535588e6126c851d868b265807a29844706547a6757fb1d45a2eb1cda820923fa7c15
|
7
|
+
data.tar.gz: b53892f024ecb7733fb99e83ab1c299d3ae0b36465a2ca806d1234896a90fd45bf47df335bc14977004aa8c113c272daadf029b4c0c7ad46773be1ad5e2dec0e
|
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,3 +1,16 @@
|
|
1
|
+
## v0.8.1
|
2
|
+
- bugfix: Empty `required` array should not be present.
|
3
|
+
[#111](https://github.com/exoego/rspec-openapi/pull/111)
|
4
|
+
|
5
|
+
## v0.8.0
|
6
|
+
- Set `required` in request body and response body
|
7
|
+
[#95](https://github.com/exoego/rspec-openapi/pull/95), [#98](https://github.com/exoego/rspec-openapi/pull/98)
|
8
|
+
- Generate OpenAPI with minitest instead of RSpec
|
9
|
+
[#90](https://github.com/exoego/rspec-openapi/pull/90)
|
10
|
+
- Generate security schemas via RSpec::OpenAPI.security_schemes
|
11
|
+
[#84](https://github.com/exoego/rspec-openapi/pull/84)
|
12
|
+
- Bunch of refactorings
|
13
|
+
|
1
14
|
## v0.7.2
|
2
15
|
- $ref enhancements: Support $ref in arbitrary depth
|
3
16
|
[#82](https://github.com/k0kubun/rspec-openapi/pull/82)
|
data/Gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source 'https://rubygems.org'
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in rspec-openapi.gemspec
|
@@ -12,6 +14,6 @@ group :test do
|
|
12
14
|
end
|
13
15
|
|
14
16
|
group :development do
|
17
|
+
gem 'code-scanning-rubocop'
|
15
18
|
gem 'pry'
|
16
|
-
gem "code-scanning-rubocop"
|
17
19
|
end
|
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,6 +9,7 @@ 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.
|
@@ -31,7 +34,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
31
34
|
RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
|
32
35
|
end
|
33
36
|
|
34
|
-
RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas }})
|
37
|
+
RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas } })
|
35
38
|
RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { 'components' => { 'schemas' => fresh_schemas } })
|
36
39
|
end
|
37
40
|
|
@@ -39,7 +42,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
39
42
|
|
40
43
|
def build_fresh_schemas(references, base, fresh)
|
41
44
|
references.inject({}) do |acc, paths|
|
42
|
-
ref_link = dig_schema(base, paths)
|
45
|
+
ref_link = dig_schema(base, paths)['$ref']
|
43
46
|
schema_name = ref_link.gsub('#/components/schemas/', '')
|
44
47
|
schema_body = dig_schema(fresh, paths)
|
45
48
|
RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
|
@@ -51,8 +54,8 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
51
54
|
end
|
52
55
|
|
53
56
|
def paths_to_top_level_refs(base)
|
54
|
-
request_bodies = RSpec::OpenAPI::HashHelper
|
55
|
-
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')
|
56
59
|
(request_bodies + responses).select do |paths|
|
57
60
|
dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
|
58
61
|
end
|
@@ -61,7 +64,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
61
64
|
def find_non_top_level_nested_refs(base, generated_names)
|
62
65
|
nested_refs = [
|
63
66
|
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
|
64
|
-
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref')
|
67
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
|
65
68
|
]
|
66
69
|
# Reject already-generated schemas to reduce unnecessary loop
|
67
70
|
nested_refs.reject do |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,12 +15,11 @@ 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
|
-
selectors
|
22
23
|
end
|
23
24
|
|
24
25
|
def matched_paths_deeply_nested(obj, begin_selector, end_selector)
|
@@ -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,3 +1,5 @@
|
|
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"
|
@@ -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,45 @@
|
|
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
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def errors?
|
35
|
+
@error_records.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def error_message
|
39
|
+
<<~ERR_MSG
|
40
|
+
RSpec::OpenAPI got errors building #{@error_records.size} requests
|
41
|
+
|
42
|
+
#{@error_records.map { |e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
|
43
|
+
ERR_MSG
|
44
|
+
end
|
45
|
+
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]
|
@@ -25,6 +27,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
25
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
|
|
@@ -37,6 +39,18 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
37
39
|
base
|
38
40
|
end
|
39
41
|
|
42
|
+
def cleanup_empty_required_array!(base)
|
43
|
+
paths_to_objects = [
|
44
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties'),
|
45
|
+
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'properties'),
|
46
|
+
]
|
47
|
+
paths_to_objects.each do |path|
|
48
|
+
parent = base.dig(*path.take(path.length - 1))
|
49
|
+
# "required" array must not be present if empty
|
50
|
+
parent.delete('required') if parent['required'].empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
40
54
|
private
|
41
55
|
|
42
56
|
def cleanup_array!(base, spec, selector, fields_for_identity = [])
|
@@ -44,15 +58,14 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
44
58
|
Marshal.dump(slice(obj, fields_for_identity))
|
45
59
|
end
|
46
60
|
|
47
|
-
RSpec::OpenAPI::HashHelper
|
61
|
+
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
48
62
|
target_array = base.dig(*paths)
|
49
63
|
spec_array = spec.dig(*paths)
|
50
|
-
unless target_array.is_a?(Array) && spec_array.is_a?(Array)
|
51
|
-
|
52
|
-
end
|
64
|
+
next unless target_array.is_a?(Array) && spec_array.is_a?(Array)
|
65
|
+
|
53
66
|
spec_identities = Set.new(spec_array.map(&marshal))
|
54
67
|
target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
|
55
|
-
target_array.sort_by! { |param| fields_for_identity.map {|f| param[f] }.join('-') }
|
68
|
+
target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
|
56
69
|
# Keep the last duplicate to produce the result stably
|
57
70
|
deduplicated = target_array.reverse.uniq { |param| slice(param, fields_for_identity) }.reverse
|
58
71
|
target_array.replace(deduplicated)
|
@@ -61,7 +74,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
61
74
|
end
|
62
75
|
|
63
76
|
def cleanup_hash!(base, spec, selector)
|
64
|
-
RSpec::OpenAPI::HashHelper
|
77
|
+
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
65
78
|
exist_in_base = !base.dig(*paths).nil?
|
66
79
|
not_in_spec = spec.dig(*paths).nil?
|
67
80
|
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.1
|
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-29 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,19 +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/FUNDING.yml"
|
49
50
|
- ".github/workflows/codeql-analysis.yml"
|
50
51
|
- ".github/workflows/rubocop.yml"
|
51
52
|
- ".github/workflows/test.yml"
|
52
53
|
- ".gitignore"
|
53
54
|
- ".rspec"
|
55
|
+
- ".rubocop.yml"
|
56
|
+
- ".rubocop_todo.yml"
|
54
57
|
- CHANGELOG.md
|
55
58
|
- Gemfile
|
56
59
|
- LICENSE.txt
|
@@ -62,9 +65,11 @@ files:
|
|
62
65
|
- lib/rspec/openapi/components_updater.rb
|
63
66
|
- lib/rspec/openapi/default_schema.rb
|
64
67
|
- lib/rspec/openapi/hash_helper.rb
|
65
|
-
- lib/rspec/openapi/
|
68
|
+
- lib/rspec/openapi/minitest_hooks.rb
|
66
69
|
- lib/rspec/openapi/record.rb
|
67
70
|
- lib/rspec/openapi/record_builder.rb
|
71
|
+
- lib/rspec/openapi/result_recorder.rb
|
72
|
+
- lib/rspec/openapi/rspec_hooks.rb
|
68
73
|
- lib/rspec/openapi/schema_builder.rb
|
69
74
|
- lib/rspec/openapi/schema_cleaner.rb
|
70
75
|
- lib/rspec/openapi/schema_file.rb
|
@@ -72,14 +77,15 @@ files:
|
|
72
77
|
- lib/rspec/openapi/version.rb
|
73
78
|
- rspec-openapi.gemspec
|
74
79
|
- test.png
|
75
|
-
homepage: https://github.com/
|
80
|
+
homepage: https://github.com/exoego/rspec-openapi
|
76
81
|
licenses:
|
77
82
|
- MIT
|
78
83
|
metadata:
|
79
|
-
homepage_uri: https://github.com/
|
80
|
-
source_code_uri: https://github.com/
|
81
|
-
changelog_uri: https://github.com/
|
82
|
-
|
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:
|
83
89
|
rdoc_options: []
|
84
90
|
require_paths:
|
85
91
|
- lib
|
@@ -94,8 +100,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
100
|
- !ruby/object:Gem::Version
|
95
101
|
version: '0'
|
96
102
|
requirements: []
|
97
|
-
rubygems_version: 3.
|
98
|
-
signing_key:
|
103
|
+
rubygems_version: 3.4.6
|
104
|
+
signing_key:
|
99
105
|
specification_version: 4
|
100
106
|
summary: Generate OpenAPI schema from RSpec request specs
|
101
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 { |p| p.is_a?(Proc) ? p.call(example) : p }
|
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
|