Merge pull request #32 from vinh0604/master

Validating response headers and setting response examples
This commit is contained in:
domaindrivendev 2016-10-19 00:09:48 -07:00 committed by GitHub
commit 17a6cd13c4
8 changed files with 161 additions and 9 deletions

View File

@ -127,7 +127,7 @@ In addition to paths, operations and responses, Swagger also supports global API
# spec/swagger_helper.rb # spec/swagger_helper.rb
RSpec.configure do |config| RSpec.configure do |config|
config.swagger_root = Rails.root.to_s + '/swagger' config.swagger_root = Rails.root.to_s + '/swagger'
config.swagger_docs = { config.swagger_docs = {
'v1/swagger.json' => { 'v1/swagger.json' => {
swagger: '2.0', swagger: '2.0',
@ -224,25 +224,66 @@ describe 'Blogs API' do
path '/blogs' do path '/blogs' do
post 'Creates a blog' do post 'Creates a blog' do
response 422, 'invalid request' do response 422, 'invalid request' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/definitions/errors_object'
... ...
end end
# spec/integration/comments_spec.rb # spec/integration/comments_spec.rb
describe 'Blogs API' do describe 'Blogs API' do
path '/blogs/{blog_id}/comments' do path '/blogs/{blog_id}/comments' do
post 'Creates a comment' do post 'Creates a comment' do
response 422, 'invalid request' do response 422, 'invalid request' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/definitions/errors_object'
... ...
end end
``` ```
### Response headers ###
In Rswag, you could use `header` method inside the response block to specify header objects for this response. Rswag will validate your response headers with those header objects and inject them into the generated swagger file:
```ruby
# spec/integration/comments_spec.rb
describe 'Blogs API' do
path '/blogs/{blog_id}/comments' do
post 'Creates a comment' do
response 422, 'invalid request' do
header 'X-Rate-Limit-Limit', type: :integer, description: 'The number of allowed requests in the current period'
header 'X-Rate-Limit-Remaining', type: :integer, description: 'The number of remaining requests in the current period'
...
end
```
### Response examples ###
You can provide custom response examples to the generated swagger file by calling the method `examples` inside the response block:
```ruby
# spec/integration/blogs_spec.rb
describe 'Blogs API' do
path '/blogs/{blog_id}' do
get 'Retrieves a blog' do
response 200, 'blog found' do
examples 'application/json' => {
id: 1,
title: 'Hello world!',
content: '...'
}
...
end
```
### Route Prefix for Swagger JSON Endpoints ### ### Route Prefix for Swagger JSON Endpoints ###
The functionality to expose Swagger files, such as those generated by rswag-specs, as JSON endpoints is implemented as a Rails Engine. As with any Engine, you can change it's mount prefix in _routes.rb_: The functionality to expose Swagger files, such as those generated by rswag-specs, as JSON endpoints is implemented as a Rails Engine. As with any Engine, you can change it's mount prefix in _routes.rb_:
@ -273,7 +314,7 @@ end
``` ```
__NOTE__: If you're using rswag-specs to generate Swagger files, you'll want to ensure they both use the same <swagger_root>. The reason for separate settings is to maintain independence between the two gems. For example, you could install rswag-api independently and create your Swagger files manually. __NOTE__: If you're using rswag-specs to generate Swagger files, you'll want to ensure they both use the same <swagger_root>. The reason for separate settings is to maintain independence between the two gems. For example, you could install rswag-api independently and create your Swagger files manually.
### Dynamic Values for Swagger JSON ## ### Dynamic Values for Swagger JSON ##
There may be cases where you need to add dynamic values to the Swagger JSON that's returned by rswag-api. For example, you may want to provide an explicit host name. Rather than hardcoding it, you can configure a filter that's executed prior to serializing every Swagger document: There may be cases where you need to add dynamic values to the Swagger JSON that's returned by rswag-api. For example, you may want to provide an explicit host name. Rather than hardcoding it, you can configure a filter that's executed prior to serializing every Swagger document:
@ -287,7 +328,7 @@ end
``` ```
Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authoriation header and remove operations based on user permissions. Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authoriation header and remove operations based on user permissions.
### Enable Swagger Endpoints for swagger-ui ### ### Enable Swagger Endpoints for swagger-ui ###
You can update the _rswag-ui.rb_ initializer, installed with rswag-ui, to specify which Swagger endpoints should be available to power the documentation UI. If you're using rswag-api, these should correspond to the Swagger endpoints it exposes. When the UI is rendered, you'll see these listed in a drop-down to the top right of the page: You can update the _rswag-ui.rb_ initializer, installed with rswag-ui, to specify which Swagger endpoints should be available to power the documentation UI. If you're using rswag-api, these should correspond to the Swagger endpoints it exposes. When the UI is rendered, you'll see these listed in a drop-down to the top right of the page:

View File

@ -25,7 +25,7 @@ module Rswag
# functionality while also setting the appropriate metadata if applicable # functionality while also setting the appropriate metadata if applicable
def description(value=nil) def description(value=nil)
return super() if value.nil? return super() if value.nil?
metadata[:operation][:description] = value metadata[:operation][:description] = value
end end
# These are array properties - note the splat operator # These are array properties - note the splat operator
@ -61,6 +61,14 @@ module Rswag
metadata[:response][:headers][name] = attributes metadata[:response][:headers][name] = attributes
end end
# NOTE: Similar to 'description', 'examples' need to handle the case when
# being invoked with no params to avoid overriding 'examples' method of
# rspec-core ExampleGroup
def examples(example = nil)
return super() if example.nil?
metadata[:response][:examples] = example
end
def run_test! def run_test!
# NOTE: rspec 2.x support # NOTE: rspec 2.x support
if RSPEC_VERSION < 3 if RSPEC_VERSION < 3

View File

@ -13,6 +13,7 @@ module Rswag
def validate!(response) def validate!(response)
validate_code!(response.code) validate_code!(response.code)
validate_headers!(response.headers)
validate_body!(response.body) validate_body!(response.body)
end end
@ -24,6 +25,20 @@ module Rswag
end end
end end
def validate_headers!(headers)
header_schema = @api_metadata[:response][:headers]
return if header_schema.nil?
header_schema.each do |header_name, schema|
validate_header!(schema, header_name, headers[header_name.to_s])
end
end
def validate_header!(schema, header_name, header_value)
JSON::Validator.validate!(schema.merge(@global_metadata), header_value.to_json)
rescue JSON::Schema::ValidationError => ex
raise UnexpectedResponse, "Expected response headers #{header_name} to match schema: #{ex.message}"
end
def validate_body!(body) def validate_body!(body)
response_schema = @api_metadata[:response][:schema] response_schema = @api_metadata[:response][:schema]
return if response_schema.nil? return if response_schema.nil?
@ -34,7 +49,7 @@ module Rswag
.merge(@global_metadata.slice(:definitions)) .merge(@global_metadata.slice(:definitions))
JSON::Validator.validate!(validation_schema, body) JSON::Validator.validate!(validation_schema, body)
rescue JSON::Schema::ValidationError => ex rescue JSON::Schema::ValidationError => ex
raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}" raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
end end
end end
end end

View File

@ -113,7 +113,7 @@ module Rswag
context "'path' parameter" do context "'path' parameter" do
before { subject.parameter(name: :id, in: :path) } before { subject.parameter(name: :id, in: :path) }
let(:api_metadata) { { operation: {} } } let(:api_metadata) { { operation: {} } }
it "automatically sets the 'required' flag" do it "automatically sets the 'required' flag" do
expect(api_metadata[:operation][:parameters]).to match( expect(api_metadata[:operation][:parameters]).to match(
[ name: :id, in: :path, required: true ] [ name: :id, in: :path, required: true ]
@ -151,6 +151,25 @@ module Rswag
) )
end end
end end
describe '#examples(example)' do
let(:json_example) do
{
'application/json' => {
foo: 'bar'
}
}
end
let(:api_metadata) { { response: {} } }
before do
subject.examples(json_example)
end
it "adds to the 'response examples' metadata" do
expect(api_metadata[:response][:examples]).to eq(json_example)
end
end
end end
end end
end end

View File

@ -66,6 +66,43 @@ module Rswag
it { expect { call }.to raise_error UnexpectedResponse } it { expect { call }.to raise_error UnexpectedResponse }
end end
end end
context "'headers' provided" do
before do
api_metadata[:response][:headers] = {
'X-Rate-Limit-Limit' => {
description: 'The number of allowed requests in the current period',
type: 'integer'
},
'X-Rate-Limit-Remaining' => {
description: 'The number of remaining requests in the current period',
type: 'integer'
},
'X-Rate-Limit-Reset' => {
description: 'The number of seconds left in the current period',
type: 'integer'
}
}
end
context 'response code & body matches' do
let(:response) { OpenStruct.new(code: 200, body: '{}', headers: {
'X-Rate-Limit-Limit' => 1,
'X-Rate-Limit-Remaining' => 1,
'X-Rate-Limit-Reset' => 1
}) }
it { expect { call }.to_not raise_error }
end
context 'response code matches & body does not' do
let(:response) { OpenStruct.new(code: 200, body: '{}', headers: {
'X-Rate-Limit-Limit' => 'invalid',
'X-Rate-Limit-Remaining' => 'invalid',
'X-Rate-Limit-Reset' => 'invalid'
}) }
it { expect { call }.to raise_error UnexpectedResponse }
end
end
end end
end end
end end

View File

@ -17,6 +17,10 @@ class BlogsController < ApplicationController
# GET /blogs/1 # GET /blogs/1
def show def show
@blog = Blog.find_by_id(params[:id]) @blog = Blog.find_by_id(params[:id])
fresh_when(@blog)
return unless stale?(@blog)
respond_with @blog, status: :not_found and return unless @blog respond_with @blog, status: :not_found and return unless @blog
respond_with @blog respond_with @blog
end end

View File

@ -50,8 +50,18 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
produces 'application/json' produces 'application/json'
response '200', 'blog found' do response '200', 'blog found' do
header 'ETag', type: :string
header 'Last-Modified', type: :string
header 'Cache-Control', type: :string
schema '$ref' => '#/definitions/blog' schema '$ref' => '#/definitions/blog'
examples 'application/json' => {
id: 1,
title: 'Hello world!',
content: 'Hello world and hello universe. Thank you all very much!!!'
}
let(:blog) { Blog.create(title: 'foo', content: 'bar') } let(:blog) { Blog.create(title: 'foo', content: 'bar') }
let(:id) { blog.id } let(:id) { blog.id }
run_test! run_test!

View File

@ -89,8 +89,26 @@
"responses": { "responses": {
"200": { "200": {
"description": "blog found", "description": "blog found",
"headers": {
"ETag": {
"type": "string"
},
"Last-Modified": {
"type": "string"
},
"Cache-Control": {
"type": "string"
}
},
"schema": { "schema": {
"$ref": "#/definitions/blog" "$ref": "#/definitions/blog"
},
"examples": {
"application/json": {
"id": 1,
"title": "Hello world!",
"content": "Hello world and hello universe. Thank you all very much!!!"
}
} }
}, },
"404": { "404": {