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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef97d7a9ebc7c50fb3cc6d6ef7d148c375a190680fa1f2a6d6adb4b4971a4106
4
- data.tar.gz: 0ca35f6a022fa7a257818b29b5e5dd35d786f2e9374e0833966756bbf1cd51e1
3
+ metadata.gz: 86927b142ea56ae6b4c2c4f70c1cf8d21159b85f614369d4af15c91489e56647
4
+ data.tar.gz: 3711fffd6052537911f4bfaafdb2d22fba78a1649f71c97f16325d79ccc3d427
5
5
  SHA512:
6
- metadata.gz: 32ccb146dafc799631c7dc37c557d278fbe771f3f017263ff38eaf644ec69967369757f6e7aa48ff0074bff38641dd72354203a36a2f399fe80035c62c9bf85d
7
- data.tar.gz: 5932e0c01b98c0b6d662c43b8a3d26f4fa6709699f9eae0de92dc40cabdfb2b3fd8c2c65bfc65bd2a6ff75ab0edac14490f443bd182a4ca1164d039da2f9b5b2
6
+ metadata.gz: 7a54a6a78f142fc39b7a4ad5a5afd92e2486ba320e414e0782f0eb2c386535588e6126c851d868b265807a29844706547a6757fb1d45a2eb1cda820923fa7c15
7
+ data.tar.gz: b53892f024ecb7733fb99e83ab1c299d3ae0b36465a2ca806d1234896a90fd45bf47df335bc14977004aa8c113c272daadf029b4c0c7ad46773be1ad5e2dec0e
@@ -20,9 +20,7 @@ jobs:
20
20
  uses: ruby/setup-ruby@v1
21
21
  with:
22
22
  ruby-version: 2.6
23
-
24
- - name: Install dependencies
25
- run: bundle install
23
+ bundler-cache: true
26
24
 
27
25
  - name: Rubocop run
28
26
  run: |
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
- This gem was heavily inspired by the following gem:
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
@@ -1,5 +1,7 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec) do |t|
5
7
  t.pattern = 'spec/rspec/openapi/**/*_spec.rb'
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "rspec/openapi"
4
+ require 'bundler/setup'
5
+ require 'rspec/openapi'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "rspec/openapi"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -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).dig('$ref')
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::matched_paths(base, 'paths.*.*.requestBody.content.application/json')
55
- responses = RSpec::OpenAPI::HashHelper::matched_paths(base, 'paths.*.*.responses.*.content.application/json')
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
- }.freeze
13
+ }
14
+
15
+ if RSpec::OpenAPI.security_schemes.present?
16
+ spec[:components] = {
17
+ securitySchemes: RSpec::OpenAPI.security_schemes,
18
+ }
19
+ end
20
+
21
+ spec.freeze
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
- selectors = paths_to_all_fields(obj).select do |key_parts|
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 != nil)
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
@@ -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
- if rack_test?(context)
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
- # Generate `path` and `summary` in a framework-friendly manner when possible
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
- response_body =
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(request),
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: metadata_options[:summary] || summary,
60
- tags: metadata_options[:tags] || tags,
61
- description: metadata_options[:description] || RSpec::OpenAPI.description_builder.call(example),
26
+ summary: summary,
27
+ tags: tags,
28
+ description: description,
29
+ security: security,
62
30
  status: response.status,
63
- response_body: 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["Content-Disposition"],
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
- # :controller and :action always exist. :format is added when routes is configured as such.
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
- property[:items] = {} # unknown
130
- else
131
- property[:items] = build_property(value.first)
132
- end
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
- begin
171
- Integer(value)
172
- rescue TypeError, ArgumentError
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|/:([^:/]+)|, '/{\1}')
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::matched_paths(base, selector).each do |paths|
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
- next
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::matched_paths(base, selector).each do |paths|
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
- YAML.load(File.read(@path)) # this can also parse JSON
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
- if !base[key].key?("$ref")
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
- if key == 'parameters'
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
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RSpec
2
4
  module OpenAPI
3
- VERSION = '0.7.2'.freeze
5
+ VERSION = '0.8.1'
4
6
  end
5
7
  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/hooks' if ENV['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 = -> (example) { example.description }
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
@@ -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 = %q{Generate OpenAPI schema from RSpec request specs}
10
- spec.description = %q{Generate OpenAPI from RSpec request specs}
11
- spec.homepage = 'https://github.com/k0kubun/rspec-openapi'
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 = Dir.chdir(File.expand_path('..', __FILE__)) do
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.7.2
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
- autorequire:
8
+ - TATSUNO Yasuhiro
9
+ autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2022-11-09 00:00:00.000000000 Z
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 from RSpec request specs
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/hooks.rb
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/k0kubun/rspec-openapi
80
+ homepage: https://github.com/exoego/rspec-openapi
76
81
  licenses:
77
82
  - MIT
78
83
  metadata:
79
- homepage_uri: https://github.com/k0kubun/rspec-openapi
80
- source_code_uri: https://github.com/k0kubun/rspec-openapi
81
- changelog_uri: https://github.com/k0kubun/rspec-openapi/blob/master/CHANGELOG.md
82
- post_install_message:
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.3.24
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
@@ -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