From 10dd37896f7a587960afbd7be8fa00fa74e84485 Mon Sep 17 00:00:00 2001 From: vinhbachsy Date: Tue, 18 Oct 2016 21:44:04 +0800 Subject: [PATCH 1/4] Support setting examples for response Add helper method `response_examples` to inject response examples to swagger --- rswag-specs/lib/rswag/specs/example_group_helpers.rb | 6 +++++- test-app/spec/integration/blogs_spec.rb | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb index 3024b15..6e5d243 100644 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb @@ -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,10 @@ module Rswag metadata[:response][:headers][name] = attributes end + def response_examples(example) + metadata[:response][:examples] = example + end + def run_test! # NOTE: rspec 2.x support if RSPEC_VERSION < 3 diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 15fe8a9..588ddb8 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -52,6 +52,12 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do response '200', 'blog found' do schema '$ref' => '#/definitions/blog' + response_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! From 5cf376891a9cdd913fea683bbdd0dc3642958950 Mon Sep 17 00:00:00 2001 From: vinhbachsy Date: Tue, 18 Oct 2016 21:45:55 +0800 Subject: [PATCH 2/4] Validate response headers based on specified header Add validate_headers step in response validator. Using JSON::Validator with validate header value with swagger header object. --- .../lib/rswag/specs/response_validator.rb | 17 ++++++++- .../rswag/specs/response_validator_spec.rb | 37 +++++++++++++++++++ test-app/app/controllers/blogs_controller.rb | 4 ++ test-app/spec/integration/blogs_spec.rb | 4 ++ test-app/swagger/v1/swagger.json | 18 +++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb index 7a04c37..3ee2266 100644 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ b/rswag-specs/lib/rswag/specs/response_validator.rb @@ -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 diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index c914264..e1d9d3f 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -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 diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb index dd1aca3..261d718 100644 --- a/test-app/app/controllers/blogs_controller.rb +++ b/test-app/app/controllers/blogs_controller.rb @@ -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 diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 588ddb8..7a49ce9 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -50,6 +50,10 @@ 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' response_examples 'application/json' => { diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index e2e0311..acbcd1e 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -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": { From 5ea97a42787a6d828131535009f0ba7dc47d9cf2 Mon Sep 17 00:00:00 2001 From: vinhbachsy Date: Tue, 18 Oct 2016 21:46:23 +0800 Subject: [PATCH 3/4] Update README for response headers and examples --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 15edb51..a7d95d3 100644 --- a/README.md +++ b/README.md @@ -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 `response_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 + response_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: From 0b0acfe4c7d956c53802d57f8442dfbab19794ea Mon Sep 17 00:00:00 2001 From: vinhbachsy Date: Wed, 19 Oct 2016 03:04:03 +0800 Subject: [PATCH 4/4] Rename response_examples to examples for consistent DSL Special handling `examples` invocation with no parameters to avoid overriding the `examples` method of rspec-core ExampleGroup --- README.md | 4 ++-- .../lib/rswag/specs/example_group_helpers.rb | 6 +++++- .../rswag/specs/example_group_helpers_spec.rb | 21 ++++++++++++++++++- test-app/spec/integration/blogs_spec.rb | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a7d95d3..73eeb2f 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ end ### Response examples ### -You can provide custom response examples to the generated swagger file by calling the method `response_examples` inside the response block: +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 @@ -275,7 +275,7 @@ describe 'Blogs API' do get 'Retrieves a blog' do response 200, 'blog found' do - response_examples 'application/json' => { + examples 'application/json' => { id: 1, title: 'Hello world!', content: '...' diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb index 6e5d243..cb96e71 100644 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb @@ -61,7 +61,11 @@ module Rswag metadata[:response][:headers][name] = attributes end - def response_examples(example) + # 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 diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb index 76c3278..96d08b0 100644 --- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb @@ -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 diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 7a49ce9..0ae884c 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -56,7 +56,7 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do schema '$ref' => '#/definitions/blog' - response_examples 'application/json' => { + examples 'application/json' => { id: 1, title: 'Hello world!', content: 'Hello world and hello universe. Thank you all very much!!!'