rspec-openapi 0.1.0 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/release.yaml +24 -0
- data/.github/workflows/codeql-analysis.yml +39 -0
- data/.github/workflows/rubocop.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.rspec +2 -1
- data/.rubocop.yml +25 -0
- data/.rubocop_todo.yml +52 -0
- data/.simplecov_spawn.rb +16 -0
- data/CHANGELOG.md +287 -0
- data/Gemfile +28 -2
- data/README.md +267 -28
- data/Rakefile +8 -4
- data/bin/console +4 -3
- data/lib/rspec/openapi/components_updater.rb +98 -0
- data/lib/rspec/openapi/default_schema.rb +14 -2
- data/lib/rspec/openapi/extractors/hanami.rb +110 -0
- data/lib/rspec/openapi/extractors/rack.rb +31 -0
- data/lib/rspec/openapi/extractors/rails.rb +66 -0
- data/lib/rspec/openapi/extractors.rb +5 -0
- data/lib/rspec/openapi/hash_helper.rb +43 -0
- data/lib/rspec/openapi/key_transformer.rb +25 -0
- data/lib/rspec/openapi/minitest_hooks.rb +50 -0
- data/lib/rspec/openapi/record.rb +13 -3
- data/lib/rspec/openapi/record_builder.rb +65 -21
- data/lib/rspec/openapi/result_recorder.rb +63 -0
- data/lib/rspec/openapi/rspec_hooks.rb +21 -0
- data/lib/rspec/openapi/schema_builder.rb +166 -41
- data/lib/rspec/openapi/schema_cleaner.rb +129 -0
- data/lib/rspec/openapi/schema_file.rb +25 -3
- data/lib/rspec/openapi/schema_merger.rb +91 -21
- data/lib/rspec/openapi/schema_sorter.rb +35 -0
- data/lib/rspec/openapi/shared_hooks.rb +15 -0
- data/lib/rspec/openapi/version.rb +3 -1
- data/lib/rspec/openapi.rb +88 -2
- data/rspec-openapi.gemspec +19 -11
- data/scripts/rspec +11 -0
- data/scripts/rspec_with_simplecov +48 -0
- data/test.png +0 -0
- metadata +69 -15
- data/.travis.yml +0 -6
- data/lib/rspec/openapi/hooks.rb +0 -24
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Extractor for rails
|
|
4
|
+
class << RSpec::OpenAPI::Extractors::Rails = Object.new
|
|
5
|
+
# @param [ActionDispatch::Request] request
|
|
6
|
+
# @param [RSpec::Core::Example] example
|
|
7
|
+
# @return Array
|
|
8
|
+
def request_attributes(request, example)
|
|
9
|
+
# Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41
|
|
10
|
+
fixed_request = request.dup
|
|
11
|
+
fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present?
|
|
12
|
+
|
|
13
|
+
route, path = find_rails_route(fixed_request)
|
|
14
|
+
|
|
15
|
+
return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless path
|
|
16
|
+
|
|
17
|
+
raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
|
|
18
|
+
|
|
19
|
+
metadata = example.metadata[:openapi] || {}
|
|
20
|
+
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
|
|
21
|
+
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
|
|
22
|
+
operation_id = metadata[:operation_id]
|
|
23
|
+
required_request_params = metadata[:required_request_params] || []
|
|
24
|
+
security = metadata[:security]
|
|
25
|
+
description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
|
|
26
|
+
deprecated = metadata[:deprecated]
|
|
27
|
+
raw_path_params = request.path_parameters
|
|
28
|
+
|
|
29
|
+
summary ||= route.requirements[:action]
|
|
30
|
+
tags ||= [route.requirements[:controller]&.classify].compact
|
|
31
|
+
# :controller and :action always exist. :format is added when routes is configured as such.
|
|
32
|
+
# TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
|
|
33
|
+
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
|
|
34
|
+
|
|
35
|
+
summary ||= "#{request.method} #{path}"
|
|
36
|
+
|
|
37
|
+
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param [RSpec::ExampleGroups::*] context
|
|
41
|
+
def request_response(context)
|
|
42
|
+
[context.request, context.response]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param [ActionDispatch::Request] request
|
|
46
|
+
def find_rails_route(request, app: Rails.application, path_prefix: '')
|
|
47
|
+
app.routes.router.recognize(request) do |route, _parameters|
|
|
48
|
+
path = route.path.spec.to_s.delete_suffix('(.:format)')
|
|
49
|
+
|
|
50
|
+
if route.app.matches?(request)
|
|
51
|
+
if route.app.engine?
|
|
52
|
+
route, path = find_rails_route(request, app: route.app.app, path_prefix: path)
|
|
53
|
+
next if route.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Params are empty when it is Engine or Rack app.
|
|
57
|
+
# In that case, we can't handle parameters in path.
|
|
58
|
+
return [route, nil] if request.params.empty?
|
|
59
|
+
|
|
60
|
+
return [route, path_prefix + path]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class << RSpec::OpenAPI::HashHelper = Object.new
|
|
4
|
+
def paths_to_all_fields(obj)
|
|
5
|
+
case obj
|
|
6
|
+
when Hash
|
|
7
|
+
obj.each.flat_map do |k, v|
|
|
8
|
+
k = k.to_sym
|
|
9
|
+
[[k]] + paths_to_all_fields(v).map { |x| [k, *x] }
|
|
10
|
+
end
|
|
11
|
+
when Array
|
|
12
|
+
obj.flat_map.with_index do |value, i|
|
|
13
|
+
[[i]] + paths_to_all_fields(value).map { |x| [i, *x] }
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def matched_paths(obj, selector)
|
|
21
|
+
selector_parts = selector.split('.').map(&:to_sym)
|
|
22
|
+
paths_to_all_fields(obj).select do |key_parts|
|
|
23
|
+
key_parts.size == selector_parts.size && key_parts.zip(selector_parts).all? do |kp, sp|
|
|
24
|
+
kp == sp || (sp == :* && !kp.nil?)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def matched_paths_deeply_nested(obj, begin_selector, end_selector)
|
|
30
|
+
path_depth_sizes = paths_to_all_fields(obj).map(&:size).uniq
|
|
31
|
+
path_depth_sizes.map do |depth|
|
|
32
|
+
begin_selector_count = begin_selector.is_a?(Symbol) ? 0 : begin_selector.count('.')
|
|
33
|
+
end_selector_count = end_selector.is_a?(Symbol) ? 0 : end_selector.count('.')
|
|
34
|
+
diff = depth - begin_selector_count - end_selector_count
|
|
35
|
+
if diff >= 0
|
|
36
|
+
selector = "#{begin_selector}.#{'*.' * diff}#{end_selector}"
|
|
37
|
+
matched_paths(obj, selector)
|
|
38
|
+
else
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
end.flatten(1)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class << RSpec::OpenAPI::KeyTransformer = Object.new
|
|
4
|
+
def symbolize(value)
|
|
5
|
+
case value
|
|
6
|
+
when Hash
|
|
7
|
+
value.to_h { |k, v| [k.to_sym, symbolize(v)] }
|
|
8
|
+
when Array
|
|
9
|
+
value.map { |v| symbolize(v) }
|
|
10
|
+
else
|
|
11
|
+
value
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stringify(value)
|
|
16
|
+
case value
|
|
17
|
+
when Hash
|
|
18
|
+
value.to_h { |k, v| [k.to_s, stringify(v)] }
|
|
19
|
+
when Array
|
|
20
|
+
value.map { |v| stringify(v) }
|
|
21
|
+
else
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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 RunPatch
|
|
9
|
+
def run(*args)
|
|
10
|
+
result = super
|
|
11
|
+
if ENV['OPENAPI'] && self.class.openapi?
|
|
12
|
+
file_path = method(name).source_location.first
|
|
13
|
+
human_name = name.sub(/^test_/, '').gsub('_', ' ')
|
|
14
|
+
example = Example.new(self, human_name, {}, file_path)
|
|
15
|
+
path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p }
|
|
16
|
+
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor)
|
|
17
|
+
RSpec::OpenAPI.path_records[path] << record if record
|
|
18
|
+
end
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ActivateOpenApiClassMethods
|
|
24
|
+
def self.prepended(base)
|
|
25
|
+
base.extend(ClassMethods)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
def openapi?
|
|
30
|
+
@openapi
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def openapi!
|
|
34
|
+
@openapi = true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Minitest::Test.prepend RSpec::OpenAPI::Minitest::ActivateOpenApiClassMethods
|
|
41
|
+
|
|
42
|
+
if ENV['OPENAPI']
|
|
43
|
+
Minitest::Test.prepend RSpec::OpenAPI::Minitest::RunPatch
|
|
44
|
+
|
|
45
|
+
Minitest.after_run do
|
|
46
|
+
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
|
|
47
|
+
result_recorder.record_results!
|
|
48
|
+
puts result_recorder.error_message if result_recorder.errors?
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/rspec/openapi/record.rb
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
RSpec::OpenAPI::Record = Struct.new(
|
|
2
|
-
:
|
|
4
|
+
:title, # @param [String] - "API Documentation - Statuses"
|
|
5
|
+
:http_method, # @param [String] - "GET"
|
|
3
6
|
:path, # @param [String] - "/v1/status/:id"
|
|
4
7
|
:path_params, # @param [Hash] - {:controller=>"v1/statuses", :action=>"create", :id=>"1"}
|
|
5
8
|
:query_params, # @param [Hash] - {:query=>"string"}
|
|
6
9
|
:request_params, # @param [Hash] - {:request=>"body"}
|
|
10
|
+
:required_request_params, # @param [Array] - ["param1", "param2"]
|
|
7
11
|
:request_content_type, # @param [String] - "application/json"
|
|
8
|
-
:
|
|
9
|
-
:
|
|
12
|
+
:request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
|
|
13
|
+
:summary, # @param [String] - "v1/statuses #show"
|
|
14
|
+
:tags, # @param [Array] - ["Status"]
|
|
15
|
+
:operation_id, # @param [String] - "request-1234"
|
|
10
16
|
:description, # @param [String] - "returns a status"
|
|
17
|
+
:security, # @param [Array] - [{securityScheme1: []}]
|
|
18
|
+
:deprecated, # @param [Boolean] - true
|
|
11
19
|
:status, # @param [Integer] - 200
|
|
12
20
|
:response_body, # @param [Object] - {"status" => "ok"}
|
|
21
|
+
:response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
|
|
13
22
|
:response_content_type, # @param [String] - "application/json"
|
|
23
|
+
:response_content_disposition, # @param [String] - "inline"
|
|
14
24
|
keyword_init: true,
|
|
15
25
|
)
|
|
@@ -1,38 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'action_dispatch'
|
|
1
4
|
require 'rspec/openapi/record'
|
|
2
5
|
|
|
3
6
|
class << RSpec::OpenAPI::RecordBuilder = Object.new
|
|
4
7
|
# @param [RSpec::ExampleGroups::*] context
|
|
5
8
|
# @param [RSpec::Core::Example] example
|
|
6
|
-
# @return [RSpec::OpenAPI::Record]
|
|
7
|
-
def build(context, example:)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# @return [RSpec::OpenAPI::Record,nil]
|
|
10
|
+
def build(context, example:, extractor:)
|
|
11
|
+
request, response = extractor.request_response(context)
|
|
12
|
+
return if request.nil?
|
|
13
|
+
|
|
14
|
+
title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
|
|
15
|
+
path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated =
|
|
16
|
+
extractor.request_attributes(request, example)
|
|
17
|
+
|
|
18
|
+
return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
|
|
19
|
+
|
|
20
|
+
request_headers, response_headers = extract_headers(request, response)
|
|
12
21
|
|
|
13
22
|
RSpec::OpenAPI::Record.new(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
title: title,
|
|
24
|
+
http_method: request.method,
|
|
25
|
+
path: path,
|
|
26
|
+
path_params: raw_path_params,
|
|
17
27
|
query_params: request.query_parameters,
|
|
18
|
-
request_params: request
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
request_params: raw_request_params(request),
|
|
29
|
+
required_request_params: required_request_params,
|
|
30
|
+
request_content_type: request.media_type,
|
|
31
|
+
request_headers: request_headers,
|
|
32
|
+
summary: summary,
|
|
33
|
+
tags: tags,
|
|
34
|
+
operation_id: operation_id,
|
|
35
|
+
description: description,
|
|
36
|
+
security: security,
|
|
37
|
+
deprecated: deprecated,
|
|
23
38
|
status: response.status,
|
|
24
|
-
response_body: response.
|
|
25
|
-
|
|
39
|
+
response_body: safe_parse_body(response, response.media_type),
|
|
40
|
+
response_headers: response_headers,
|
|
41
|
+
response_content_type: response.media_type,
|
|
42
|
+
response_content_disposition: response.header['Content-Disposition'],
|
|
26
43
|
).freeze
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
private
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
def safe_parse_body(response, media_type)
|
|
49
|
+
# Use raw body, because Nokogiri-parsed HTML are modified (new lines injection, meta injection, and so on) :(
|
|
50
|
+
return response.body if media_type == 'text/html'
|
|
51
|
+
|
|
52
|
+
response.parsed_body
|
|
53
|
+
rescue JSON::ParserError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_headers(request, response)
|
|
58
|
+
request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
|
|
59
|
+
header_key = header.gsub('-', '_').upcase.to_sym
|
|
60
|
+
|
|
61
|
+
header_value = request.get_header(['HTTP', header_key].join('_')) ||
|
|
62
|
+
request.get_header(header_key) ||
|
|
63
|
+
request.get_header(header_key.to_s)
|
|
64
|
+
headers_arr << [header, header_value] if header_value
|
|
65
|
+
end
|
|
66
|
+
response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
|
|
67
|
+
header_key = header
|
|
68
|
+
header_value = response.headers[header_key]
|
|
69
|
+
headers_arr << [header_key, header_value] if header_value
|
|
35
70
|
end
|
|
36
|
-
|
|
71
|
+
[request_headers, response_headers]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# workaround to get real request parameters
|
|
75
|
+
# because ActionController::ParamsWrapper overwrites request_parameters
|
|
76
|
+
def raw_request_params(request)
|
|
77
|
+
original = request.delete_header('action_dispatch.request.request_parameters')
|
|
78
|
+
request.request_parameters
|
|
79
|
+
ensure
|
|
80
|
+
request.set_header('action_dispatch.request.request_parameters', original)
|
|
37
81
|
end
|
|
38
82
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
@path_records.each do |path, records|
|
|
11
|
+
# Look for a path-specific config file and run it.
|
|
12
|
+
config_file = File.join(File.dirname(path), RSpec::OpenAPI.config_filename)
|
|
13
|
+
begin
|
|
14
|
+
require config_file if File.exist?(config_file)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
puts "WARNING: Unable to load #{config_file}: #{e}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
title = records.first.title
|
|
20
|
+
RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
|
|
21
|
+
schema = RSpec::OpenAPI::DefaultSchema.build(title)
|
|
22
|
+
schema[:info].merge!(RSpec::OpenAPI.info)
|
|
23
|
+
RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
|
|
24
|
+
new_from_zero = {}
|
|
25
|
+
records.each do |record|
|
|
26
|
+
record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
|
|
27
|
+
RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
|
|
28
|
+
RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
|
|
29
|
+
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
|
|
30
|
+
@error_records[e] = record # Avoid failing the build
|
|
31
|
+
end
|
|
32
|
+
cleanup_schema!(new_from_zero, spec)
|
|
33
|
+
execute_post_process_hook(path, records, spec)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def errors?
|
|
39
|
+
@error_records.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def error_message
|
|
43
|
+
<<~ERR_MSG
|
|
44
|
+
RSpec::OpenAPI got errors building #{@error_records.size} requests
|
|
45
|
+
|
|
46
|
+
#{@error_records.map { |e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
|
|
47
|
+
ERR_MSG
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def execute_post_process_hook(path, records, spec)
|
|
53
|
+
RSpec::OpenAPI.post_process_hook.call(path, records, spec) if RSpec::OpenAPI.post_process_hook.is_a?(Proc)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cleanup_schema!(new_from_zero, spec)
|
|
57
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup_conflicting_security_parameters!(spec)
|
|
58
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
|
|
59
|
+
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
|
|
60
|
+
RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
|
|
61
|
+
RSpec::OpenAPI::SchemaSorter.deep_sort!(spec)
|
|
62
|
+
end
|
|
63
|
+
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.then { |p| p.is_a?(Proc) ? p.call(example) : p }
|
|
8
|
+
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor)
|
|
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
|