Skip to content

Commit f591008

Browse files
authoredJan 10, 2025··
Support multiple response schemas for OpenAPI 2 (#433)
1 parent 7bc4f08 commit f591008

File tree

5 files changed

+60
-36
lines changed

5 files changed

+60
-36
lines changed
 

‎lib/committee/drivers/open_api_2/driver.rb

+15-14
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,17 @@ def schema_class
8585

8686
def find_best_fit_response(link_data)
8787
if response_data = link_data["responses"]["200"] || response_data = link_data["responses"][200]
88-
[200, response_data]
88+
200
8989
elsif response_data = link_data["responses"]["201"] || response_data = link_data["responses"][201]
90-
[201, response_data]
90+
201
9191
else
9292
# Sort responses so that we can try to prefer any 3-digit status code.
9393
# If there are none, we'll just take anything from the list.
9494
ordered_responses = link_data["responses"].select { |k, v| k.to_s =~ /[0-9]{3}/ }
9595
if first = ordered_responses.first
96-
[first[0].to_i, first[1]]
96+
first[0].to_i
9797
else
98-
[nil, nil]
98+
nil
9999
end
100100
end
101101
end
@@ -165,18 +165,16 @@ def parse_routes!(data, schema, store)
165165
schemas_data["properties"][href]["properties"][method] = schema_data
166166
end
167167

168-
# Arbitrarily pick one response for the time being. Prefers in order:
169-
# a 200, 201, any 3-digit numerical response, then anything at all.
170-
status, response_data = find_best_fit_response(link_data)
171-
if status
172-
link.status_success = status
168+
target_schemas_data["properties"][href]["properties"][method] ||= { "properties" => {} }
169+
link_data["responses"].each do |key, response_data|
170+
status = key.to_i
171+
next unless response_data["schema"]
173172

174-
# A link need not necessarily specify a target schema.
175-
if response_data["schema"]
176-
target_schemas_data["properties"][href]["properties"][method] = response_data["schema"]
177-
end
173+
target_schemas_data["properties"][href]["properties"][method]["properties"][status] = response_data["schema"]
178174
end
179175

176+
link.status_success = find_best_fit_response(link_data)
177+
180178
rx = %r{^#{href_to_regex(link.href)}$}
181179
Committee.log_debug "Created route: #{link.method} #{link.href} (regex #{rx})"
182180

@@ -206,7 +204,10 @@ def parse_routes!(data, schema, store)
206204
end
207205

208206
# response
209-
link.target_schema = target_schemas.properties[link.href].properties[method]
207+
link.target_schemas = {}
208+
target_schemas.properties[link.href].properties[method].properties.each do |status, schema|
209+
link.target_schemas[status] = schema
210+
end
210211
end
211212
end
212213

‎lib/committee/drivers/open_api_2/link.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ class Link
2323

2424
# The link's output schema. i.e. How we validate an endpoint's response
2525
# data.
26-
attr_accessor :target_schema
26+
attr_accessor :target_schemas
2727

2828
attr_accessor :header_schema
2929

3030
def rel
3131
raise "Committee: rel not implemented for OpenAPI"
3232
end
33+
34+
def target_schema
35+
target_schemas[status_success] ||
36+
target_schemas[200] ||
37+
target_schemas[201] ||
38+
target_schemas.values.first
39+
end
3340
end
3441
end
3542
end

‎lib/committee/schema_validator/hyper_schema/response_validator.rb

+14-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ def initialize(link, options = {})
1111
@validate_success_only = options[:validate_success_only]
1212
@allow_blank_structures = options[:allow_blank_structures]
1313

14-
@validator = JsonSchema::Validator.new(target_schema(link))
14+
@validators = {}
15+
if link.is_a? Drivers::OpenAPI2::Link
16+
link.target_schemas.each do |status, schema|
17+
@validators[status] = JsonSchema::Validator.new(target_schema(link))
18+
end
19+
else
20+
@validators[link.status_success] = JsonSchema::Validator.new(target_schema(link))
21+
end
1522
end
1623

1724
def call(status, headers, data)
@@ -45,9 +52,12 @@ def call(status, headers, data)
4552
end
4653

4754
begin
48-
if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
49-
errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
50-
raise InvalidResponse, "Invalid response.\n\n#{errors}"
55+
if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
56+
raise InvalidResponse, "Invalid response.#{@link.href} status code #{status} definition does not exist" if @validators[status].nil?
57+
if !@validators[status].validate(data)
58+
errors = JsonSchema::SchemaError.aggregate(@validators[status].errors).join("\n")
59+
raise InvalidResponse, "Invalid response.\n\n#{errors}"
60+
end
5161
end
5262
rescue => e
5363
raise InvalidResponse, "Invalid response.\n\nschema is undefined" if /undefined method .all_of. for nil/ =~ e.message

‎test/drivers/open_api_2/link_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@link.method = "GET"
1212
@link.status_success = 200
1313
@link.schema = { "title" => "input" }
14-
@link.target_schema = { "title" => "target" }
14+
@link.target_schemas = { 200 => { "title" => "target" } }
1515
end
1616

1717
it "uses set #enc_type" do

‎test/schema_validator/hyper_schema/response_generator_test.rb

+22-16
Original file line numberDiff line numberDiff line change
@@ -74,62 +74,68 @@
7474

7575
it "generates first enum value for a schema with enum" do
7676
link = Committee::Drivers::OpenAPI2::Link.new
77-
link.target_schema = JsonSchema::Schema.new
78-
link.target_schema.enum = ["foo"]
79-
link.target_schema.type = ["string"]
77+
target_schema = JsonSchema::Schema.new
78+
target_schema.enum = ["foo"]
79+
target_schema.type = ["string"]
80+
link.target_schemas = { 200 => target_schema }
8081
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
8182
assert_equal("foo", data)
8283
end
8384

8485
it "generates basic types" do
8586
link = Committee::Drivers::OpenAPI2::Link.new
86-
link.target_schema = JsonSchema::Schema.new
87+
target_schema = JsonSchema::Schema.new
88+
link.target_schemas = { 200 => target_schema }
8789

88-
link.target_schema.type = ["integer"]
90+
target_schema.type = ["integer"]
8991
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
9092
assert_equal 0, data
9193

92-
link.target_schema.type = ["null"]
94+
target_schema.type = ["null"]
9395
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
9496
assert_nil data
9597

96-
link.target_schema.type = ["string"]
98+
target_schema.type = ["string"]
9799
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
98100
assert_equal "", data
99101
end
100102

101103
it "generates an empty array for an array type" do
102104
link = Committee::Drivers::OpenAPI2::Link.new
103-
link.target_schema = JsonSchema::Schema.new
104-
link.target_schema.type = ["array"]
105+
target_schema = JsonSchema::Schema.new
106+
link.target_schemas = { 200 => target_schema }
107+
target_schema.type = ["array"]
105108
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
106109
assert_equal([], data)
107110
end
108111

109112
it "generates an empty object for an object with no fields" do
110113
link = Committee::Drivers::OpenAPI2::Link.new
111-
link.target_schema = JsonSchema::Schema.new
112-
link.target_schema.type = ["object"]
114+
target_schema = JsonSchema::Schema.new
115+
link.target_schemas = { 200 => target_schema }
116+
target_schema.type = ["object"]
113117
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
114118
assert_equal({}, data)
115119
end
116120

117121
it "prefers an example to a built-in value" do
118122
link = Committee::Drivers::OpenAPI2::Link.new
119-
link.target_schema = JsonSchema::Schema.new
123+
target_schema = JsonSchema::Schema.new
124+
link.target_schemas = { 200 => target_schema }
120125

121-
link.target_schema.data = { "example" => 123 }
122-
link.target_schema.type = ["integer"]
126+
target_schema.data = { "example" => 123 }
127+
target_schema.type = ["integer"]
123128

124129
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
125130
assert_equal 123, data
126131
end
127132

128133
it "prefers non-null types to null types" do
129134
link = Committee::Drivers::OpenAPI2::Link.new
130-
link.target_schema = JsonSchema::Schema.new
135+
target_schema = JsonSchema::Schema.new
136+
link.target_schemas = { 200 => target_schema }
131137

132-
link.target_schema.type = ["null", "integer"]
138+
target_schema.type = ["null", "integer"]
133139
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
134140
assert_equal 0, data
135141
end

0 commit comments

Comments
 (0)
Please sign in to comment.