rspec-openapi 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/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
|