Skip to content

Commit 53fae01

Browse files
committed
Add output of warnings to Kernel#warn
This sends warnings for a document to Kernel#warn so they are output for a user. A document can be created with emit_warnings: false as a means to suppress these. Previously we only used warnings as an attribute of a document which could be accessed which meant they were quite hidden. This was based on the expectation that users are opening schemas they don't know about and thus a warning isn't helpful. However I now think for most people they are working with a schema in their codebase and thus it is better to be more explicit.
1 parent b12715f commit 53fae01

23 files changed

Lines changed: 138 additions & 91 deletions

lib/openapi3_parser.rb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,44 @@ module Openapi3Parser
77
# For a variety of inputs this will construct an OpenAPI document. For a
88
# String/File input it will try to determine if the input is JSON or YAML.
99
#
10-
# @param [String, Hash, File] input Source for the OpenAPI document
10+
# @param [String, Hash, File] input Source for the OpenAPI document
11+
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
12+
# warnings are output, best set to
13+
# false when parsing specification
14+
# files you've not authored
1115
#
1216
# @return [Document]
13-
def self.load(input)
14-
Document.new(SourceInput::Raw.new(input))
17+
def self.load(input, emit_warnings: true)
18+
Document.new(SourceInput::Raw.new(input), emit_warnings:)
1519
end
1620

1721
# For a given string filename this will read the file and parse it as an
1822
# OpenAPI document. It will try detect automatically whether the contents
1923
# are JSON or YAML.
2024
#
21-
# @param [String] path Filename of the OpenAPI document
25+
# @param [String] path Filename of the OpenAPI document
26+
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
27+
# warnings are output, best set to
28+
# false when parsing specification
29+
# files you've not authored
2230
#
2331
# @return [Document]
24-
def self.load_file(path)
25-
Document.new(SourceInput::File.new(path))
32+
def self.load_file(path, emit_warnings: true)
33+
Document.new(SourceInput::File.new(path), emit_warnings:)
2634
end
2735

2836
# For a given string URL this will request the resource and parse it as an
2937
# OpenAPI document. It will try detect automatically whether the contents
3038
# are JSON or YAML.
3139
#
32-
# @param [String] url URL of the OpenAPI document
40+
# @param [String] url URL of the OpenAPI document
41+
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
42+
# warnings are output, best set to
43+
# false when parsing specification
44+
# files you've not authored
3345
#
3446
# @return [Document]
35-
def self.load_url(url)
36-
Document.new(SourceInput::Url.new(url.to_s))
47+
def self.load_url(url, emit_warnings: true)
48+
Document.new(SourceInput::Url.new(url.to_s), emit_warnings:)
3749
end
3850
end

lib/openapi3_parser/document.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ module Openapi3Parser
99
# @attr_reader [OpenapiVersion] openapi_version
1010
# @attr_reader [Source] root_source
1111
# @attr_reader [Array<String>] warnings
12+
# @attr_reader [Boolean] emit_warnings
1213
class Document
1314
extend Forwardable
1415
include Enumerable
1516

16-
attr_reader :openapi_version, :root_source, :warnings
17+
attr_reader :openapi_version, :root_source, :warnings, :emit_warnings
1718

1819
# A collection of the openapi versions that are supported
1920
SUPPORTED_OPENAPI_VERSIONS = %w[3.0 3.1].freeze
@@ -78,10 +79,15 @@ class Document
7879
:security, :tags, :external_docs, :extension, :[], :each,
7980
:keys
8081

81-
# @param [SourceInput] source_input
82-
def initialize(source_input)
82+
# @param [SourceInput] source_input
83+
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
84+
# warnings are output, best set to
85+
# false when parsing specification
86+
# files you've not authored
87+
def initialize(source_input, emit_warnings: true)
8388
@reference_registry = ReferenceRegistry.new
8489
@root_source = Source.new(source_input, self, reference_registry)
90+
@emit_warnings = emit_warnings
8591
@warnings = []
8692
@openapi_version = determine_openapi_version(root_source.data["openapi"])
8793
@build_in_progress = false
@@ -169,6 +175,7 @@ def look_up_pointer(pointer, relative_pointer, subject)
169175
end
170176

171177
def add_warning(text)
178+
warn("Warning: #{text}") if emit_warnings
172179
@warnings << text
173180
end
174181

@@ -192,12 +199,12 @@ def determine_openapi_version(version)
192199
if version
193200
add_warning(
194201
"Unsupported OpenAPI version (#{version}), treating as a " \
195-
"#{DEFAULT_OPENAPI_VERSION} document"
202+
"#{DEFAULT_OPENAPI_VERSION} document."
196203
)
197204
else
198205
add_warning(
199206
"Unspecified OpenAPI version, treating as a " \
200-
"#{DEFAULT_OPENAPI_VERSION} document"
207+
"#{DEFAULT_OPENAPI_VERSION} document."
201208
)
202209
end
203210

