rspec-openapi 0.35.0 → 0.36.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 +101 -0
- data/lib/rspec/openapi/components_updater.rb +16 -9
- data/lib/rspec/openapi/extractors/shared_extractor.rb +42 -6
- data/lib/rspec/openapi/hash_helper.rb +42 -3
- data/lib/rspec/openapi/key_transformer.rb +17 -7
- data/lib/rspec/openapi/minitest_hooks.rb +2 -1
- data/lib/rspec/openapi/parallel_records.rb +59 -4
- data/lib/rspec/openapi/partial_run.rb +84 -0
- data/lib/rspec/openapi/record.rb +2 -0
- data/lib/rspec/openapi/result_recorder.rb +21 -8
- data/lib/rspec/openapi/rspec_hooks.rb +11 -3
- data/lib/rspec/openapi/schema_builder/build_context.rb +18 -4
- data/lib/rspec/openapi/schema_builder/variation_merger.rb +28 -9
- data/lib/rspec/openapi/schema_builder.rb +91 -58
- data/lib/rspec/openapi/schema_cleaner.rb +73 -56
- data/lib/rspec/openapi/schema_file.rb +27 -20
- data/lib/rspec/openapi/schema_merger.rb +22 -1
- data/lib/rspec/openapi/schema_sorter.rb +24 -4
- data/lib/rspec/openapi/version.rb +1 -1
- data/lib/rspec/openapi/yaml_dumper.rb +103 -0
- data/lib/rspec/openapi/yaml_loader.rb +118 -0
- data/lib/rspec/openapi.rb +14 -1
- metadata +5 -2
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'stringio'
|
|
6
7
|
|
|
7
8
|
# For Ruby 2.7
|
|
8
9
|
require 'date'
|
|
9
10
|
|
|
10
|
-
# TODO: Support JSON
|
|
11
11
|
class RSpec::OpenAPI::SchemaFile
|
|
12
12
|
# @param [String] path
|
|
13
13
|
def initialize(path)
|
|
@@ -16,28 +16,24 @@ class RSpec::OpenAPI::SchemaFile
|
|
|
16
16
|
|
|
17
17
|
# Reads the existing spec, lets the block mutate it, writes the result back
|
|
18
18
|
# and returns the (symbolized) spec so it can be mirrored to other files.
|
|
19
|
+
# A document that does not load is left as it is.
|
|
19
20
|
# @return [Hash]
|
|
20
|
-
def edit
|
|
21
|
+
def edit
|
|
21
22
|
spec = read
|
|
22
|
-
|
|
23
|
+
begin
|
|
24
|
+
yield spec
|
|
25
|
+
ensure
|
|
26
|
+
write(spec)
|
|
27
|
+
end
|
|
23
28
|
spec
|
|
24
|
-
ensure
|
|
25
|
-
write(spec)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
# Writes an already-built spec to this file, choosing the format from the
|
|
29
|
-
# file extension.
|
|
32
|
+
# file extension. Both formats write the symbol keys as strings themselves.
|
|
30
33
|
# @param [Hash] spec
|
|
31
34
|
def write(spec)
|
|
32
|
-
stringified = RSpec::OpenAPI::KeyTransformer.stringify(spec)
|
|
33
35
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
34
|
-
|
|
35
|
-
if json?
|
|
36
|
-
JSON.pretty_generate(stringified)
|
|
37
|
-
else
|
|
38
|
-
prepend_comment(YAML.dump(stringified))
|
|
39
|
-
end
|
|
40
|
-
File.write(@path, output)
|
|
36
|
+
File.write(@path, json? ? JSON.pretty_generate(spec) : yaml_document(spec))
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
private
|
|
@@ -46,18 +42,29 @@ class RSpec::OpenAPI::SchemaFile
|
|
|
46
42
|
def read
|
|
47
43
|
return {} unless File.exist?(@path)
|
|
48
44
|
|
|
49
|
-
content =
|
|
50
|
-
|
|
45
|
+
content = File.read(@path)
|
|
46
|
+
spec = json? ? read_json(content) : RSpec::OpenAPI::YAMLLoader.load(content)
|
|
47
|
+
spec.nil? ? {} : spec
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_json(content)
|
|
51
|
+
return nil if content.match?(/\A\s*\z/)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
spec = JSON.parse(content, symbolize_names: true, max_nesting: false)
|
|
54
|
+
RSpec::OpenAPI::KeyTransformer.normalize_example_names!(spec)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
+
def yaml_document(spec)
|
|
58
|
+
io = StringIO.new(+'')
|
|
59
|
+
io.write(comment_lines) unless RSpec::OpenAPI.comment.nil?
|
|
60
|
+
RSpec::OpenAPI::YAMLDumper.dump(spec, io)
|
|
61
|
+
io.string
|
|
62
|
+
end
|
|
57
63
|
|
|
64
|
+
def comment_lines
|
|
58
65
|
comment = RSpec::OpenAPI.comment.dup
|
|
59
66
|
comment << "\n" unless comment.end_with?("\n")
|
|
60
|
-
|
|
67
|
+
comment.gsub(/^/, '# ').gsub(/^# \n/, "#\n")
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def json?
|
|
@@ -25,6 +25,16 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
25
25
|
merge_schema!(base, RSpec::OpenAPI::KeyTransformer.symbolize(spec))
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Same, for a spec built for this merge alone, like the schema of a record.
|
|
29
|
+
# Its keys are symbols already, and its hashes and arrays become part of base
|
|
30
|
+
# rather than being copied, so the caller must not use them afterwards.
|
|
31
|
+
#
|
|
32
|
+
# @param [Hash] base
|
|
33
|
+
# @param [Hash] spec
|
|
34
|
+
def merge_built!(base, spec)
|
|
35
|
+
merge_schema!(base, spec)
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
SCHEMA_KEYS = [:schema, :schemas, :itemSchema].freeze
|
|
29
39
|
DATA_KEYS = [:example, :examples, :default, :enum].freeze
|
|
30
40
|
NUMERIC_TYPES = ['integer', 'number'].freeze
|
|
@@ -32,6 +42,8 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
32
42
|
:title, :description, :deprecated, :readOnly, :writeOnly, :example, :examples, :default, :externalDocs, :xml,
|
|
33
43
|
:discriminator,
|
|
34
44
|
].freeze
|
|
45
|
+
# Like nullable, readOnly describes the whole value, so it stays beside a oneOf.
|
|
46
|
+
ONE_OF_WRAPPER_KEYS = [:nullable, :readOnly].freeze
|
|
35
47
|
|
|
36
48
|
private
|
|
37
49
|
|
|
@@ -63,6 +75,8 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
63
75
|
def merge_schema_entry!(base, key, value, in_schema)
|
|
64
76
|
if in_schema && key == :properties && base[key].is_a?(Hash) && value.is_a?(Hash)
|
|
65
77
|
value.each { |name, property| merge_entry!(base[key], name, property, true) }
|
|
78
|
+
elsif in_schema && key == :readOnly
|
|
79
|
+
merge_read_only!(base, value)
|
|
66
80
|
elsif DATA_KEYS.include?(key)
|
|
67
81
|
merge_data!(base, key, value)
|
|
68
82
|
else
|
|
@@ -70,6 +84,12 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
70
84
|
end
|
|
71
85
|
end
|
|
72
86
|
|
|
87
|
+
# A property only responses hold is readOnly, one a request sends is not. So
|
|
88
|
+
# false wins, and a side without an opinion leaves the other's.
|
|
89
|
+
def merge_read_only!(base, value)
|
|
90
|
+
base[:readOnly] = value && base.fetch(:readOnly, true)
|
|
91
|
+
end
|
|
92
|
+
|
|
73
93
|
# Recorded data stays data however it is shaped, so a field named like a
|
|
74
94
|
# schema key never gets merged as one.
|
|
75
95
|
def merge_data!(base, key, value)
|
|
@@ -137,6 +157,7 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
137
157
|
|
|
138
158
|
one_of_variants(spec).each { |variant| merge_variant!(base, variant) }
|
|
139
159
|
base[:nullable] = true if nullable
|
|
160
|
+
merge_read_only!(base, spec[:readOnly]) if spec.key?(:readOnly)
|
|
140
161
|
base
|
|
141
162
|
end
|
|
142
163
|
|
|
@@ -156,7 +177,7 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
|
|
|
156
177
|
end
|
|
157
178
|
|
|
158
179
|
def one_of_variants(schema)
|
|
159
|
-
variants = schema[:oneOf] || [schema.reject { |key, _| key
|
|
180
|
+
variants = schema[:oneOf] || [schema.reject { |key, _| ONE_OF_WRAPPER_KEYS.include?(key) }]
|
|
160
181
|
variants.reject { |variant| variant.is_a?(Hash) && variant.empty? }
|
|
161
182
|
end
|
|
162
183
|
|
|
@@ -23,7 +23,12 @@ class << RSpec::OpenAPI::SchemaSorter = Object.new
|
|
|
23
23
|
deep_sort_by_selector!(spec, "#{operation}.responses")
|
|
24
24
|
|
|
25
25
|
# content-type
|
|
26
|
+
deep_sort_by_selector!(spec, "#{operation}.requestBody.content")
|
|
26
27
|
deep_sort_by_selector!(spec, "#{operation}.responses.*.content")
|
|
28
|
+
|
|
29
|
+
# named examples (keyed by test description)
|
|
30
|
+
deep_sort_by_selector!(spec, "#{operation}.requestBody.content.*.examples")
|
|
31
|
+
deep_sort_by_selector!(spec, "#{operation}.responses.*.content.*.examples")
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
|
|
@@ -32,14 +37,29 @@ class << RSpec::OpenAPI::SchemaSorter = Object.new
|
|
|
32
37
|
# @param [Hash] base
|
|
33
38
|
# @param [String] selector
|
|
34
39
|
def deep_sort_by_selector!(base, selector)
|
|
35
|
-
RSpec::OpenAPI::HashHelper.
|
|
36
|
-
deep_sort_hash!(
|
|
40
|
+
RSpec::OpenAPI::HashHelper.each_matched(base, nil, selector) do |_parent, _key, hash, _counterpart|
|
|
41
|
+
deep_sort_hash!(hash)
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# Re-inserting a key moves it to the end, so re-inserting every key in sorted
|
|
41
|
-
# order sorts the hash in place.
|
|
46
|
+
# order sorts the hash in place. A document read back from a file is sorted
|
|
47
|
+
# already, so a hash whose keys are in order is left alone, without the two
|
|
48
|
+
# arrays of keys a sort would build. The keys are all symbols, which sort as
|
|
49
|
+
# their names do.
|
|
42
50
|
def deep_sort_hash!(hash)
|
|
43
|
-
|
|
51
|
+
return if sorted?(hash)
|
|
52
|
+
|
|
53
|
+
hash.keys.sort.each { |key| hash[key] = hash.delete(key) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sorted?(hash)
|
|
57
|
+
previous = nil
|
|
58
|
+
hash.each_key do |key|
|
|
59
|
+
return false if previous && previous > key
|
|
60
|
+
|
|
61
|
+
previous = key
|
|
62
|
+
end
|
|
63
|
+
true
|
|
44
64
|
end
|
|
45
65
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
# Writes a document straight to the libyaml emitter. YAML.dump first builds a
|
|
6
|
+
# tree of Psych nodes and then walks it to make the same emitter calls, so the
|
|
7
|
+
# output is the same without the tree. Keys are written as strings, which is
|
|
8
|
+
# what they are read back as, so the document needs no stringified copy.
|
|
9
|
+
#
|
|
10
|
+
# Nothing in a document is shared, so there are no anchors to find and the
|
|
11
|
+
# bookkeeping for them is skipped.
|
|
12
|
+
class RSpec::OpenAPI::YAMLDumper < Psych::Visitors::YAMLTree
|
|
13
|
+
# @param [Object] object
|
|
14
|
+
# @param [IO, StringIO] io
|
|
15
|
+
def self.dump(object, io)
|
|
16
|
+
dumper = create({}, Psych::Emitter.new(io))
|
|
17
|
+
dumper << object
|
|
18
|
+
dumper.finish
|
|
19
|
+
io
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Stands in for the emitter while the superclass writes a string, to keep
|
|
23
|
+
# the arguments of the one scalar call it makes.
|
|
24
|
+
class Recorder
|
|
25
|
+
attr_reader :scalar_arguments
|
|
26
|
+
|
|
27
|
+
def initialize(emitter)
|
|
28
|
+
@emitter = emitter
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def scalar(value, anchor, tag, plain, quoted, style) # rubocop:disable Metrics/ParameterLists
|
|
32
|
+
# The emitter copies a string of any encoding but UTF-8 on every call.
|
|
33
|
+
value = value.dup.force_encoding(Encoding::UTF_8) if value.encoding != Encoding::UTF_8 && value.ascii_only?
|
|
34
|
+
@scalar_arguments = [value, tag, plain, quoted, style].freeze
|
|
35
|
+
@emitter.scalar(value, anchor, tag, plain, quoted, style)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
private_constant :Recorder
|
|
39
|
+
|
|
40
|
+
def initialize(emitter, scanner, options)
|
|
41
|
+
super
|
|
42
|
+
# How a string is written takes several regexp matches to decide, and a
|
|
43
|
+
# document repeats its keys and the names of its types thousands of times.
|
|
44
|
+
# So the decision is kept by key, which saves the string of the key too,
|
|
45
|
+
# and by string.
|
|
46
|
+
@key_scalars = {}.compare_by_identity
|
|
47
|
+
@string_scalars = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_Hash(hash) # rubocop:disable Naming/MethodName
|
|
51
|
+
@emitter.start_mapping(nil, nil, true, Psych::Nodes::Mapping::BLOCK)
|
|
52
|
+
hash.each do |key, value|
|
|
53
|
+
emit_key(key)
|
|
54
|
+
accept(value)
|
|
55
|
+
end
|
|
56
|
+
@emitter.end_mapping
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def visit_Array(array) # rubocop:disable Naming/MethodName
|
|
60
|
+
@emitter.start_sequence(nil, nil, true, Psych::Nodes::Sequence::BLOCK)
|
|
61
|
+
array.each { |item| accept(item) }
|
|
62
|
+
@emitter.end_sequence
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_String(string) # rubocop:disable Naming/MethodName
|
|
66
|
+
scalar = @string_scalars[string]
|
|
67
|
+
return emit_scalar(scalar) if scalar
|
|
68
|
+
|
|
69
|
+
@string_scalars[string] = recording { super }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def emit_key(key)
|
|
75
|
+
scalar = @key_scalars[key]
|
|
76
|
+
return emit_scalar(scalar) if scalar
|
|
77
|
+
|
|
78
|
+
name = key.to_s
|
|
79
|
+
visit_String(name)
|
|
80
|
+
@key_scalars[key] = @string_scalars[name]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def emit_scalar(scalar)
|
|
84
|
+
@emitter.scalar(scalar[0], nil, scalar[1], scalar[2], scalar[3], scalar[4])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def recording
|
|
88
|
+
emitter = @emitter
|
|
89
|
+
@emitter = recorder = Recorder.new(emitter)
|
|
90
|
+
yield
|
|
91
|
+
recorder.scalar_arguments
|
|
92
|
+
ensure
|
|
93
|
+
@emitter = emitter
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# :nocov:
|
|
97
|
+
# Reached by values other than hashes, arrays and plain scalars, such as the
|
|
98
|
+
# dates of a hand-written document, which the suite does not hold.
|
|
99
|
+
def register(_target, yaml_obj)
|
|
100
|
+
yaml_obj
|
|
101
|
+
end
|
|
102
|
+
# :nocov:
|
|
103
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
# Builds the document with symbol keys straight from the parser events.
|
|
7
|
+
# YAML.safe_load builds a tree of Psych nodes first and hands back string keys,
|
|
8
|
+
# which KeyTransformer.symbolize then converted in a copy of the whole document.
|
|
9
|
+
# The names of examples are normalized on the way, as that copy did.
|
|
10
|
+
#
|
|
11
|
+
# A key names an entry, so it is a string however it reads: `200:` is the name
|
|
12
|
+
# of a response, not a number. Resolving it as YAML.safe_load does would give
|
|
13
|
+
# a key with no name to take.
|
|
14
|
+
#
|
|
15
|
+
# Anchors, tags and merge keys are left to YAML.safe_load, which accepts or
|
|
16
|
+
# rejects them as it always did. A valid alias follows its anchor, so only a
|
|
17
|
+
# dangling one gets this far, and it is handed off as well.
|
|
18
|
+
class RSpec::OpenAPI::YAMLLoader < Psych::Handler
|
|
19
|
+
class Unsupported < StandardError; end
|
|
20
|
+
private_constant :Unsupported
|
|
21
|
+
|
|
22
|
+
PERMITTED_CLASSES = [Date, Time].freeze
|
|
23
|
+
# Marks a mapping waiting for its next key.
|
|
24
|
+
NONE = Object.new.freeze
|
|
25
|
+
private_constant :NONE
|
|
26
|
+
|
|
27
|
+
# @param [String] yaml
|
|
28
|
+
# @return [Object, nil] the first document, or nil when there is none
|
|
29
|
+
def self.load(yaml)
|
|
30
|
+
loader = new
|
|
31
|
+
Psych::Parser.new(loader).parse(yaml)
|
|
32
|
+
loader.root
|
|
33
|
+
rescue Unsupported
|
|
34
|
+
RSpec::OpenAPI::KeyTransformer.symbolize(YAML.safe_load(yaml, permitted_classes: PERMITTED_CLASSES))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
super
|
|
39
|
+
class_loader = Psych::ClassLoader::Restricted.new(PERMITTED_CLASSES.map(&:name), [])
|
|
40
|
+
@scanner = Psych::ScalarScanner.new(class_loader)
|
|
41
|
+
@documents = []
|
|
42
|
+
@containers = [@documents]
|
|
43
|
+
# Per open mapping: the key waiting for its value, and whether the mapping
|
|
44
|
+
# holds named examples.
|
|
45
|
+
@pending_keys = []
|
|
46
|
+
@examples_maps = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def root
|
|
50
|
+
@documents.first
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def scalar(value, anchor, tag, _plain, quoted, _style) # rubocop:disable Metrics/ParameterLists
|
|
54
|
+
supported!(anchor, tag, value)
|
|
55
|
+
value = @scanner.tokenize(value) unless quoted || key_next?
|
|
56
|
+
add(value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def start_sequence(anchor, tag, _implicit, _style)
|
|
60
|
+
supported!(anchor, tag)
|
|
61
|
+
@containers.push(add([]))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def end_sequence
|
|
65
|
+
@containers.pop
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def start_mapping(anchor, tag, _implicit, _style)
|
|
69
|
+
supported!(anchor, tag)
|
|
70
|
+
@examples_maps.push(@containers.last.is_a?(Hash) && @pending_keys.last == :examples)
|
|
71
|
+
@containers.push(add({}))
|
|
72
|
+
@pending_keys.push(NONE)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def end_mapping
|
|
76
|
+
@containers.pop
|
|
77
|
+
@pending_keys.pop
|
|
78
|
+
@examples_maps.pop
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def alias(_anchor)
|
|
82
|
+
raise Unsupported
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# A `<<` key merges its value into the mapping, so it is handed off too.
|
|
88
|
+
def supported!(anchor, tag, value = nil)
|
|
89
|
+
raise Unsupported if anchor || tag || value == '<<'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A sequence inside a mapping pushes no pending key, so the mapping's NONE
|
|
93
|
+
# shows through it and the container is checked as well.
|
|
94
|
+
def key_next?
|
|
95
|
+
@pending_keys.last.equal?(NONE) && @containers.last.is_a?(Hash)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Values arrive in document order: onto the open sequence, or as the key and
|
|
99
|
+
# then the value of the open mapping. The documents are the outermost
|
|
100
|
+
# sequence.
|
|
101
|
+
def add(value)
|
|
102
|
+
container = @containers.last
|
|
103
|
+
if container.is_a?(Array)
|
|
104
|
+
container << value
|
|
105
|
+
elsif @pending_keys.last.equal?(NONE)
|
|
106
|
+
@pending_keys[-1] = key_for(value)
|
|
107
|
+
else
|
|
108
|
+
container[@pending_keys[-1]] = value
|
|
109
|
+
@pending_keys[-1] = NONE
|
|
110
|
+
end
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def key_for(key)
|
|
115
|
+
key = RSpec::OpenAPI::ExampleKey.normalize(key) if @examples_maps.last && key.is_a?(String)
|
|
116
|
+
key.to_sym
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/rspec/openapi.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'rspec/openapi/stream_parser'
|
|
|
9
9
|
require 'rspec/openapi/record_builder'
|
|
10
10
|
require 'rspec/openapi/exchange_recorder'
|
|
11
11
|
require 'rspec/openapi/parallel_records'
|
|
12
|
+
require 'rspec/openapi/partial_run'
|
|
12
13
|
require 'rspec/openapi/result_recorder'
|
|
13
14
|
require 'rspec/openapi/schema_builder'
|
|
14
15
|
require 'rspec/openapi/schema_file'
|
|
@@ -17,6 +18,8 @@ require 'rspec/openapi/schema_merger'
|
|
|
17
18
|
require 'rspec/openapi/schema_cleaner'
|
|
18
19
|
require 'rspec/openapi/schema_sorter'
|
|
19
20
|
require 'rspec/openapi/key_transformer'
|
|
21
|
+
require 'rspec/openapi/yaml_dumper'
|
|
22
|
+
require 'rspec/openapi/yaml_loader'
|
|
20
23
|
require 'rspec/openapi/shared_hooks'
|
|
21
24
|
require 'rspec/openapi/extractors'
|
|
22
25
|
require 'rspec/openapi/extractors/shared_extractor'
|
|
@@ -32,6 +35,11 @@ module RSpec::OpenAPI
|
|
|
32
35
|
'text/event-stream',
|
|
33
36
|
].freeze
|
|
34
37
|
|
|
38
|
+
# Properties a server assigns by convention, for read_only_builder. Foreign
|
|
39
|
+
# keys such as `user_id` are left out, since clients send them.
|
|
40
|
+
DEFAULT_READ_ONLY_KEYS = ['id', 'uuid', 'created_at', 'updated_at'].freeze
|
|
41
|
+
DEFAULT_READ_ONLY_BUILDER = ->(_example, key) { DEFAULT_READ_ONLY_KEYS.include?(key) }
|
|
42
|
+
|
|
35
43
|
class Config
|
|
36
44
|
class << self
|
|
37
45
|
attr_accessor :debug_enabled
|
|
@@ -52,6 +60,7 @@ module RSpec::OpenAPI
|
|
|
52
60
|
@summary_builder = ->(example) { example.metadata[:summary] }
|
|
53
61
|
@tags_builder = ->(example) { example.metadata[:tags] }
|
|
54
62
|
@formats_builder = ->(example) { example.metadata[:formats] }
|
|
63
|
+
@read_only_builder = nil
|
|
55
64
|
@info = {}
|
|
56
65
|
@application_version = '1.0.0'
|
|
57
66
|
@openapi_version = '3.2.0'
|
|
@@ -65,6 +74,8 @@ module RSpec::OpenAPI
|
|
|
65
74
|
@ignored_path_params = [:controller, :action, :format]
|
|
66
75
|
@ignored_paths = []
|
|
67
76
|
@post_process_hook = nil
|
|
77
|
+
# true or false forces the mode. nil lets each run decide, see PartialRun.
|
|
78
|
+
@partial_update = { '1' => true, '0' => false }[ENV.fetch('OPENAPI_PARTIAL_UPDATE', nil)]
|
|
68
79
|
|
|
69
80
|
# This is the configuraion override file name we look for within each path.
|
|
70
81
|
@config_filename = 'rspec_openapi.rb'
|
|
@@ -80,6 +91,7 @@ module RSpec::OpenAPI
|
|
|
80
91
|
:summary_builder,
|
|
81
92
|
:tags_builder,
|
|
82
93
|
:formats_builder,
|
|
94
|
+
:read_only_builder,
|
|
83
95
|
:info,
|
|
84
96
|
:application_version,
|
|
85
97
|
:request_headers,
|
|
@@ -91,7 +103,8 @@ module RSpec::OpenAPI
|
|
|
91
103
|
:path_records,
|
|
92
104
|
:ignored_paths,
|
|
93
105
|
:ignored_path_params,
|
|
94
|
-
:post_process_hook
|
|
106
|
+
:post_process_hook,
|
|
107
|
+
:partial_update
|
|
95
108
|
|
|
96
109
|
attr_reader :config_filename, :openapi_version
|
|
97
110
|
|
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.36.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Takashi Kokubun
|
|
@@ -78,6 +78,7 @@ files:
|
|
|
78
78
|
- lib/rspec/openapi/nullable_converter.rb
|
|
79
79
|
- lib/rspec/openapi/operation_converter.rb
|
|
80
80
|
- lib/rspec/openapi/parallel_records.rb
|
|
81
|
+
- lib/rspec/openapi/partial_run.rb
|
|
81
82
|
- lib/rspec/openapi/record.rb
|
|
82
83
|
- lib/rspec/openapi/record_builder.rb
|
|
83
84
|
- lib/rspec/openapi/result_recorder.rb
|
|
@@ -92,6 +93,8 @@ files:
|
|
|
92
93
|
- lib/rspec/openapi/shared_hooks.rb
|
|
93
94
|
- lib/rspec/openapi/stream_parser.rb
|
|
94
95
|
- lib/rspec/openapi/version.rb
|
|
96
|
+
- lib/rspec/openapi/yaml_dumper.rb
|
|
97
|
+
- lib/rspec/openapi/yaml_loader.rb
|
|
95
98
|
- rspec-openapi.gemspec
|
|
96
99
|
homepage: https://github.com/exoego/rspec-openapi
|
|
97
100
|
licenses:
|
|
@@ -99,7 +102,7 @@ licenses:
|
|
|
99
102
|
metadata:
|
|
100
103
|
homepage_uri: https://github.com/exoego/rspec-openapi
|
|
101
104
|
source_code_uri: https://github.com/exoego/rspec-openapi
|
|
102
|
-
changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.
|
|
105
|
+
changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.36.0
|
|
103
106
|
rubygems_mfa_required: 'true'
|
|
104
107
|
rdoc_options: []
|
|
105
108
|
require_paths:
|