Fixes response_validator to handle 3.0 responses and validate against the schema.

JSON::Validator already handles anyOf oneOf schema definitions, so those can be passed in and validation errors are returned properly
This commit is contained in:
Jay Danielian
2019-07-20 14:33:51 -04:00
parent cd348b53f8
commit 4c2097e017
6 changed files with 52 additions and 21 deletions

View File

@@ -55,7 +55,15 @@ module Rswag
def request_body(attributes) def request_body(attributes)
# can make this generic, and accept any incoming hash (like parameter method) # can make this generic, and accept any incoming hash (like parameter method)
attributes.compact! attributes.compact!
metadata[:operation][:requestBody] = attributes
if metadata[:operation][:requestBody].blank?
metadata[:operation][:requestBody] = attributes
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
# merge in
content_hash = metadata[:operation][:requestBody][:content]
incoming_content_hash = attributes[:content]
content_hash.merge!(incoming_content_hash) if incoming_content_hash
end
end end
def request_body_json(schema:, required: true, description: nil, examples: nil) def request_body_json(schema:, required: true, description: nil, examples: nil)
@@ -77,6 +85,18 @@ module Rswag
end end
end end
def request_body_text_plain(required: false, description: nil, examples: nil)
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
# TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
def request_body_xml(schema:,required: false, description: nil, examples: nil)
passed_examples = Array(examples)
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
def request_body_multipart(schema:, description: nil) def request_body_multipart(schema:, description: nil)
content_hash = { 'multipart/form-data' => { schema: schema }} content_hash = { 'multipart/form-data' => { schema: schema }}
request_body(description: description, content: content_hash) request_body(description: description, content: content_hash)

View File

@@ -39,19 +39,25 @@ module Rswag
end end
def validate_body!(metadata, swagger_doc, body) def validate_body!(metadata, swagger_doc, body)
response_schema = metadata[:response][:schema] test_schemas = extract_schemas(metadata)
return if response_schema.nil? return if test_schemas.nil?
components = swagger_doc[:components] || {} components = swagger_doc[:components] || {}
components_schemas = { components: { schemas: components[:schemas] } } components_schemas = { components: { schemas: components[:schemas] } }
validation_schema = response_schema validation_schema = test_schemas[:schema] # response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(components_schemas) .merge(components_schemas)
errors = JSON::Validator.fully_validate(validation_schema, body) errors = JSON::Validator.fully_validate(validation_schema, body)
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
end end
def extract_schemas(metadata)
produces = Array(metadata[:operation][:produces])
response_content = metadata[:response][:content] || {}
producer_content = produces.first || 'application/json'
response_content[producer_content]
end
end end
class UnexpectedResponse < StandardError; end class UnexpectedResponse < StandardError; end

View File

@@ -15,7 +15,6 @@ class BlogsController < ApplicationController
# request body definition for 3.0 # request body definition for 3.0
blog_params = params.require(:blog).permit(:title, :content, :headline, :text) blog_params = params.require(:blog).permit(:title, :content, :headline, :text)
@blog = Blog.create(blog_params) @blog = Blog.create(blog_params)
respond_with @blog respond_with @blog
end end

View File

@@ -74,7 +74,7 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } } let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } }
response '201', 'flexible blog created' do response '201', 'flexible blog created' do
schema '$ref' => '#/components/schemas/blog' schema :oneOf => [{'$ref' => '#/components/schemas/blog'},{'$ref' => '#/components/schemas/flexible_blog'}]
run_test! run_test!
end end
end end

View File

@@ -54,9 +54,9 @@ RSpec.configure do |config|
id: { type: 'integer' }, id: { type: 'integer' },
title: { type: 'string' }, title: { type: 'string' },
content: { type: 'string', nullable: true }, content: { type: 'string', nullable: true },
thumbnail: { type: 'string' } thumbnail: { type: 'string', nullable: true }
}, },
required: %w[id title content thumbnail] required: %w[id title]
}, },
flexible_blog: { flexible_blog: {
type: 'object', type: 'object',
@@ -64,9 +64,9 @@ RSpec.configure do |config|
id: { type: 'integer' }, id: { type: 'integer' },
headline: { type: 'string' }, headline: { type: 'string' },
text: { type: 'string', nullable: true }, text: { type: 'string', nullable: true },
thumbnail: { type: 'string' } thumbnail: { type: 'string', nullable:true }
}, },
required: %w[id headline thumbnail] required: %w[id headline]
} }
}, },
securitySchemes: { securitySchemes: {

View File

@@ -243,7 +243,14 @@
"thumbnail": null "thumbnail": null
}, },
"schema": { "schema": {
"$ref": "#/components/schemas/blog" "oneOf": [
{
"$ref": "#/components/schemas/blog"
},
{
"$ref": "#/components/schemas/flexible_blog"
}
]
} }
} }
} }
@@ -395,14 +402,13 @@
"nullable": true "nullable": true
}, },
"thumbnail": { "thumbnail": {
"type": "string" "type": "string",
"nullable": true
} }
}, },
"required": [ "required": [
"id", "id",
"title", "title"
"content",
"thumbnail"
] ]
}, },
"flexible_blog": { "flexible_blog": {
@@ -419,13 +425,13 @@
"nullable": true "nullable": true
}, },
"thumbnail": { "thumbnail": {
"type": "string" "type": "string",
"nullable": true
} }
}, },
"required": [ "required": [
"id", "id",
"headline", "headline"
"thumbnail"
] ]
} }
}, },