spec/lib/openapi3_parser/document/reference_registry_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
RSpec.describe Openapi3Parser::Document::ReferenceRegistry do
44
describe "#register" do
55
let(:source_location) do
6-
create_source_location({ contact: { name: "John Smith" } },
6+
create_source_location({ openapi: "3.0.0", contact: { name: "John Smith" } },
77
pointer_segments: %w[contact])
88
end
99

@@ -64,7 +64,7 @@
6464
describe "#factory" do
6565
let(:object_type) { "Openapi3Parser::NodeFactory::Contact" }
6666
let(:source_location) do
67-
create_source_location({ contact: { name: "John Smith" } },
67+
create_source_location({ openapi: "3.0.0", contact: { name: "John Smith" } },
6868
pointer_segments: %w[contact])
6969
end
7070

spec/lib/openapi3_parser/document_spec.rb

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,37 +35,48 @@ def raw_source_input(data)
3535
end
3636

3737
context "when no OpenAPI version is provided" do
38-
let(:instance) do
39-
described_class.new(
40-
raw_source_input(source_data.merge("openapi" => nil))
41-
)
42-
end
38+
let(:input) { raw_source_input(source_data.merge("openapi" => nil)) }
4339

4440
it "treats the version as the default for the library" do
45-
expect(instance.openapi_version)
46-
.to eq(Openapi3Parser::Document::DEFAULT_OPENAPI_VERSION)
41+
instance = nil
42+
expect { instance = described_class.new(input) }.to output.to_stderr
43+
expect(instance.openapi_version).to eq(Openapi3Parser::Document::DEFAULT_OPENAPI_VERSION)
4744
end
4845

4946
it "has a warning" do
50-
expect(instance.warnings).to include(/Unspecified OpenAPI version/)
47+
instance = nil
48+
warning = /Unspecified OpenAPI version/
49+
expect { instance = described_class.new(input) }
50+
.to output(warning).to_stderr
51+
expect(instance.warnings).to include(warning)
52+
end
53+
54+
it "doesn't output to stderr when emit_warnings is false" do
55+
expect { described_class.new(input, emit_warnings: false) }
56+
.not_to output.to_stderr
5157
end
5258
end
5359

5460
context "when an unsupported OpenAPI version is provided" do
55-
let(:instance) do
56-
described_class.new(
57-
raw_source_input(source_data.merge("openapi" => "2.0.0"))
58-
)
59-
end
61+
let(:input) { raw_source_input(source_data.merge("openapi" => "2.0.0")) }
6062

6163
it "treats the version as the default for the library" do
62-
expect(instance.openapi_version)
63-
.to eq(Openapi3Parser::Document::DEFAULT_OPENAPI_VERSION)
64+
instance = nil
65+
expect { instance = described_class.new(input) }.to output.to_stderr
66+
expect(instance.openapi_version).to eq(Openapi3Parser::Document::DEFAULT_OPENAPI_VERSION)
6467
end
6568

6669
it "has a warning" do
67-
expect(instance.warnings)
68-
.to include(/Unsupported OpenAPI version #{Regexp.escape('(2.0.0)')}/)
70+
instance = nil
71+
warning = /Unsupported OpenAPI version #{Regexp.escape('(2.0.0)')}/
72+
expect { instance = described_class.new(input) }
73+
.to output(warning).to_stderr
74+
expect(instance.warnings).to include(warning)
75+
end
76+
77+
it "doesn't output to stderr when emit_warnings is false" do
78+
expect { described_class.new(input, emit_warnings: false) }
79+
.not_to output.to_stderr
6980
end
7081
end
7182
end
@@ -131,7 +142,7 @@ def raw_source_input(data)
131142
end
132143

133144
it "returns errors for invalid source data" do
134-
instance = described_class.new(raw_source_input({}))
145+
instance = described_class.new(raw_source_input({ "openapi" => "3.0.0" }))
135146
expect(instance.errors).not_to be_empty
136147
end
137148

spec/lib/openapi3_parser/node/context_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@
168168

169169
describe "#==" do
170170
let(:document_location) do
171-
create_source_location({}, pointer_segments: %w[field_a])
171+
create_source_location({ "openapi" => "3.0.0" }, pointer_segments: %w[field_a])
172172
end
173173

174174
let(:source_location) do
175-
create_source_location({},
175+
create_source_location({ "openapi" => "3.0.0" },
176176
document: document_location.source.document,
177177
pointer_segments: %w[ref_a])
178178
end
@@ -213,17 +213,17 @@
213213

214214
describe "#same_data_inputs?" do
215215
let(:source_location) do
216-
create_source_location({}, pointer_segments: %w[ref_a])
216+
create_source_location({ openapi: "3.0.0" }, pointer_segments: %w[ref_a])
217217
end
218218

219219
let(:document_location) do
220-
create_source_location({},
220+
create_source_location({ openapi: "3.0.0" },
221221
document: source_location.source.document,
222222
pointer_segments: %w[field_a])
223223
end
224224

225225
let(:other_document_location) do
226-
create_source_location({},
226+
create_source_location({ openapi: "3.0.0" },
227227
document: source_location.source.document,
228228
pointer_segments: %w[field_b])
229229
end
@@ -305,7 +305,7 @@
305305
end
306306

307307
it "returns nil when there isn't a parent (for example at root)" do
308-
instance = create_node_context({}, document_input: {})
308+
instance = create_node_context({}, document_input: { "openapi" => "3.0.0" })
309309
expect(instance.parent_node).to be_nil
310310
end
311311
end

spec/lib/openapi3_parser/node_factory/components_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@
101101

102102
let(:node_factory_context) do
103103
create_node_factory_context(input,
104-
document_input: { "components" => input })
104+
document_input: { "openapi" => "3.0.0",
105+
"components" => input })
105106
end
106107
end
107108

spec/lib/openapi3_parser/node_factory/context_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
RSpec.describe Openapi3Parser::NodeFactory::Context do
44
describe ".root" do
5-
let(:input) { {} }
5+
let(:input) { { "openapi" => "3.0.0" } }
66
let(:source) { create_source(input) }
77

88
it "returns a context instance" do
@@ -21,7 +21,7 @@
2121
end
2222

2323
describe ".next_field" do
24-
let(:input) { { "key" => "value" } }
24+
let(:input) { { "openapi" => "3.0.0", "key" => "value" } }
2525
let(:parent_context) do
2626
create_node_factory_context(input, document_input: input)
2727
end
@@ -48,7 +48,7 @@
4848
end
4949

5050
describe ".resolved_reference" do
51-
let(:input) { "data" }
51+
let(:input) { { "openapi" => "3.0.0" } }
5252
let(:source_location) { create_source_location(input) }
5353

5454
let(:reference_context) do
@@ -67,7 +67,7 @@
6767
instance = described_class.resolved_reference(
6868
reference_context, source_location:
6969
)
70-
expect(instance.input).to eq "data"
70+
expect(instance.input).to eq(input)
7171
end
7272

7373
it "has the resolved reference location" do
@@ -88,7 +88,7 @@
8888

8989
describe "#location_summary" do
9090
it "returns a string representation of the pointer segments" do
91-
source_location = create_source_location({}, pointer_segments: %w[path to field])
91+
source_location = create_source_location({ "openapi" => "3.0.0" }, pointer_segments: %w[path to field])
9292
instance = described_class.new({}, source_location:)
9393
expect(instance.location_summary).to eq "#/path/to/field"
9494
end

spec/lib/openapi3_parser/node_factory/fields/reference_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
pointer_segments: %w[field $ref]
1010
)
1111
end
12-
let(:document_input) { {} }
12+
let(:document_input) { { "openapi" => "3.0.0" } }
1313

1414
describe "#resolved_input" do
1515
it "raises an error because a reference itself isn't resolved" do
@@ -33,8 +33,8 @@
3333
let(:instance) { described_class.new(factory_context, factory_class) }
3434

3535
context "when the reference can be resolved" do
36-
let(:document_input) do
37-
{ "reference" => { "name" => "Joe" } }
36+
before do
37+
document_input["reference"] = { "name" => "joe" }
3838
end
3939

4040
it "is valid" do
@@ -43,8 +43,8 @@
4343
end
4444

4545
context "when the reference can't be resolved" do
46-
let(:document_input) do
47-
{ "reference" => { "url" => "invalid url" } }
46+
before do
47+
document_input["reference"] = { "url" => "invalid url" }
4848
end
4949

5050
it "is invalid" do

spec/lib/openapi3_parser/node_factory/media_type_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
let(:document_input) do
3636
{
37+
"openapi" => "3.0.0",
3738
"components" => {
3839
"schemas" => {
3940
"Pet" => {

spec/lib/openapi3_parser/node_factory/oauth_flows_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
let(:document_input) do
1414
{
15+
"openapi" => "3.0.0",
1516
"myReference" => {
1617
"authorizationUrl" => "https://example.com/api/oauth/dialog",
1718
"tokenUrl" => "https://example.com/api/oauth/token",

0 commit comments

Comments
 (0)