From 31a7e806f23f686dda6ed0ac7a0468b8d50d5f07 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Tue, 26 Nov 2024 19:11:49 +0100 Subject: [PATCH] Support multiple response schemas for OpenAPI 2 --- lib/committee/drivers/open_api_2/driver.rb | 33 ++++++++-------- lib/committee/drivers/open_api_2/link.rb | 9 ++++- .../hyper_schema/response_validator.rb | 18 +++++++-- test/drivers/open_api_2/link_test.rb | 2 +- .../hyper_schema/response_generator_test.rb | 38 +++++++++++-------- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/lib/committee/drivers/open_api_2/driver.rb b/lib/committee/drivers/open_api_2/driver.rb index 9a29ae17..ad378554 100644 --- a/lib/committee/drivers/open_api_2/driver.rb +++ b/lib/committee/drivers/open_api_2/driver.rb @@ -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 @@ -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})" @@ -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 diff --git a/lib/committee/drivers/open_api_2/link.rb b/lib/committee/drivers/open_api_2/link.rb index b9888de6..344af372 100644 --- a/lib/committee/drivers/open_api_2/link.rb +++ b/lib/committee/drivers/open_api_2/link.rb @@ -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 diff --git a/lib/committee/schema_validator/hyper_schema/response_validator.rb b/lib/committee/schema_validator/hyper_schema/response_validator.rb index d396219c..fc053eb0 100644 --- a/lib/committee/schema_validator/hyper_schema/response_validator.rb +++ b/lib/committee/schema_validator/hyper_schema/response_validator.rb @@ -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) @@ -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 diff --git a/test/drivers/open_api_2/link_test.rb b/test/drivers/open_api_2/link_test.rb index 3b210a73..fff785f7 100644 --- a/test/drivers/open_api_2/link_test.rb +++ b/test/drivers/open_api_2/link_test.rb @@ -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 diff --git a/test/schema_validator/hyper_schema/response_generator_test.rb b/test/schema_validator/hyper_schema/response_generator_test.rb index d8e5d84b..a23b0f75 100644 --- a/test/schema_validator/hyper_schema/response_generator_test.rb +++ b/test/schema_validator/hyper_schema/response_generator_test.rb @@ -74,52 +74,57 @@ 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 @@ -127,9 +132,10 @@ 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