From 609484f0147ad69a96b69784d4c0db3044a2d5de 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 | 29 +++++++------- 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, 60 insertions(+), 36 deletions(-) diff --git a/lib/committee/drivers/open_api_2/driver.rb b/lib/committee/drivers/open_api_2/driver.rb index f053b5fa..24053c07 100644 --- a/lib/committee/drivers/open_api_2/driver.rb +++ b/lib/committee/drivers/open_api_2/driver.rb @@ -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 @@ -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})" @@ -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 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 cc368a42..ea16ebc0 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 4b0496fa..5a3eb7d6 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 3de348c4..4e87df8c 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