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
RSpec.configure do |config|
config.swagger_root = Rails.root.to_s + '/swagger'
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
@ -224,25 +224,66 @@ describe 'Blogs API' do
path '/blogs' do
post 'Creates a blog' do
response 422, 'invalid request' do
schema '$ref' => '#/definitions/errors_object'
...
end
# 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
schema '$ref' => '#/definitions/errors_object'
...
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 ###
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.
### 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:
@ -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.
### 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:

View File

@ -25,7 +25,7 @@ module Rswag
# functionality while also setting the appropriate metadata if applicable
def description(value=nil)
return super() if value.nil?
metadata[:operation][:description] = value
metadata[:operation][:description] = value
end
# These are array properties - note the splat operator
@ -61,6 +61,14 @@ module Rswag
metadata[:response][:headers][name] = attributes
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!
# NOTE: rspec 2.x support
if RSPEC_VERSION < 3

View File

@ -13,6 +13,7 @@ module Rswag
def validate!(response)
validate_code!(response.code)
validate_headers!(response.headers)
validate_body!(response.body)
end
@ -24,6 +25,20 @@ module Rswag
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)
response_schema = @api_metadata[:response][:schema]
return if response_schema.nil?
@ -34,7 +49,7 @@ module Rswag
.merge(@global_metadata.slice(:definitions))
JSON::Validator.validate!(validation_schema, body)
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

View File

@ -113,7 +113,7 @@ module Rswag
context "'path' parameter" do
before { subject.parameter(name: :id, in: :path) }
let(:api_metadata) { { operation: {} } }
it "automatically sets the 'required' flag" do
expect(api_metadata[:operation][:parameters]).to match(
[ name: :id, in: :path, required: true ]
@ -151,6 +151,25 @@ module Rswag
)
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

View File

@ -66,6 +66,43 @@ module Rswag
it { expect { call }.to raise_error UnexpectedResponse }
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

View File

@ -17,6 +17,10 @@ class BlogsController < ApplicationController
# GET /blogs/1
def show
@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
end

View File

@ -50,8 +50,18 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
produces 'application/json'
response '200', 'blog found' do
header 'ETag', type: :string
header 'Last-Modified', type: :string
header 'Cache-Control', type: :string
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(:id) { blog.id }
run_test!

View File

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