Skip to content

Commit

Permalink
Support multiple response schemas for OpenAPI 2 (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
renatolond authored Jan 10, 2025
1 parent 7bc4f08 commit f591008
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 36 deletions.
29 changes: 15 additions & 14 deletions lib/committee/drivers/open_api_2/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,17 @@ def schema_class

def find_best_fit_response(link_data)
if response_data = link_data["responses"]["200"] || response_data = link_data["responses"][200]
[200, response_data]
200
elsif response_data = link_data["responses"]["201"] || response_data = link_data["responses"][201]
[201, response_data]
201
else
# Sort responses so that we can try to prefer any 3-digit status code.
# If there are none, we'll just take anything from the list.
ordered_responses = link_data["responses"].select { |k, v| k.to_s =~ /[0-9]{3}/ }
if first = ordered_responses.first
[first[0].to_i, first[1]]
first[0].to_i
else
[nil, nil]
nil
end
end
end
Expand Down Expand Up @@ -165,18 +165,16 @@ def parse_routes!(data, schema, store)
schemas_data["properties"][href]["properties"][method] = schema_data
end

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

# A link need not necessarily specify a target schema.
if response_data["schema"]
target_schemas_data["properties"][href]["properties"][method] = response_data["schema"]
end
target_schemas_data["properties"][href]["properties"][method]["properties"][status] = response_data["schema"]
end

link.status_success = find_best_fit_response(link_data)

rx = %r{^#{href_to_regex(link.href)}$}
Committee.log_debug "Created route: #{link.method} #{link.href} (regex #{rx})"

Expand Down Expand Up @@ -206,7 +204,10 @@ def parse_routes!(data, schema, store)
end

# response
link.target_schema = target_schemas.properties[link.href].properties[method]
link.target_schemas = {}
target_schemas.properties[link.href].properties[method].properties.each do |status, schema|
link.target_schemas[status] = schema
end
end
end

Expand Down
9 changes: 8 additions & 1 deletion lib/committee/drivers/open_api_2/link.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ class Link

# The link's output schema. i.e. How we validate an endpoint's response
# data.
attr_accessor :target_schema
attr_accessor :target_schemas

attr_accessor :header_schema

def rel
raise "Committee: rel not implemented for OpenAPI"
end

def target_schema
target_schemas[status_success] ||
target_schemas[200] ||
target_schemas[201] ||
target_schemas.values.first
end
end
end
end
Expand Down
18 changes: 14 additions & 4 deletions lib/committee/schema_validator/hyper_schema/response_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ def initialize(link, options = {})
@validate_success_only = options[:validate_success_only]
@allow_blank_structures = options[:allow_blank_structures]

@validator = JsonSchema::Validator.new(target_schema(link))
@validators = {}
if link.is_a? Drivers::OpenAPI2::Link
link.target_schemas.each do |status, schema|
@validators[status] = JsonSchema::Validator.new(target_schema(link))
end
else
@validators[link.status_success] = JsonSchema::Validator.new(target_schema(link))
end
end

def call(status, headers, data)
Expand Down Expand Up @@ -45,9 +52,12 @@ def call(status, headers, data)
end

begin
if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
raise InvalidResponse, "Invalid response.\n\n#{errors}"
if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
raise InvalidResponse, "Invalid response.#{@link.href} status code #{status} definition does not exist" if @validators[status].nil?
if !@validators[status].validate(data)
errors = JsonSchema::SchemaError.aggregate(@validators[status].errors).join("\n")
raise InvalidResponse, "Invalid response.\n\n#{errors}"
end
end
rescue => e
raise InvalidResponse, "Invalid response.\n\nschema is undefined" if /undefined method .all_of. for nil/ =~ e.message
Expand Down
2 changes: 1 addition & 1 deletion test/drivers/open_api_2/link_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@link.method = "GET"
@link.status_success = 200
@link.schema = { "title" => "input" }
@link.target_schema = { "title" => "target" }
@link.target_schemas = { 200 => { "title" => "target" } }
end

it "uses set #enc_type" do
Expand Down
38 changes: 22 additions & 16 deletions test/schema_validator/hyper_schema/response_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,62 +74,68 @@

it "generates first enum value for a schema with enum" do
link = Committee::Drivers::OpenAPI2::Link.new
link.target_schema = JsonSchema::Schema.new
link.target_schema.enum = ["foo"]
link.target_schema.type = ["string"]
target_schema = JsonSchema::Schema.new
target_schema.enum = ["foo"]
target_schema.type = ["string"]
link.target_schemas = { 200 => target_schema }
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal("foo", data)
end

it "generates basic types" do
link = Committee::Drivers::OpenAPI2::Link.new
link.target_schema = JsonSchema::Schema.new
target_schema = JsonSchema::Schema.new
link.target_schemas = { 200 => target_schema }

link.target_schema.type = ["integer"]
target_schema.type = ["integer"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal 0, data

link.target_schema.type = ["null"]
target_schema.type = ["null"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_nil data

link.target_schema.type = ["string"]
target_schema.type = ["string"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal "", data
end

it "generates an empty array for an array type" do
link = Committee::Drivers::OpenAPI2::Link.new
link.target_schema = JsonSchema::Schema.new
link.target_schema.type = ["array"]
target_schema = JsonSchema::Schema.new
link.target_schemas = { 200 => target_schema }
target_schema.type = ["array"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal([], data)
end

it "generates an empty object for an object with no fields" do
link = Committee::Drivers::OpenAPI2::Link.new
link.target_schema = JsonSchema::Schema.new
link.target_schema.type = ["object"]
target_schema = JsonSchema::Schema.new
link.target_schemas = { 200 => target_schema }
target_schema.type = ["object"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal({}, data)
end

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

link.target_schema.data = { "example" => 123 }
link.target_schema.type = ["integer"]
target_schema.data = { "example" => 123 }
target_schema.type = ["integer"]

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

it "prefers non-null types to null types" do
link = Committee::Drivers::OpenAPI2::Link.new
link.target_schema = JsonSchema::Schema.new
target_schema = JsonSchema::Schema.new
link.target_schemas = { 200 => target_schema }

link.target_schema.type = ["null", "integer"]
target_schema.type = ["null", "integer"]
data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
assert_equal 0, data
end
Expand Down

0 comments on commit f591008

Please sign in to comment.