Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple response schemas for OpenAPI 2 #433

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions lib/committee/drivers/open_api_2/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,18 @@ 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 @@ -174,19 +174,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

# 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"=> {}}
link_data["responses"].each do |key, response_data|
status = key.to_i
next unless response_data["schema"]

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 @@ -218,8 +215,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
Comment on lines +34 to +39
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I already call find_best_fit_response in the driver, I'm not sure this is needed here. Maybe just calling target_schemas[status_success] would be enough?
This is mostly for compatibility with hyper schema anyway.

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
Loading