rspec-openapi 0.32.0 → 0.33.0
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/README.md +7 -0
- data/lib/rspec/openapi/components_updater.rb +6 -5
- data/lib/rspec/openapi/exchange_recorder.rb +3 -0
- data/lib/rspec/openapi/extractors/rails.rb +2 -2
- data/lib/rspec/openapi/hash_helper.rb +50 -31
- data/lib/rspec/openapi/key_transformer.rb +8 -10
- data/lib/rspec/openapi/minitest_hooks.rb +11 -1
- data/lib/rspec/openapi/parallel_records.rb +116 -0
- data/lib/rspec/openapi/result_recorder.rb +3 -2
- data/lib/rspec/openapi/schema_builder/variation_merger.rb +146 -0
- data/lib/rspec/openapi/schema_builder.rb +8 -151
- data/lib/rspec/openapi/schema_cleaner.rb +8 -22
- data/lib/rspec/openapi/schema_merger.rb +23 -3
- data/lib/rspec/openapi/version.rb +1 -1
- data/lib/rspec/openapi.rb +10 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbbe2720c2fa2a498263a4c54a017982e17bf9aa7f34f422b8ccb2737c2b3ac1
|
|
4
|
+
data.tar.gz: 5725ab9fe452d6aa465d431597ab5f47b218b302b7222a7495fb1423bbf6087b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9783bfa40b973043bb0f1c1f4fc2d2ddaccde0371a43762024a8f1a7fddeb4245635e6c478ebc70949da2ebe3b5e5fdd53dc97327735eac8ce8c101c1302a78f
|
|
7
|
+
data.tar.gz: bed996f8f40811638b55338d652ec4483a583a631e5defaf3bd67e01d8eadd64533b68340ef28491b57f96fb52bc518acad34845d5e41f26b4fe9f3e50b6c71d
|
data/README.md
CHANGED
|
@@ -986,6 +986,13 @@ Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request spec
|
|
|
986
986
|
$ OPENAPI=1 bundle exec rails t
|
|
987
987
|
```
|
|
988
988
|
|
|
989
|
+
### Parallel test execution
|
|
990
|
+
|
|
991
|
+
Minitest's parallel execution is supported, both thread-based (`parallelize_me!`) and
|
|
992
|
+
process-based (Rails' `parallelize`). With forked workers, each worker dumps its
|
|
993
|
+
records to a temporary directory on exit and the main process merges them back
|
|
994
|
+
before writing the schema. No configuration is needed.
|
|
995
|
+
|
|
989
996
|
## Links
|
|
990
997
|
|
|
991
998
|
Existing RSpec plugins which have OpenAPI integration:
|
|
@@ -32,12 +32,12 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
|
32
32
|
# Skip if the property using $ref is not found in the parent schema. The property may be removed.
|
|
33
33
|
next if nested_schema.nil?
|
|
34
34
|
|
|
35
|
-
schema_name = extract_schema_name(base.dig(*paths))
|
|
35
|
+
schema_name = extract_schema_name(base.dig(*paths)).to_sym
|
|
36
36
|
fresh_schemas[schema_name] ||= {}
|
|
37
|
-
RSpec::OpenAPI::SchemaMerger.
|
|
37
|
+
RSpec::OpenAPI::SchemaMerger.merge_normalized!(fresh_schemas[schema_name], nested_schema)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
RSpec::OpenAPI::SchemaMerger.
|
|
40
|
+
RSpec::OpenAPI::SchemaMerger.merge_normalized!(base, { components: { schemas: fresh_schemas } })
|
|
41
41
|
RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { components: { schemas: fresh_schemas } })
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -49,7 +49,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
|
49
49
|
schema_name = extract_schema_name(ref_link)
|
|
50
50
|
schema_body = dig_schema(fresh, paths.grep_v(Integer))
|
|
51
51
|
|
|
52
|
-
RSpec::OpenAPI::SchemaMerger.
|
|
52
|
+
RSpec::OpenAPI::SchemaMerger.merge_normalized!(acc, { schema_name => schema_body })
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -101,8 +101,9 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
|
|
|
101
101
|
[paths] if schema_ref?(dig_schema(base, paths)&.dig(:$ref))
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Only ever given the value at a path ending in $ref, which is the link itself.
|
|
104
105
|
def extract_schema_name(ref_link)
|
|
105
|
-
ref_link
|
|
106
|
+
ref_link.delete_prefix(SCHEMA_REF_PREFIX)
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def schema_ref?(ref_link)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Records every request/response exchange issued in an example so that
|
|
4
|
+
# the exchange matching `request_pattern` can be looked up afterwards.
|
|
3
5
|
module RSpec::OpenAPI::ExchangeRecorder
|
|
4
6
|
THREAD_KEY = :rspec_openapi_exchanges
|
|
5
7
|
VERBS = [:get, :post, :put, :patch, :delete, :head, :options].freeze
|
|
6
8
|
|
|
9
|
+
# Wraps HTTP verb methods to capture the exchange right after each request.
|
|
7
10
|
module VerbTracking
|
|
8
11
|
VERBS.each do |verb|
|
|
9
12
|
define_method(verb) do |*args, &block|
|
|
@@ -12,10 +12,10 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
|
|
|
12
12
|
|
|
13
13
|
route, path = find_rails_route(fixed_request)
|
|
14
14
|
|
|
15
|
+
# find_rails_route yields nil, [route, nil] or [route, path]; a path always
|
|
16
|
+
# comes with the route it was taken from.
|
|
15
17
|
return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless path
|
|
16
18
|
|
|
17
|
-
raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
|
|
18
|
-
|
|
19
19
|
attrs = SharedExtractor.attributes(example)
|
|
20
20
|
# :controller and :action always exist. :format is added when routes is configured as such.
|
|
21
21
|
# TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
|
|
@@ -1,43 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class << RSpec::OpenAPI::HashHelper = Object.new
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
4
|
+
# Paths in obj matching a dot separated selector, where `*` is any one key or
|
|
5
|
+
# array index. Only paths of exactly the selector's length can match, so this
|
|
6
|
+
# descends the branches that still match rather than listing every path in the
|
|
7
|
+
# document and filtering. A document built from a large suite holds tens of
|
|
8
|
+
# thousands of nodes and these selectors run dozens of times per build.
|
|
20
9
|
def matched_paths(obj, selector)
|
|
21
10
|
selector_parts = selector.split('.').map(&:to_sym)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
end
|
|
26
|
-
end
|
|
11
|
+
matches = []
|
|
12
|
+
collect_matches(obj, selector_parts, 0, [], matches)
|
|
13
|
+
matches
|
|
27
14
|
end
|
|
28
15
|
|
|
29
16
|
def matched_paths_deeply_nested(obj, begin_selector, end_selector)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
begin_depth = begin_selector.to_s.count('.')
|
|
18
|
+
end_depth = end_selector.to_s.count('.')
|
|
19
|
+
|
|
20
|
+
# Every path in obj has all of its prefixes in obj too, so the depths that
|
|
21
|
+
# occur are exactly 1 up to the deepest one.
|
|
22
|
+
(1..max_depth(obj)).flat_map do |depth|
|
|
23
|
+
gap = depth - begin_depth - end_depth
|
|
24
|
+
next [] if gap.negative?
|
|
25
|
+
|
|
26
|
+
matched_paths(obj, "#{begin_selector}.#{'*.' * gap}#{end_selector}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def collect_matches(obj, selector_parts, depth, prefix, matches)
|
|
33
|
+
part = selector_parts[depth]
|
|
34
|
+
last = depth == selector_parts.size - 1
|
|
35
|
+
|
|
36
|
+
each_child(obj) do |key, value|
|
|
37
|
+
next unless part == :* || part == key
|
|
38
|
+
|
|
39
|
+
path = [*prefix, key]
|
|
40
|
+
if last
|
|
41
|
+
matches << path
|
|
38
42
|
else
|
|
39
|
-
|
|
43
|
+
collect_matches(value, selector_parts, depth + 1, path, matches)
|
|
40
44
|
end
|
|
41
|
-
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def each_child(obj)
|
|
49
|
+
case obj
|
|
50
|
+
when Hash then obj.each { |key, value| yield key.to_sym, value }
|
|
51
|
+
when Array then obj.each_with_index { |value, index| yield index, value }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def max_depth(obj)
|
|
56
|
+
case obj
|
|
57
|
+
when Hash then obj.empty? ? 0 : 1 + obj.each_value.map { |value| max_depth(value) }.max
|
|
58
|
+
when Array then obj.empty? ? 0 : 1 + obj.map { |value| max_depth(value) }.max
|
|
59
|
+
else 0
|
|
60
|
+
end
|
|
42
61
|
end
|
|
43
62
|
end
|
|
@@ -18,18 +18,16 @@ class << RSpec::OpenAPI::KeyTransformer = Object.new
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# `examples` is a map of named examples under a media type, whose names are
|
|
22
|
+
# normalized here. Under a schema it is a JSON Schema keyword holding a plain
|
|
23
|
+
# array instead, which has no names to normalize.
|
|
21
24
|
def symbolize_examples(value)
|
|
22
|
-
|
|
23
|
-
when Hash
|
|
24
|
-
value.to_h do |k, v|
|
|
25
|
-
k = k.downcase.tr(' ', '_') unless k.is_a?(Symbol)
|
|
25
|
+
return symbolize(value) unless value.is_a?(Hash)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
else
|
|
32
|
-
value
|
|
27
|
+
value.to_h do |k, v|
|
|
28
|
+
k = k.downcase.tr(' ', '_') unless k.is_a?(Symbol)
|
|
29
|
+
|
|
30
|
+
[k.to_sym, symbolize(v)]
|
|
33
31
|
end
|
|
34
32
|
end
|
|
35
33
|
|
|
@@ -5,6 +5,10 @@ require 'minitest'
|
|
|
5
5
|
module RSpec::OpenAPI::Minitest
|
|
6
6
|
Example = Struct.new(:context, :description, :metadata, :file_path)
|
|
7
7
|
|
|
8
|
+
# Guards path_records against concurrent appends from Minitest's
|
|
9
|
+
# thread-based parallel executor (parallelize_me!).
|
|
10
|
+
RECORD_MUTEX = Mutex.new
|
|
11
|
+
|
|
8
12
|
module RunPatch
|
|
9
13
|
def run(*args)
|
|
10
14
|
result = super
|
|
@@ -14,7 +18,12 @@ module RSpec::OpenAPI::Minitest
|
|
|
14
18
|
example = Example.new(self, human_name, {}, file_path)
|
|
15
19
|
path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p }
|
|
16
20
|
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor)
|
|
17
|
-
|
|
21
|
+
if record
|
|
22
|
+
RECORD_MUTEX.synchronize do
|
|
23
|
+
RSpec::OpenAPI::ParallelRecords.schedule_dump!
|
|
24
|
+
RSpec::OpenAPI.path_records[path] << record
|
|
25
|
+
end
|
|
26
|
+
end
|
|
18
27
|
end
|
|
19
28
|
result
|
|
20
29
|
end
|
|
@@ -43,6 +52,7 @@ if ENV['OPENAPI']
|
|
|
43
52
|
Minitest::Test.prepend RSpec::OpenAPI::Minitest::RunPatch
|
|
44
53
|
|
|
45
54
|
Minitest.after_run do
|
|
55
|
+
RSpec::OpenAPI::ParallelRecords.merge!
|
|
46
56
|
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
|
|
47
57
|
result_recorder.record_results!
|
|
48
58
|
puts result_recorder.error_message if result_recorder.errors?
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
|
|
9
|
+
# Collects records across processes when the test framework forks parallel
|
|
10
|
+
# workers, like Rails' `parallelize`. Records accumulate in each worker's own
|
|
11
|
+
# memory, so without this the main process would write the schema from an empty
|
|
12
|
+
# set. Each worker dumps its records to a shared directory on exit, and the
|
|
13
|
+
# main process merges every dump back in before recording results.
|
|
14
|
+
#
|
|
15
|
+
# The dumps go through YAML.safe_load with an allowlist rather than Marshal,
|
|
16
|
+
# so reading them cannot instantiate arbitrary objects even if the directory
|
|
17
|
+
# were tampered with.
|
|
18
|
+
module RSpec::OpenAPI::ParallelRecords
|
|
19
|
+
# The pid of the process that loaded this gem. Forked workers inherit the
|
|
20
|
+
# constant, which is how they know they are not the main process.
|
|
21
|
+
MAIN_PID = Process.pid
|
|
22
|
+
|
|
23
|
+
# Unpredictable per-run token. Workers inherit it across fork, while other
|
|
24
|
+
# local users cannot guess it to pre-create or plant files in the dump
|
|
25
|
+
# directory, which lives in the world-writable system tmpdir.
|
|
26
|
+
RUN_ID = SecureRandom.hex(16)
|
|
27
|
+
|
|
28
|
+
# Marks the stand-in a multipart upload leaves in a dump. The schema
|
|
29
|
+
# builder only ever asks an upload for its class and metadata, never for
|
|
30
|
+
# its content, so the file itself does not need to survive the trip.
|
|
31
|
+
UPLOADED_FILE_MARKER = '__rspec_openapi_uploaded_file'
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def worker?
|
|
35
|
+
Process.pid != MAIN_PID
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called when a worker records an example. Registered lazily from inside
|
|
39
|
+
# the worker because an at_exit inherited from the main process has already
|
|
40
|
+
# run there by the time workers fork: minitest executes the whole suite
|
|
41
|
+
# from an at_exit hook, so handlers the gem registered at load time are
|
|
42
|
+
# popped before any fork happens.
|
|
43
|
+
def schedule_dump!
|
|
44
|
+
return if @dump_scheduled || !worker?
|
|
45
|
+
|
|
46
|
+
@dump_scheduled = true
|
|
47
|
+
at_exit { dump! }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dump!
|
|
51
|
+
return if RSpec::OpenAPI.path_records.empty?
|
|
52
|
+
|
|
53
|
+
FileUtils.mkdir_p(dump_dir, mode: 0o700)
|
|
54
|
+
encoded = RSpec::OpenAPI.path_records.transform_values do |records|
|
|
55
|
+
records.map { |record| transform_record(record) { |value| encode(value) } }
|
|
56
|
+
end
|
|
57
|
+
File.write(File.join(dump_dir, "#{Process.pid}.yaml"), YAML.dump(encoded))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def merge!
|
|
61
|
+
Dir.glob(File.join(dump_dir, '*.yaml')).sort.each do |file|
|
|
62
|
+
records_by_path = YAML.safe_load(File.read(file), permitted_classes: permitted_classes, aliases: true)
|
|
63
|
+
records_by_path.each do |path, records|
|
|
64
|
+
decoded = records.map { |record| transform_record(record) { |value| decode(value) } }
|
|
65
|
+
RSpec::OpenAPI.path_records[path].concat(decoded)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
FileUtils.rm_rf(dump_dir)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def dump_dir
|
|
75
|
+
File.join(Dir.tmpdir, "rspec-openapi-records-#{RUN_ID}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Built lazily because ActiveSupport is not loaded in every setup. Rails
|
|
79
|
+
# request objects hand back HashWithIndifferentAccess for parameters.
|
|
80
|
+
def permitted_classes
|
|
81
|
+
classes = [Symbol, RSpec::OpenAPI::Record]
|
|
82
|
+
classes << ActiveSupport::HashWithIndifferentAccess if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
83
|
+
classes
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def transform_record(record, &block)
|
|
87
|
+
RSpec::OpenAPI::Record.new(**record.to_h.transform_values(&block))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Uploads hold an open Tempfile, which no serializer can represent, so
|
|
91
|
+
# dump their metadata instead.
|
|
92
|
+
def encode(value)
|
|
93
|
+
case value
|
|
94
|
+
when Array then value.map { |item| encode(item) }
|
|
95
|
+
when Hash then value.transform_values { |item| encode(item) }
|
|
96
|
+
when defined?(ActionDispatch::Http::UploadedFile) && ActionDispatch::Http::UploadedFile
|
|
97
|
+
{ UPLOADED_FILE_MARKER => { 'filename' => value.original_filename, 'type' => value.content_type } }
|
|
98
|
+
else value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Rebuilds a real UploadedFile so the schema builder's class checks match,
|
|
103
|
+
# backed by an empty StringIO in place of the worker's Tempfile.
|
|
104
|
+
def decode(value)
|
|
105
|
+
return value unless value.is_a?(Array) || value.is_a?(Hash)
|
|
106
|
+
return value.map { |item| decode(item) } if value.is_a?(Array)
|
|
107
|
+
|
|
108
|
+
meta = value[UPLOADED_FILE_MARKER]
|
|
109
|
+
if meta && value.size == 1 && defined?(ActionDispatch::Http::UploadedFile)
|
|
110
|
+
ActionDispatch::Http::UploadedFile.new(tempfile: StringIO.new, filename: meta['filename'], type: meta['type'])
|
|
111
|
+
else
|
|
112
|
+
value.transform_values { |item| decode(item) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -51,12 +51,13 @@ class RSpec::OpenAPI::ResultRecorder
|
|
|
51
51
|
RSpec::OpenAPI::OperationConverter.normalize!(spec)
|
|
52
52
|
schema = RSpec::OpenAPI::DefaultSchema.build(title)
|
|
53
53
|
schema[:info].merge!(RSpec::OpenAPI.info)
|
|
54
|
+
# Normalize the document once here rather than on every merge below.
|
|
54
55
|
RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
|
|
55
56
|
new_from_zero = {}
|
|
56
57
|
records.each do |record|
|
|
57
58
|
record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
|
|
58
|
-
RSpec::OpenAPI::SchemaMerger.
|
|
59
|
-
RSpec::OpenAPI::SchemaMerger.
|
|
59
|
+
RSpec::OpenAPI::SchemaMerger.merge_normalized!(spec, record_schema)
|
|
60
|
+
RSpec::OpenAPI::SchemaMerger.merge_normalized!(new_from_zero, record_schema)
|
|
60
61
|
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
|
|
61
62
|
@error_records[e] = record # Avoid failing the build
|
|
62
63
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class << RSpec::OpenAPI::SchemaBuilder
|
|
4
|
+
# Merges multiple observed schema variations of the same value (array items,
|
|
5
|
+
# stream items, repeated object properties) into a single OpenAPI schema.
|
|
6
|
+
# Divergent types are combined into oneOf, and keys that only appear in some
|
|
7
|
+
# variations are marked nullable.
|
|
8
|
+
VariationMerger = Object.new
|
|
9
|
+
|
|
10
|
+
class << VariationMerger
|
|
11
|
+
def merge_variations(variations)
|
|
12
|
+
# Drop empty `{}` schemas (e.g. items of an empty array) — they carry no
|
|
13
|
+
# type info and would otherwise spuriously mark every property of their
|
|
14
|
+
# populated siblings as nullable via the missing-key nullable rule.
|
|
15
|
+
non_empty = variations.reject(&:empty?)
|
|
16
|
+
return {} if non_empty.empty?
|
|
17
|
+
return non_empty.first if non_empty.size == 1
|
|
18
|
+
|
|
19
|
+
types = non_empty.map { |v| v[:type] }.compact.uniq
|
|
20
|
+
return one_of_schema(non_empty) if types.size > 1
|
|
21
|
+
|
|
22
|
+
case types.first
|
|
23
|
+
when 'object' then merge_object_variations(non_empty)
|
|
24
|
+
when 'array' then merge_array_item_variations(non_empty)
|
|
25
|
+
else non_empty.first
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def merge_non_object_item_variations(schemas)
|
|
30
|
+
nullable_only = ->(schema) { schema.keys == [:nullable] }
|
|
31
|
+
typed = schemas.reject(&nullable_only)
|
|
32
|
+
|
|
33
|
+
merged = typed.size == 1 ? typed.first.dup : merge_multi(typed)
|
|
34
|
+
merged[:nullable] = true if schemas.any?(&nullable_only) && merged.is_a?(Hash)
|
|
35
|
+
merged
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Merge the per-key property schemas of multiple object variations.
|
|
39
|
+
# When `allow_recursive_merge` is true, objects are recursively merged via
|
|
40
|
+
# merge_variations and existing oneOf entries are flattened.
|
|
41
|
+
# When false (callsite: array-items merging), divergent property variations
|
|
42
|
+
# become oneOf without recursive descent.
|
|
43
|
+
def merge_property_variations(variations, allow_recursive_merge:)
|
|
44
|
+
property_keys(variations).each_with_object({}) do |key, merged_props|
|
|
45
|
+
all = variations.map { |v| v[:properties]&.[](key) }
|
|
46
|
+
prop_variations = all.reject { |p| p.nil? || p.keys == [:nullable] }
|
|
47
|
+
has_nullable = nullable_present?(all, recursive: allow_recursive_merge)
|
|
48
|
+
|
|
49
|
+
next if prop_variations.empty? && !has_nullable
|
|
50
|
+
|
|
51
|
+
merged_props[key] = merge_single_property(prop_variations, has_nullable,
|
|
52
|
+
variations_total: variations.size,
|
|
53
|
+
allow_recursive_merge: allow_recursive_merge,)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def merge_object_variations(variations)
|
|
60
|
+
{
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: merge_property_variations(variations, allow_recursive_merge: true),
|
|
63
|
+
required: variations.map { |v| v[:required] || [] }.reduce(:&) || [],
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Merge multiple array schemas by merging their `items` schemas.
|
|
68
|
+
def merge_array_item_variations(variations)
|
|
69
|
+
{ type: 'array', items: merge_variations(variations.map { |v| v[:items] }.compact) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def property_keys(variations)
|
|
73
|
+
variations.flat_map { |v| v[:properties]&.keys || [] }.uniq
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# `recursive` mirrors merge_variations' original rule that also treats
|
|
77
|
+
# `{ ..., nullable: true }` as a nullable signal. Array-items merging only
|
|
78
|
+
# looks at outright nil or `{ nullable: true }` markers.
|
|
79
|
+
def nullable_present?(all_props, recursive:)
|
|
80
|
+
all_props.any? do |p|
|
|
81
|
+
p.nil? || (p.is_a?(Hash) && (p.keys == [:nullable] || (recursive && p[:nullable] == true)))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def merge_single_property(prop_variations, has_nullable, variations_total:, allow_recursive_merge:)
|
|
86
|
+
return { nullable: true } if prop_variations.empty?
|
|
87
|
+
|
|
88
|
+
merged =
|
|
89
|
+
if prop_variations.size == 1
|
|
90
|
+
prop_variations.first.dup
|
|
91
|
+
else
|
|
92
|
+
merge_multi(prop_variations)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return merged unless merged.is_a?(Hash)
|
|
96
|
+
|
|
97
|
+
# In recursive mode, multi-variation merges also flag nullable when the key
|
|
98
|
+
# only appeared in some of the source variations.
|
|
99
|
+
needs_nullable =
|
|
100
|
+
if allow_recursive_merge && prop_variations.size > 1
|
|
101
|
+
has_nullable || prop_variations.size < variations_total
|
|
102
|
+
else
|
|
103
|
+
has_nullable
|
|
104
|
+
end
|
|
105
|
+
merged[:nullable] = true if needs_nullable
|
|
106
|
+
merged
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Combine multiple variations of the same property: flatten existing oneOf
|
|
110
|
+
# entries, recurse into objects and arrays, and combine divergent types into
|
|
111
|
+
# oneOf. Scalar variations of a single type collapse to the first schema.
|
|
112
|
+
def merge_multi(prop_variations)
|
|
113
|
+
return { oneOf: flatten_one_of(prop_variations) } if prop_variations.any? { |p| p.key?(:oneOf) }
|
|
114
|
+
|
|
115
|
+
prop_types = prop_variations.map { |p| p[:type] }.compact.uniq
|
|
116
|
+
return one_of_schema(prop_variations) if prop_types.size > 1
|
|
117
|
+
|
|
118
|
+
case prop_types.first
|
|
119
|
+
when 'array' then merge_array_item_variations(prop_variations)
|
|
120
|
+
when 'object' then merge_variations(prop_variations)
|
|
121
|
+
else prop_variations.first.dup
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def flatten_one_of(prop_variations)
|
|
126
|
+
prop_variations.each_with_object([]) do |prop, options|
|
|
127
|
+
clean = without_nullable(prop)
|
|
128
|
+
if clean.key?(:oneOf)
|
|
129
|
+
options.concat(clean[:oneOf])
|
|
130
|
+
elsif !clean.empty?
|
|
131
|
+
options << clean
|
|
132
|
+
end
|
|
133
|
+
end.uniq
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def without_nullable(prop)
|
|
137
|
+
prop.reject { |k, _| k == :nullable }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def one_of_schema(variations)
|
|
141
|
+
{ oneOf: variations.map { |p| without_nullable(p) }.uniq }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private_constant :VariationMerger
|
|
146
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
RSpec::OpenAPI::SchemaBuilder = Object.new
|
|
4
4
|
require_relative 'schema_builder/build_context'
|
|
5
|
+
require_relative 'schema_builder/variation_merger'
|
|
5
6
|
|
|
6
7
|
class << RSpec::OpenAPI::SchemaBuilder
|
|
7
8
|
# @param [RSpec::OpenAPI::Record] record
|
|
@@ -74,7 +75,7 @@ class << RSpec::OpenAPI::SchemaBuilder
|
|
|
74
75
|
return nil if items.empty?
|
|
75
76
|
|
|
76
77
|
variations = items.map { |item| build_property(item, ctx) }
|
|
77
|
-
|
|
78
|
+
VariationMerger.merge_variations(variations)
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Returns the per-content-type body (schema + optional example/examples).
|
|
@@ -131,20 +132,18 @@ class << RSpec::OpenAPI::SchemaBuilder
|
|
|
131
132
|
build_parameter(key, value, location: 'header', record: record)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
parameters
|
|
135
|
+
parameters.empty? ? nil : parameters
|
|
135
136
|
end
|
|
136
137
|
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# `key[subkey]`; query params are pre-flattened and may be optional.
|
|
138
|
+
# Path and header params are always required; query params are pre-flattened
|
|
139
|
+
# by flatten_query_params and may be optional.
|
|
140
140
|
def build_parameter(key, value, location:, record:)
|
|
141
141
|
is_query = location == 'query'
|
|
142
|
-
compound_name = !is_query
|
|
143
142
|
required = is_query ? record.required_request_params.include?(key) : true
|
|
144
143
|
cast = try_cast(value)
|
|
145
144
|
ctx = BuildContext.new(record: record, context: :request, key: key, path: key.to_s)
|
|
146
145
|
{
|
|
147
|
-
name:
|
|
146
|
+
name: key.to_s,
|
|
148
147
|
in: location,
|
|
149
148
|
required: required,
|
|
150
149
|
schema: build_property(cast, ctx),
|
|
@@ -159,16 +158,6 @@ class << RSpec::OpenAPI::SchemaBuilder
|
|
|
159
158
|
end
|
|
160
159
|
end
|
|
161
160
|
|
|
162
|
-
def build_parameter_name(key, value)
|
|
163
|
-
key = key.to_s
|
|
164
|
-
if value.is_a?(Hash) && (value_keys = value.keys).size == 1
|
|
165
|
-
value_key = value_keys.first
|
|
166
|
-
build_parameter_name("#{key}[#{value_key}]", value[value_key])
|
|
167
|
-
else
|
|
168
|
-
key
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
161
|
def flatten_query_params(params, parent_key = nil)
|
|
173
162
|
params.each_with_object({}) do |(key, value), result|
|
|
174
163
|
full_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s
|
|
@@ -279,8 +268,6 @@ class << RSpec::OpenAPI::SchemaBuilder
|
|
|
279
268
|
# distinguishable from "no override"; for :hybrid_additional_properties
|
|
280
269
|
# plain lookup is enough because only Hash values are meaningful.
|
|
281
270
|
def infer_override(path, record, context, kind)
|
|
282
|
-
return nil unless record
|
|
283
|
-
|
|
284
271
|
overrides = record.send("#{context}_#{kind}")
|
|
285
272
|
return nil unless overrides
|
|
286
273
|
|
|
@@ -333,141 +320,11 @@ class << RSpec::OpenAPI::SchemaBuilder
|
|
|
333
320
|
def build_array_items_schema(array, ctx)
|
|
334
321
|
schemas = array.map { |item| build_property(item, ctx) }
|
|
335
322
|
return schemas.first if schemas.size == 1
|
|
336
|
-
return merge_non_object_item_variations(schemas) unless array.all?(Hash)
|
|
323
|
+
return VariationMerger.merge_non_object_item_variations(schemas) unless array.all?(Hash)
|
|
337
324
|
|
|
338
325
|
merged = schemas.first.dup
|
|
339
|
-
merged[:properties] = merge_property_variations(schemas, allow_recursive_merge: false)
|
|
326
|
+
merged[:properties] = VariationMerger.merge_property_variations(schemas, allow_recursive_merge: false)
|
|
340
327
|
merged[:required] = schemas.map { |s| s[:required] || [] }.reduce(:&) || []
|
|
341
328
|
merged
|
|
342
329
|
end
|
|
343
|
-
|
|
344
|
-
def merge_non_object_item_variations(schemas)
|
|
345
|
-
nullable_only = ->(schema) { schema.keys == [:nullable] }
|
|
346
|
-
typed = schemas.reject(&nullable_only)
|
|
347
|
-
|
|
348
|
-
merged = typed.size == 1 ? typed.first.dup : merge_multi(typed)
|
|
349
|
-
merged[:nullable] = true if schemas.any?(&nullable_only) && merged.is_a?(Hash)
|
|
350
|
-
merged
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def build_merged_schema_from_variations(variations)
|
|
354
|
-
# Drop empty `{}` schemas (e.g. items of an empty array) — they carry no
|
|
355
|
-
# type info and would otherwise spuriously mark every property of their
|
|
356
|
-
# populated siblings as nullable via the missing-key nullable rule.
|
|
357
|
-
non_empty = variations.reject(&:empty?)
|
|
358
|
-
return {} if non_empty.empty?
|
|
359
|
-
return non_empty.first if non_empty.size == 1
|
|
360
|
-
|
|
361
|
-
types = non_empty.map { |v| v[:type] }.compact.uniq
|
|
362
|
-
return one_of_schema(non_empty) if types.size > 1
|
|
363
|
-
|
|
364
|
-
case types.first
|
|
365
|
-
when 'object'
|
|
366
|
-
{
|
|
367
|
-
type: 'object',
|
|
368
|
-
properties: merge_property_variations(non_empty, allow_recursive_merge: true),
|
|
369
|
-
required: non_empty.map { |v| v[:required] || [] }.reduce(:&) || [],
|
|
370
|
-
}
|
|
371
|
-
when 'array'
|
|
372
|
-
items_variations = non_empty.map { |v| v[:items] }.compact
|
|
373
|
-
{ type: 'array', items: build_merged_schema_from_variations(items_variations) }
|
|
374
|
-
else
|
|
375
|
-
non_empty.first
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
# Merge the per-key property schemas of multiple object variations.
|
|
380
|
-
# When `allow_recursive_merge` is true, objects are recursively merged via
|
|
381
|
-
# build_merged_schema_from_variations and existing oneOf entries are flattened.
|
|
382
|
-
# When false (callsite: array-items merging), divergent property variations
|
|
383
|
-
# become oneOf without recursive descent.
|
|
384
|
-
def merge_property_variations(variations, allow_recursive_merge:)
|
|
385
|
-
property_keys(variations).each_with_object({}) do |key, merged_props|
|
|
386
|
-
all = variations.map { |v| v[:properties]&.[](key) }
|
|
387
|
-
prop_variations = all.reject { |p| p.nil? || p.keys == [:nullable] }
|
|
388
|
-
has_nullable = nullable_present?(all, recursive: allow_recursive_merge)
|
|
389
|
-
|
|
390
|
-
next if prop_variations.empty? && !has_nullable
|
|
391
|
-
|
|
392
|
-
merged_props[key] = merge_single_property(prop_variations, has_nullable,
|
|
393
|
-
variations_total: variations.size,
|
|
394
|
-
allow_recursive_merge: allow_recursive_merge,)
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def property_keys(variations)
|
|
399
|
-
variations.flat_map { |v| v[:properties]&.keys || [] }.uniq
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
# `recursive` mirrors build_merged_schema_from_variations' original rule that
|
|
403
|
-
# also treats `{ ..., nullable: true }` as a nullable signal. Array-items
|
|
404
|
-
# merging only looks at outright nil or `{ nullable: true }` markers.
|
|
405
|
-
def nullable_present?(all_props, recursive:)
|
|
406
|
-
all_props.any? do |p|
|
|
407
|
-
p.nil? || (p.is_a?(Hash) && (p.keys == [:nullable] || (recursive && p[:nullable] == true)))
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def merge_single_property(prop_variations, has_nullable, variations_total:, allow_recursive_merge:)
|
|
412
|
-
return { nullable: true } if prop_variations.empty?
|
|
413
|
-
|
|
414
|
-
merged =
|
|
415
|
-
if prop_variations.size == 1
|
|
416
|
-
prop_variations.first.dup
|
|
417
|
-
else
|
|
418
|
-
merge_multi(prop_variations)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
return merged unless merged.is_a?(Hash)
|
|
422
|
-
|
|
423
|
-
# In recursive mode, multi-variation merges also flag nullable when the key
|
|
424
|
-
# only appeared in some of the source variations.
|
|
425
|
-
needs_nullable =
|
|
426
|
-
if allow_recursive_merge && prop_variations.size > 1
|
|
427
|
-
has_nullable || prop_variations.size < variations_total
|
|
428
|
-
else
|
|
429
|
-
has_nullable
|
|
430
|
-
end
|
|
431
|
-
merged[:nullable] = true if needs_nullable
|
|
432
|
-
merged
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
# Combine multiple variations of the same property: flatten existing oneOf
|
|
436
|
-
# entries, recurse into objects and arrays, and combine divergent types into
|
|
437
|
-
# oneOf. Scalar variations of a single type collapse to the first schema.
|
|
438
|
-
def merge_multi(prop_variations)
|
|
439
|
-
return { oneOf: flatten_one_of(prop_variations) } if prop_variations.any? { |p| p.key?(:oneOf) }
|
|
440
|
-
|
|
441
|
-
prop_types = prop_variations.map { |p| p[:type] }.compact.uniq
|
|
442
|
-
return one_of_schema(prop_variations) if prop_types.size > 1
|
|
443
|
-
|
|
444
|
-
case prop_types.first
|
|
445
|
-
when 'array'
|
|
446
|
-
items_variations = prop_variations.map { |p| p[:items] }.compact
|
|
447
|
-
{ type: 'array', items: build_merged_schema_from_variations(items_variations) }
|
|
448
|
-
when 'object'
|
|
449
|
-
build_merged_schema_from_variations(prop_variations)
|
|
450
|
-
else
|
|
451
|
-
prop_variations.first.dup
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def flatten_one_of(prop_variations)
|
|
456
|
-
prop_variations.each_with_object([]) do |prop, options|
|
|
457
|
-
clean = without_nullable(prop)
|
|
458
|
-
if clean.key?(:oneOf)
|
|
459
|
-
options.concat(clean[:oneOf])
|
|
460
|
-
elsif !clean.empty?
|
|
461
|
-
options << clean
|
|
462
|
-
end
|
|
463
|
-
end.uniq
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def without_nullable(prop)
|
|
467
|
-
prop.reject { |k, _| k == :nullable }
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def one_of_schema(variations)
|
|
471
|
-
{ oneOf: variations.map { |p| without_nullable(p) }.uniq }
|
|
472
|
-
end
|
|
473
330
|
end
|
|
@@ -79,10 +79,9 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
-
# Recursively remove temporary fields like :_example_key and :_example_name from the schema
|
|
82
|
+
# Recursively remove temporary fields like :_example_key and :_example_name from the schema.
|
|
83
|
+
# Every caller has already established that it holds a Hash.
|
|
83
84
|
def cleanup_temporary_fields!(hash)
|
|
84
|
-
return unless hash.is_a?(Hash)
|
|
85
|
-
|
|
86
85
|
hash.delete(:_example_key)
|
|
87
86
|
hash.delete(:_example_summary)
|
|
88
87
|
hash.delete(:_example_name)
|
|
@@ -113,9 +112,9 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
|
113
112
|
path_definition.delete(:parameters) if path_definition[:parameters].empty?
|
|
114
113
|
end
|
|
115
114
|
|
|
116
|
-
def cleanup_array!(base, spec, selector, fields_for_identity
|
|
115
|
+
def cleanup_array!(base, spec, selector, fields_for_identity)
|
|
117
116
|
marshal = lambda do |obj|
|
|
118
|
-
Marshal.dump(slice(
|
|
117
|
+
Marshal.dump(obj.slice(*fields_for_identity))
|
|
119
118
|
end
|
|
120
119
|
|
|
121
120
|
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
|
@@ -127,33 +126,20 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
|
|
|
127
126
|
target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
|
|
128
127
|
target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
|
|
129
128
|
# Keep the last duplicate to produce the result stably
|
|
130
|
-
deduplicated = target_array.reverse.uniq { |param| slice(
|
|
129
|
+
deduplicated = target_array.reverse.uniq { |param| param.slice(*fields_for_identity) }.reverse
|
|
131
130
|
target_array.replace(deduplicated)
|
|
132
131
|
end
|
|
133
132
|
base
|
|
134
133
|
end
|
|
135
134
|
|
|
135
|
+
# Every selector above names at least two segments, so a matched path always
|
|
136
|
+
# has a parent to delete the entry from.
|
|
136
137
|
def cleanup_hash!(base, spec, selector)
|
|
137
138
|
RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
|
|
138
139
|
exist_in_base = !base.dig(*paths).nil?
|
|
139
140
|
not_in_spec = spec.dig(*paths).nil?
|
|
140
|
-
if exist_in_base && not_in_spec
|
|
141
|
-
if paths.size == 1
|
|
142
|
-
base.delete(paths.last)
|
|
143
|
-
else
|
|
144
|
-
parent_node = base.dig(*paths[0..-2])
|
|
145
|
-
parent_node.delete(paths.last)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
141
|
+
base.dig(*paths[0..-2]).delete(paths.last) if exist_in_base && not_in_spec
|
|
148
142
|
end
|
|
149
143
|
base
|
|
150
144
|
end
|
|
151
|
-
|
|
152
|
-
def slice(obj, fields_for_identity)
|
|
153
|
-
if fields_for_identity.any?
|
|
154
|
-
obj.slice(*fields_for_identity)
|
|
155
|
-
else
|
|
156
|
-
obj
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
145
|
end
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
4
|
+
# Merge spec into base, normalizing the keys of both to symbols first.
|
|
5
|
+
#
|
|
4
6
|
# @param [Hash] base
|
|
5
7
|
# @param [Hash] spec
|
|
6
8
|
def merge!(base, spec)
|
|
7
|
-
spec = RSpec::OpenAPI::KeyTransformer.symbolize(spec)
|
|
8
9
|
base.replace(RSpec::OpenAPI::KeyTransformer.symbolize(base))
|
|
9
|
-
|
|
10
|
+
merge_normalized!(base, spec)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Same, for a base whose keys are already symbols.
|
|
14
|
+
#
|
|
15
|
+
# Normalizing base rebuilds every hash and array in it. Callers that merge
|
|
16
|
+
# record after record into one document hold a base of thousands of nodes and
|
|
17
|
+
# merge into it twice per request, so paying for that rebuild each time makes
|
|
18
|
+
# the whole build quadratic in the number of requests. A document read back
|
|
19
|
+
# from a file is normalized by SchemaFile, and anything this file builds is
|
|
20
|
+
# normalized by construction, so those callers can skip it.
|
|
21
|
+
#
|
|
22
|
+
# @param [Hash] base
|
|
23
|
+
# @param [Hash] spec
|
|
24
|
+
def merge_normalized!(base, spec)
|
|
25
|
+
merge_schema!(base, RSpec::OpenAPI::KeyTransformer.symbolize(spec))
|
|
10
26
|
end
|
|
11
27
|
|
|
12
28
|
SIMILARITY_THRESHOLD = 0.5
|
|
@@ -180,7 +196,11 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
180
196
|
end
|
|
181
197
|
|
|
182
198
|
def convert_example_to_examples!(hash)
|
|
183
|
-
|
|
199
|
+
# This writes straight into the document rather than going through
|
|
200
|
+
# KeyTransformer, so symbolize the name here the way symbolize_examples
|
|
201
|
+
# would. Otherwise the next merge sees a string key beside the symbol one
|
|
202
|
+
# the builder produces and treats them as two different examples.
|
|
203
|
+
name = RSpec::OpenAPI::ExampleKey.normalize(hash.delete(:_example_key))&.to_sym || :default
|
|
184
204
|
summary = hash.delete(:_example_summary)
|
|
185
205
|
value = hash.delete(:example)
|
|
186
206
|
example = {}
|
data/lib/rspec/openapi.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'rspec/openapi/operation_converter'
|
|
|
8
8
|
require 'rspec/openapi/stream_parser'
|
|
9
9
|
require 'rspec/openapi/record_builder'
|
|
10
10
|
require 'rspec/openapi/exchange_recorder'
|
|
11
|
+
require 'rspec/openapi/parallel_records'
|
|
11
12
|
require 'rspec/openapi/result_recorder'
|
|
12
13
|
require 'rspec/openapi/schema_builder'
|
|
13
14
|
require 'rspec/openapi/schema_file'
|
|
@@ -131,10 +132,13 @@ module RSpec::OpenAPI
|
|
|
131
132
|
# Allow Rails request specs to issue extra verbs (e.g. QUERY); ActionDispatch
|
|
132
133
|
# otherwise rejects unknown verbs. No-op outside Rails.
|
|
133
134
|
def register_http_methods(methods)
|
|
134
|
-
#
|
|
135
|
+
# :nocov:
|
|
136
|
+
# Guards users without Rails. Every job here installs it, so this arm is
|
|
137
|
+
# unreachable from the suite. SimpleCov's token is `nocov`, so the marker
|
|
138
|
+
# this used to carry never actually excluded anything.
|
|
135
139
|
return unless defined?(ActionDispatch::Request::HTTP_METHODS)
|
|
136
140
|
|
|
137
|
-
#
|
|
141
|
+
# :nocov:
|
|
138
142
|
Array(methods).each do |method|
|
|
139
143
|
verb = method.to_s.upcase
|
|
140
144
|
next if ActionDispatch::Request::HTTP_METHODS.include?(verb)
|
|
@@ -149,6 +153,9 @@ end
|
|
|
149
153
|
if ENV['OPENAPI']
|
|
150
154
|
RSpec::OpenAPI::Config.load_environment_settings
|
|
151
155
|
|
|
156
|
+
# :nocov:
|
|
157
|
+
# These two rescues report a framework the user does not have. Every job here
|
|
158
|
+
# installs both Hanami and Rails, so no run can reach them.
|
|
152
159
|
begin
|
|
153
160
|
require 'hanami'
|
|
154
161
|
rescue LoadError
|
|
@@ -164,6 +171,7 @@ if ENV['OPENAPI']
|
|
|
164
171
|
else
|
|
165
172
|
require 'rspec/openapi/extractors/rails'
|
|
166
173
|
end
|
|
174
|
+
# :nocov:
|
|
167
175
|
end
|
|
168
176
|
|
|
169
177
|
require 'rspec/openapi/minitest_hooks' if Object.const_defined?('Minitest')
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-openapi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.33.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Takashi Kokubun
|
|
@@ -77,12 +77,14 @@ files:
|
|
|
77
77
|
- lib/rspec/openapi/minitest_hooks.rb
|
|
78
78
|
- lib/rspec/openapi/nullable_converter.rb
|
|
79
79
|
- lib/rspec/openapi/operation_converter.rb
|
|
80
|
+
- lib/rspec/openapi/parallel_records.rb
|
|
80
81
|
- lib/rspec/openapi/record.rb
|
|
81
82
|
- lib/rspec/openapi/record_builder.rb
|
|
82
83
|
- lib/rspec/openapi/result_recorder.rb
|
|
83
84
|
- lib/rspec/openapi/rspec_hooks.rb
|
|
84
85
|
- lib/rspec/openapi/schema_builder.rb
|
|
85
86
|
- lib/rspec/openapi/schema_builder/build_context.rb
|
|
87
|
+
- lib/rspec/openapi/schema_builder/variation_merger.rb
|
|
86
88
|
- lib/rspec/openapi/schema_cleaner.rb
|
|
87
89
|
- lib/rspec/openapi/schema_file.rb
|
|
88
90
|
- lib/rspec/openapi/schema_merger.rb
|
|
@@ -97,7 +99,7 @@ licenses:
|
|
|
97
99
|
metadata:
|
|
98
100
|
homepage_uri: https://github.com/exoego/rspec-openapi
|
|
99
101
|
source_code_uri: https://github.com/exoego/rspec-openapi
|
|
100
|
-
changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.
|
|
102
|
+
changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.33.0
|
|
101
103
|
rubygems_mfa_required: 'true'
|
|
102
104
|
rdoc_options: []
|
|
103
105
|
require_paths:
|