25 Commits

Author SHA1 Message Date
richie
77d4cbe0ea Prep for v1.2.0 release 2017-01-08 07:51:24 -08:00
domaindrivendev
32a7ab8234 Merge pull request #40 from PavelBezpalov/master
Add missing in production assets
2017-01-05 08:16:10 -08:00
Pavel Bezpalov
95b009a72f Add missing in production assets 2017-01-04 16:38:12 +02:00
richie
471dff5e34 Prep for v1.1.0 release 2016-11-08 12:43:31 -08:00
richie
99be8135f7 Wire up capybara & add simple feature spec for swagger-ui 2016-11-08 12:38:51 -08:00
richie
8315eda8b2 Update ui to use digest assets in prod 2016-11-02 22:15:09 -07:00
domaindrivendev
e1fe9f3239 Merge pull request #33 from vinh0604/master
Simplify response header validation
2016-10-19 17:15:50 -07:00
vinhbachsy
64b0de494f Simplify response header validation
Change to checks for the presence of required headers instead of using 
JSON::Validator
2016-10-20 01:33:43 +08:00
domaindrivendev
9d4069bcfe A minor readme update 2016-10-19 00:37:06 -07:00
domaindrivendev
17a6cd13c4 Merge pull request #32 from vinh0604/master
Validating response headers and setting response examples
2016-10-19 00:09:48 -07:00
vinhbachsy
0b0acfe4c7 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
2016-10-19 03:04:03 +08:00
vinhbachsy
5ea97a4278 Update README for response headers and examples 2016-10-18 21:46:35 +08:00
vinhbachsy
5cf376891a 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.
2016-10-18 21:46:35 +08:00
vinhbachsy
10dd37896f Support setting examples for response
Add helper method `response_examples` to inject response examples to swagger
2016-10-18 21:46:35 +08:00
richie
3506fee3d0 Prep for 1.0.3 release 2016-10-17 14:50:57 -07:00
richie
5df130922f Support x-nullable in respone_validator 2016-10-17 14:47:11 -07:00
richie
5a19cd2373 Push latest Gemfile.lock 2016-10-14 17:50:42 -07:00
richie
7025ec0063 Prep for v1.0.2 release 2016-10-14 17:48:16 -07:00
richie
23f4120fe3 Parse defined security requirements correctly 2016-10-14 17:44:27 -07:00
richie
312f68ae72 Rename ExampleHelpers.config - appeared to be conflicting in Rails 4+ 2016-10-14 09:55:55 -07:00
richie
63e0e53104 Merge branch 'path-item-parameters' 2016-10-13 18:17:30 -07:00
richie
4d675056c1 Support parameters provided at the 'path' level 2016-10-13 18:17:19 -07:00
richie
de09df59e1 Move path metadata into 'path_item' hash 2016-10-13 17:23:31 -07:00
richie
b81b2927be More useful error messages in rswag-specs 2016-10-13 16:56:44 -07:00
richie
f1850bc6d0 Ensure Gemfile.lock is up-to-date 2016-10-12 14:16:29 -07:00
36 changed files with 639 additions and 243 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
**/*/log **/*/log
**/*/*.gem **/*/*.gem
**/*/*.sqlite3 **/*/*.sqlite3
**/*/public/assets

View File

@@ -7,4 +7,8 @@ env:
- "RAILS_VERSION=5.0.0" - "RAILS_VERSION=5.0.0"
cache: bundler cache: bundler
install: bundle update install: bundle update
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3 # give xvfb some time to start
script: ./ci/test.sh script: ./ci/test.sh

View File

@@ -25,5 +25,12 @@ group :test do
gem 'test-unit' gem 'test-unit'
gem 'rspec-rails' gem 'rspec-rails'
gem 'generator_spec' gem 'generator_spec'
gem 'capybara'
gem 'capybara-webkit'
gem 'rswag-specs', path: './rswag-specs' gem 'rswag-specs', path: './rswag-specs'
end end
group :assets do
gem 'uglifier'
gem 'therubyracer'
end

View File

@@ -1,13 +1,13 @@
PATH PATH
remote: ./rswag-api remote: ./rswag-api
specs: specs:
rswag-api (1.0.0) rswag-api (1.2.0)
rails (>= 3.1, < 5.1) rails (>= 3.1, < 5.1)
PATH PATH
remote: ./rswag-specs remote: ./rswag-specs
specs: specs:
rswag-specs (1.0.0) rswag-specs (1.2.0)
json-schema (~> 2.2) json-schema (~> 2.2)
rails (>= 3.1, < 5.1) rails (>= 3.1, < 5.1)
rspec-rails (>= 2.14, < 4) rspec-rails (>= 2.14, < 4)
@@ -15,7 +15,7 @@ PATH
PATH PATH
remote: ./rswag-ui remote: ./rswag-ui
specs: specs:
rswag-ui (1.0.0) rswag-ui (1.2.0)
rails (>= 3.1, < 5.1) rails (>= 3.1, < 5.1)
GEM GEM
@@ -51,8 +51,19 @@ GEM
addressable (2.4.0) addressable (2.4.0)
arel (3.0.3) arel (3.0.3)
builder (3.0.4) builder (3.0.4)
capybara (2.10.1)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara-webkit (1.1.0)
capybara (~> 2.0, >= 2.0.2)
json
diff-lcs (1.2.5) diff-lcs (1.2.5)
erubis (2.7.0) erubis (2.7.0)
execjs (2.7.0)
generator_spec (0.9.3) generator_spec (0.9.3)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
railties (>= 3.0.0) railties (>= 3.0.0)
@@ -62,11 +73,15 @@ GEM
json (1.8.3) json (1.8.3)
json-schema (2.7.0) json-schema (2.7.0)
addressable (>= 2.4) addressable (>= 2.4)
libv8 (3.16.14.15)
mail (2.5.4) mail (2.5.4)
mime-types (~> 1.16) mime-types (~> 1.16)
treetop (~> 1.4.8) treetop (~> 1.4.8)
mime-types (1.25.1) mime-types (1.25.1)
mini_portile2 (2.1.0)
multi_json (1.12.1) multi_json (1.12.1)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
polyglot (0.3.5) polyglot (0.3.5)
power_assert (0.3.1) power_assert (0.3.1)
rack (1.4.7) rack (1.4.7)
@@ -94,6 +109,7 @@ GEM
rake (11.3.0) rake (11.3.0)
rdoc (3.12.2) rdoc (3.12.2)
json (~> 1.4) json (~> 1.4)
ref (2.0.0)
rspec-core (3.5.4) rspec-core (3.5.4)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-expectations (3.5.0) rspec-expectations (3.5.0)
@@ -124,17 +140,26 @@ GEM
railties (~> 3.0) railties (~> 3.0)
test-unit (3.2.1) test-unit (3.2.1)
power_assert power_assert
therubyracer (0.12.2)
libv8 (~> 3.16.14.0)
ref
thor (0.19.1) thor (0.19.1)
tilt (1.4.1) tilt (1.4.1)
treetop (1.4.15) treetop (1.4.15)
polyglot polyglot
polyglot (>= 0.3.1) polyglot (>= 0.3.1)
tzinfo (0.3.51) tzinfo (0.3.51)
uglifier (3.0.2)
execjs (>= 0.3.0, < 3)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
capybara
capybara-webkit
generator_spec generator_spec
rails (= 3.2.22) rails (= 3.2.22)
rspec-rails rspec-rails
@@ -144,3 +169,5 @@ DEPENDENCIES
sqlite3 sqlite3
strong_parameters strong_parameters
test-unit test-unit
therubyracer
uglifier

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',
@@ -171,8 +171,8 @@ The steps described above will get you up and running with minimal setup. Howeve
|Gem|Description|Added/Updated| |Gem|Description|Added/Updated|
|---------|-----------|-------------| |---------|-----------|-------------|
|__rswag-specs__|Swagger-based DSL for rspec & accompanying rake task for generating Swagger files|_spec/swagger_helper.rb_| |__rswag-specs__|Swagger-based DSL for rspec & accompanying rake task for generating Swagger files|_spec/swagger_helper.rb_|
|__rswag-api__ |Rails Engine that exposes the Swagger files as JSON endpoints|_config/initializers/rswag-api.rb, config/routes.rb_| |__rswag-api__ |Rails Engine that exposes your Swagger files as JSON endpoints|_config/initializers/rswag-api.rb, config/routes.rb_|
|__rswag-ui__ |Rails Engine that includes [swagger-ui](https://github.com/swagger-api/swagger-ui) and powers it from the Swagger endpoints|_config/initializers/rswag-ui.rb, config/routes.rb_| |__rswag-ui__ |Rails Engine that includes [swagger-ui](https://github.com/swagger-api/swagger-ui) and powers it from your Swagger endpoints|_config/initializers/rswag-ui.rb, config/routes.rb_|
### Output Location for Generated Swagger Files ### ### Output Location for Generated Swagger Files ###
@@ -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 &lt;swagger_root&gt;. 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 &lt;swagger_root&gt;. 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

@@ -1,5 +1,5 @@
module Rswag module Rswag
module Api module Api
VERSION = '1.0.1' VERSION = '1.2.0'
end end
end end

View File

@@ -2,6 +2,7 @@ require 'rspec/core'
require 'rswag/specs/version' require 'rswag/specs/version'
require 'rswag/specs/example_group_helpers' require 'rswag/specs/example_group_helpers'
require 'rswag/specs/example_helpers' require 'rswag/specs/example_helpers'
require 'rswag/specs/configuration'
require 'rswag/specs/railtie' if defined?(Rails::Railtie) require 'rswag/specs/railtie' if defined?(Rails::Railtie)
module Rswag module Rswag
@@ -15,6 +16,10 @@ module Rswag
c.include ExampleHelpers, type: :request c.include ExampleHelpers, type: :request
end end
def self.config
@config ||= Configuration.new(RSpec.configuration)
end
# Support Rails 3+ and RSpec 2+ (sigh!) # Support Rails 3+ and RSpec 2+ (sigh!)
RAILS_VERSION = Rails::VERSION::MAJOR RAILS_VERSION = Rails::VERSION::MAJOR
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i

View File

@@ -0,0 +1,37 @@
module Rswag
module Specs
class Configuration
def initialize(rspec_config)
@rspec_config = rspec_config
end
def swagger_root
@swagger_root ||= begin
if @rspec_config.swagger_root.nil?
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
end
@rspec_config.swagger_root
end
end
def swagger_docs
@swagger_docs ||= begin
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
end
@rspec_config.swagger_docs
end
end
def get_swagger_doc(name)
return swagger_docs.values.first if name.nil?
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
swagger_docs[name]
end
end
class ConfigurationError < StandardError; end
end
end

View File

@@ -2,9 +2,9 @@ module Rswag
module Specs module Specs
module ExampleGroupHelpers module ExampleGroupHelpers
def path(path, &block) def path(template, &block)
api_metadata = { path: path} api_metadata = { path_item: { template: template } }
describe(path, api_metadata, &block) describe(template, api_metadata, &block)
end end
[ :get, :post, :patch, :put, :delete, :head ].each do |verb| [ :get, :post, :patch, :put, :delete, :head ].each do |verb|
@@ -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
@@ -37,8 +37,14 @@ module Rswag
def parameter(attributes) def parameter(attributes)
attributes[:required] = true if attributes[:in].to_sym == :path attributes[:required] = true if attributes[:in].to_sym == :path
metadata[:operation][:parameters] ||= []
metadata[:operation][:parameters] << attributes if metadata.has_key?(:operation)
metadata[:operation][:parameters] ||= []
metadata[:operation][:parameters] << attributes
else
metadata[:path_item][:parameters] ||= []
metadata[:path_item][:parameters] << attributes
end
end end
def response(code, description, &block) def response(code, description, &block)
@@ -55,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

@@ -6,7 +6,8 @@ module Rswag
module ExampleHelpers module ExampleHelpers
def submit_request(api_metadata) def submit_request(api_metadata)
factory = RequestFactory.new(api_metadata, global_metadata(api_metadata[:swagger_doc])) global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc])
factory = RequestFactory.new(api_metadata, global_metadata)
if RAILS_VERSION < 5 if RAILS_VERSION < 5
send( send(
@@ -28,15 +29,15 @@ module Rswag
end end
def assert_response_matches_metadata(api_metadata) def assert_response_matches_metadata(api_metadata)
validator = ResponseValidator.new(api_metadata, global_metadata(api_metadata[:swagger_doc])) global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc])
validator = ResponseValidator.new(api_metadata, global_metadata)
validator.validate!(response) validator.validate!(response)
end end
private private
def global_metadata(swagger_doc) def rswag_config
swagger_docs = ::RSpec.configuration.swagger_docs ::Rswag::Specs.config
swagger_doc.nil? ? swagger_docs.values.first : swagger_docs[swagger_doc]
end end
end end
end end

View File

@@ -0,0 +1,25 @@
require 'json-schema'
module Rswag
module Specs
class ExtendedSchema < JSON::Schema::Validator
def initialize
super
extend_schema_definition("http://json-schema.org/draft-04/schema#")
@attributes['type'] = ExtendedTypeAttribute
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
end
end
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
def self.validate(current_schema, data, fragments, processor, validator, options={})
return if data.nil? && current_schema.schema['x-nullable'] == true
super
end
end
JSON::Validator.register_validator(ExtendedSchema.new)
end
end

View File

@@ -11,7 +11,7 @@ module Rswag
end end
def build_fullpath(example) def build_fullpath(example)
@api_metadata[:path].dup.tap do |t| @api_metadata[:path_item][:template].dup.tap do |t|
t.prepend(@global_metadata[:basePath] || '') t.prepend(@global_metadata[:basePath] || '')
parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) } parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
t.concat(build_query_string(example)) t.concat(build_query_string(example))
@@ -44,7 +44,13 @@ module Rswag
private private
def parameters_in(location) def parameters_in(location)
(@api_metadata[:operation][:parameters] || []) path_item_params = @api_metadata[:path_item][:parameters] || []
operation_params = @api_metadata[:operation][:parameters] || []
applicable_params = operation_params
.concat(path_item_params)
.uniq { |p| p[:name] } # operation params should override path_item params
applicable_params
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references .map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
.concat(resolve_api_key_parameters) .concat(resolve_api_key_parameters)
.select { |p| p[:in] == location } .select { |p| p[:in] == location }
@@ -59,9 +65,13 @@ module Rswag
def resolve_api_key_parameters def resolve_api_key_parameters
@api_key_params ||= begin @api_key_params ||= begin
global_requirements = (@global_metadata[:security] || {}) # First figure out the security requirement applicable to the operation
requirements = global_requirements.merge(@api_metadata[:operation][:security] || {}) global_requirements = (@global_metadata[:security] || [] ).map { |r| r.keys.first }
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements.keys) operation_requirements = (@api_metadata[:operation][:security] || [] ).map { |r| r.keys.first }
requirements = global_requirements | operation_requirements
# Then obtain the scheme definitions for those requirements
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements)
definitions.values.select { |d| d[:type] == :apiKey } definitions.values.select { |d| d[:type] == :apiKey }
end end
end end

View File

@@ -1,4 +1,6 @@
require 'active_support/core_ext/hash/slice'
require 'json-schema' require 'json-schema'
require 'rswag/specs/extended_schema'
module Rswag module Rswag
module Specs module Specs
@@ -11,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
@@ -22,13 +25,26 @@ module Rswag
end end
end end
def validate_headers!(headers)
header_schema = @api_metadata[:response][:headers]
return if header_schema.nil?
header_schema.keys.each do |header_name|
raise UnexpectedResponse, "Expected response header #{header_name} to be present" if headers[header_name.to_s].nil?
end
end
def validate_body!(body) def validate_body!(body)
schema = @api_metadata[:response][:schema] response_schema = @api_metadata[:response][:schema]
return if schema.nil? return if response_schema.nil?
begin begin
JSON::Validator.validate!(schema.merge(@global_metadata), body) validation_schema = response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(@global_metadata.slice(:definitions))
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

@@ -1,5 +1,4 @@
require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/deep_merge'
require 'rspec/core/formatters/base_text_formatter'
require 'swagger_helper' require 'swagger_helper'
module Rswag module Rswag
@@ -11,12 +10,9 @@ module Rswag
::RSpec::Core::Formatters.register self, :example_group_finished, :stop ::RSpec::Core::Formatters.register self, :example_group_finished, :stop
end end
def initialize(output) def initialize(output, config = Rswag::Specs.config)
@output = output @output = output
@swagger_root = ::RSpec.configuration.swagger_root @config = config
raise ConfigurationError, 'Missing swagger_root. See swagger_helper.rb' if @swagger_root.nil?
@swagger_docs = ::RSpec.configuration.swagger_docs || []
raise ConfigurationError, 'Missing swagger_docs. See swagger_helper.rb' if @swagger_docs.empty?
@output.puts 'Generating Swagger docs ...' @output.puts 'Generating Swagger docs ...'
end end
@@ -30,13 +26,13 @@ module Rswag
end end
return unless metadata.has_key?(:response) return unless metadata.has_key?(:response)
swagger_doc = get_swagger_doc(metadata[:swagger_doc]) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
swagger_doc.deep_merge!(metadata_to_swagger(metadata)) swagger_doc.deep_merge!(metadata_to_swagger(metadata))
end end
def stop(notification=nil) def stop(notification=nil)
@swagger_docs.each do |url_path, doc| @config.swagger_docs.each do |url_path, doc|
file_path = File.join(@swagger_root, url_path) file_path = File.join(@config.swagger_root, url_path)
dirname = File.dirname(file_path) dirname = File.dirname(file_path)
FileUtils.mkdir_p dirname unless File.exists?(dirname) FileUtils.mkdir_p dirname unless File.exists?(dirname)
@@ -50,30 +46,22 @@ module Rswag
private private
def get_swagger_doc(tag)
return @swagger_docs.values.first if tag.nil?
raise ConfigurationError, "Unknown swagger_doc '#{tag}'" unless @swagger_docs.has_key?(tag)
@swagger_docs[tag]
end
def metadata_to_swagger(metadata) def metadata_to_swagger(metadata)
response_code = metadata[:response][:code] response_code = metadata[:response][:code]
response = metadata[:response].reject { |k,v| k == :code } response = metadata[:response].reject { |k,v| k == :code }
verb = metadata[:operation][:verb] verb = metadata[:operation][:verb]
operation = metadata[:operation] operation = metadata[:operation]
.reject { |k,v| k == :verb } .reject { |k,v| k == :verb }
.merge(responses: { response_code => response }) .merge(responses: { response_code => response })
{ path_template = metadata[:path_item][:template]
paths: { path_item = metadata[:path_item]
metadata[:path] => { .reject { |k,v| k == :template }
verb => operation .merge(verb => operation)
}
} { paths: { path_template => path_item } }
}
end end
end end
class ConfigurationError < StandardError; end
end end
end end

View File

@@ -1,5 +1,5 @@
module Rswag module Rswag
module Specs module Specs
VERSION = '1.0.1' VERSION = '1.2.0'
end end
end end

View File

@@ -0,0 +1,77 @@
require 'rswag/specs/configuration'
module Rswag
module Specs
describe Configuration do
subject { described_class.new(rspec_config) }
let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) }
let(:swagger_root) { 'foobar' }
let(:swagger_docs) do
{
'v1/swagger.json' => { info: { title: 'v1' } },
'v2/swagger.json' => { info: { title: 'v2' } }
}
end
describe '#swagger_root' do
let(:response) { subject.swagger_root }
context 'provided in rspec config' do
it { expect(response).to eq('foobar') }
end
context 'not provided' do
let(:swagger_root) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
end
describe '#swagger_docs' do
let(:response) { subject.swagger_docs }
context 'provided in rspec config' do
it { expect(response).to be_an_instance_of(Hash) }
end
context 'not provided' do
let(:swagger_docs) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
context 'provided but empty' do
let(:swagger_docs) { {} }
it { expect { response }.to raise_error ConfigurationError }
end
end
describe '#get_swagger_doc(tag=nil)' do
let(:swagger_doc) { subject.get_swagger_doc(tag) }
context 'no tag provided' do
let(:tag) { nil }
it 'returns the first doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v1' })
end
end
context 'tag provided' do
context 'matching doc' do
let(:tag) { 'v2/swagger.json' }
it 'returns the matching doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v2' })
end
end
context 'no matching doc' do
let(:tag) { 'foobar' }
it { expect { swagger_doc }.to raise_error ConfigurationError }
end
end
end
end
end
end

View File

@@ -5,20 +5,21 @@ module Rswag
describe ExampleGroupHelpers do describe ExampleGroupHelpers do
subject { double('example_group') } subject { double('example_group') }
let(:api_metadata) { {} }
before do before do
subject.extend ExampleGroupHelpers subject.extend ExampleGroupHelpers
allow(subject).to receive(:describe) allow(subject).to receive(:describe)
allow(subject).to receive(:context) allow(subject).to receive(:context)
allow(subject).to receive(:metadata).and_return(api_metadata) allow(subject).to receive(:metadata).and_return(api_metadata)
end end
let(:api_metadata) { {} }
describe '#path(path)' do describe '#path(path)' do
before { subject.path('/blogs') } before { subject.path('/blogs') }
it "delegates to 'describe' with 'path' metadata" do it "delegates to 'describe' with 'path' metadata" do
expect(subject).to have_received(:describe).with( expect(subject).to have_received(:describe).with(
'/blogs', path: '/blogs' '/blogs', path_item: { template: '/blogs' }
) )
end end
end end
@@ -34,7 +35,6 @@ module Rswag
end end
describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do
let(:api_metadata) { { operation: {} } }
before do before do
subject.tags('Blogs', 'Admin') subject.tags('Blogs', 'Admin')
subject.description('Some description') subject.description('Some description')
@@ -44,6 +44,7 @@ module Rswag
subject.schemes('http', 'https') subject.schemes('http', 'https')
subject.deprecated(true) subject.deprecated(true)
end end
let(:api_metadata) { { operation: {} } }
it "adds to the 'operation' metadata" do it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match( expect(api_metadata[:operation]).to match(
@@ -59,7 +60,6 @@ module Rswag
end end
describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do
let(:api_metadata) { { operation: {} } }
before do before do
subject.tags('Blogs', 'Admin') subject.tags('Blogs', 'Admin')
subject.description('Some description') subject.description('Some description')
@@ -70,6 +70,7 @@ module Rswag
subject.deprecated(true) subject.deprecated(true)
subject.security(api_key: []) subject.security(api_key: [])
end end
let(:api_metadata) { { operation: {} } }
it "adds to the 'operation' metadata" do it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match( expect(api_metadata[:operation]).to match(
@@ -86,10 +87,21 @@ module Rswag
end end
describe '#parameter(attributes)' do describe '#parameter(attributes)' do
let(:api_metadata) { { operation: {} } }
context 'always' do context "when called at the 'path' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet
it "adds to the 'path_item parameters' metadata" do
expect(api_metadata[:path_item][:parameters]).to match(
[ name: :blog, in: :body, schema: { type: 'object' } ]
)
end
end
context "when called at the 'operation' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined
it "adds to the 'operation parameters' metadata" do it "adds to the 'operation parameters' metadata" do
expect(api_metadata[:operation][:parameters]).to match( expect(api_metadata[:operation][:parameters]).to match(
@@ -100,7 +112,8 @@ 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: {} } }
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 ]
@@ -120,8 +133,8 @@ module Rswag
end end
describe '#schema(value)' do describe '#schema(value)' do
let(:api_metadata) { { response: {} } }
before { subject.schema(type: 'object') } before { subject.schema(type: 'object') }
let(:api_metadata) { { response: {} } }
it "adds to the 'response' metadata" do it "adds to the 'response' metadata" do
expect(api_metadata[:response][:schema]).to match(type: 'object') expect(api_metadata[:response][:schema]).to match(type: 'object')
@@ -129,8 +142,8 @@ module Rswag
end end
describe '#header(name, attributes)' do describe '#header(name, attributes)' do
let(:api_metadata) { { response: {} } }
before { subject.header('Date', type: 'string') } before { subject.header('Date', type: 'string') }
let(:api_metadata) { { response: {} } }
it "adds to the 'response headers' metadata" do it "adds to the 'response headers' metadata" do
expect(api_metadata[:response][:headers]).to match( expect(api_metadata[:response][:headers]).to match(
@@ -138,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

@@ -4,9 +4,19 @@ module Rswag
module Specs module Specs
describe ExampleHelpers do describe ExampleHelpers do
subject { double('example') }
before do
subject.extend ExampleHelpers
# Mock out some infrastructure
stub_const('Rails::VERSION::MAJOR', 3)
rswag_config = double('rswag_config')
allow(rswag_config).to receive(:get_swagger_doc).and_return(global_metadata)
allow(subject).to receive(:rswag_config).and_return(rswag_config)
end
let(:api_metadata) do let(:api_metadata) do
{ {
path: '/blogs/{blog_id}/comments/{id}', path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: { operation: {
verb: :put, verb: :put,
summary: 'Updates a blog', summary: 'Updates a blog',
@@ -16,9 +26,9 @@ module Rswag
{ name: 'q1', in: :query, type: 'string' }, { name: 'q1', in: :query, type: 'string' },
{ name: :blog, in: :body, schema: { type: 'object' } } { name: :blog, in: :body, schema: { type: 'object' } }
], ],
security: { security: [
api_key: [] { api_key: [] }
} ]
} }
} }
end end
@@ -34,22 +44,14 @@ module Rswag
} }
end end
subject { double('example') }
before do
subject.extend ExampleHelpers
allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2)
allow(subject).to receive(:q1).and_return('foo')
allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:global_metadata).and_return(global_metadata)
allow(subject).to receive(:put)
end
describe '#submit_request(api_metadata)' do describe '#submit_request(api_metadata)' do
before do before do
stub_const('Rails::VERSION::MAJOR', 3) allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2)
allow(subject).to receive(:q1).and_return('foo')
allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put)
subject.submit_request(api_metadata) subject.submit_request(api_metadata)
end end

View File

@@ -4,9 +4,15 @@ module Rswag
module Specs module Specs
describe RequestFactory do describe RequestFactory do
subject { RequestFactory.new(api_metadata, global_metadata) }
before do
allow(example).to receive(:blog_id).and_return(1)
allow(example).to receive(:id).and_return('2')
end
let(:api_metadata) do let(:api_metadata) do
{ {
path: '/blogs/{blog_id}/comments/{id}', path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: { operation: {
verb: :put, verb: :put,
summary: 'Updates a blog', summary: 'Updates a blog',
@@ -18,14 +24,7 @@ module Rswag
} }
end end
let(:global_metadata) { {} } let(:global_metadata) { {} }
subject { RequestFactory.new(api_metadata, global_metadata) }
let(:example) { double('example') } let(:example) { double('example') }
before do
allow(example).to receive(:blog_id).and_return(1)
allow(example).to receive(:id).and_return('2')
end
describe '#build_fullpath(example)' do describe '#build_fullpath(example)' do
let(:path) { subject.build_fullpath(example) } let(:path) { subject.build_fullpath(example) }
@@ -103,7 +102,7 @@ module Rswag
end end
context 'global requirement' do context 'global requirement' do
before { global_metadata[:security] = { api_key: [] } } before { global_metadata[:security] = [ { api_key: [] } ] }
it "appends the api_key using metadata and example value" do it "appends the api_key using metadata and example value" do
expect(path).to eq('/blogs/1/comments/2?api_key=fookey') expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
@@ -111,7 +110,7 @@ module Rswag
end end
context 'operation-specific requirement' do context 'operation-specific requirement' do
before { api_metadata[:operation][:security] = { api_key: [] } } before { api_metadata[:operation][:security] = [ { api_key: [] } ] }
it "appends the api_key using metadata and example value" do it "appends the api_key using metadata and example value" do
expect(path).to eq('/blogs/1/comments/2?api_key=fookey') expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
@@ -126,6 +125,17 @@ module Rswag
expect(path).to eq('/foobar/blogs/1/comments/2') expect(path).to eq('/foobar/blogs/1/comments/2')
end end
end end
context "defined at the 'path' level" do
before do
api_metadata[:path_item][:parameters] = [ { name: :blog_id, in: :path } ]
api_metadata[:operation][:parameters] = [ { name: :id, in: :path } ]
end
it "builds path from parameters defined at path and operation levels" do
expect(path).to eq('/blogs/1/comments/2')
end
end
end end
describe '#build_body(example)' do describe '#build_body(example)' do

View File

@@ -4,11 +4,11 @@ module Rswag
module Specs module Specs
describe ResponseValidator do describe ResponseValidator do
subject { ResponseValidator.new(api_metadata, global_metadata) }
let(:api_metadata) { { response: { code: 200 } } } let(:api_metadata) { { response: { code: 200 } } }
let(:global_metadata) { {} } let(:global_metadata) { {} }
subject { ResponseValidator.new(api_metadata, global_metadata) }
describe '#validate!(response)' do describe '#validate!(response)' do
let(:call) { subject.validate!(response) } let(:call) { subject.validate!(response) }
@@ -66,6 +66,42 @@ 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' => 1,
'X-Rate-Limit-Remaining' => 1
}) }
it { expect { call }.to raise_error UnexpectedResponse }
end
end
end end
end end
end end

View File

@@ -5,112 +5,59 @@ module Rswag
module Specs module Specs
describe SwaggerFormatter do describe SwaggerFormatter do
# Mock infrastructure - output, RSpec.configuration etc. subject { described_class.new(output, config) }
# Mock out some infrastructure
before do
allow(config).to receive(:swagger_root).and_return(swagger_root)
end
let(:config) { double('config') }
let(:output) { double('output').as_null_object } let(:output) { double('output').as_null_object }
let(:swagger_root) { File.expand_path('../tmp', __FILE__) } let(:swagger_root) { File.expand_path('../tmp/swagger', __FILE__) }
let(:swagger_docs) do
{
'v1/swagger.json' => { info: { version: 'v1' } }
}
end
let(:config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) }
before { allow(RSpec).to receive(:configuration).and_return(config) }
subject { described_class.new(output) }
describe '::new(output)' do
context 'swagger_root not configured' do
let(:swagger_root) { nil }
it { expect { subject }.to raise_error ConfigurationError }
end
context 'swagger_docs not configured' do
let(:swagger_docs) { nil }
it { expect { subject }.to raise_error ConfigurationError }
end
end
describe '#example_group_finished(notification)' do describe '#example_group_finished(notification)' do
# Mock notification parameter before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
subject.example_group_finished(notification)
end
let(:swagger_doc) { {} }
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
let(:api_metadata) do let(:api_metadata) do
{ {
path: '/blogs', path_item: { template: '/blogs' },
operation: { verb: :post, summary: 'Creates a blog' }, operation: { verb: :post, summary: 'Creates a blog' },
response: { code: '201', description: 'blog created' } response: { code: '201', description: 'blog created' }
} }
end end
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
let(:call) { subject.example_group_finished(notification) } it 'converts to swagger and merges into the corresponding swagger doc' do
expect(swagger_doc).to match(
context 'single swagger_doc' do paths: {
before { call } '/blogs' => {
post: {
it 'converts metadata to swagger and merges into the doc' do summary: 'Creates a blog',
expect(swagger_docs.values.first).to match( responses: {
info: { version: 'v1' }, '201' => { description: 'blog created' }
paths: {
'/blogs' => {
post: {
summary: 'Creates a blog',
responses: {
'201' => { description: 'blog created' }
}
} }
} }
} }
)
end
end
context 'multiple swagger_docs' do
let(:swagger_docs) do
{
'v1/swagger.json' => {},
'v2/swagger.json' => {}
} }
end )
context "no 'swagger_doc' tag" do
before { call }
it 'merges into the first doc' do
expect(swagger_docs.values.first).to have_key(:paths)
end
end
context "matching 'swagger_doc' tag" do
before do
api_metadata[:swagger_doc] = 'v2/swagger.json'
call
end
it 'merges into the matched doc' do
expect(swagger_docs.values.last).to have_key(:paths)
end
end
context "non matching 'swagger_doc' tag" do
before { api_metadata[:swagger_doc] = 'foobar' }
it { expect { call }.to raise_error ConfigurationError }
end
end end
end end
describe '#stop' do describe '#stop' do
let(:notification) { double('notification') }
let(:swagger_docs) do
{
'v1/swagger.json' => { info: { version: 'v1' } },
'v2/swagger.json' => { info: { version: 'v2' } },
}
end
before do before do
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) FileUtils.rm_r(swagger_root) if File.exists?(swagger_root)
allow(config).to receive(:swagger_docs).and_return(
'v1/swagger.json' => { info: { version: 'v1' } },
'v2/swagger.json' => { info: { version: 'v2' } }
)
subject.stop(notification) subject.stop(notification)
end end
let(:notification) { double('notification') }
it 'writes the swagger_doc(s) to file' do it 'writes the swagger_doc(s) to file' do
expect(File).to exist("#{swagger_root}/v1/swagger.json") expect(File).to exist("#{swagger_root}/v1/swagger.json")
expect(File).to exist("#{swagger_root}/v2/swagger.json") expect(File).to exist("#{swagger_root}/v2/swagger.json")

View File

@@ -3,33 +3,33 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Swagger UI</title> <title>Swagger UI</title>
<link rel="icon" type="image/png" href="/assets/swagger-ui/images/favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="<%= asset_path 'swagger-ui/images/favicon-32x32.png' %>" sizes="32x32" />
<link rel="icon" type="image/png" href="/assets/swagger-ui/images/favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="<%= asset_path 'swagger-ui/images/favicon-16x16.png' %>" sizes="16x16" />
<link href='/assets/swagger-ui/css/typography.css' media='screen' rel='stylesheet' type='text/css'/> <link href="<%= asset_path 'swagger-ui/css/typography.css' %>" media='screen' rel='stylesheet' type='text/css'/>
<link href='/assets/swagger-ui/css/reset.css' media='screen' rel='stylesheet' type='text/css'/> <link href="<%= asset_path 'swagger-ui/css/reset.css' %>" media='screen' rel='stylesheet' type='text/css'/>
<link href='/assets/swagger-ui/css/screen.css' media='screen' rel='stylesheet' type='text/css'/> <link href="<%= asset_path 'swagger-ui/css/screen.css' %>" media='screen' rel='stylesheet' type='text/css'/>
<link href='/assets/swagger-ui/css/reset.css' media='print' rel='stylesheet' type='text/css'/> <link href="<%= asset_path 'swagger-ui/css/reset.css' %>" media='print' rel='stylesheet' type='text/css'/>
<link href='/assets/swagger-ui/css/print.css' media='print' rel='stylesheet' type='text/css'/> <link href="<%= asset_path 'swagger-ui/css/print.css' %>" media='print' rel='stylesheet' type='text/css'/>
<script src='/assets/swagger-ui/lib/object-assign-pollyfill.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/object-assign-pollyfill.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/jquery-1.8.0.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/jquery-1.8.0.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/jquery.slideto.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/jquery.slideto.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/jquery.wiggle.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/jquery.wiggle.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/jquery.ba-bbq.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/jquery.ba-bbq.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/handlebars-4.0.5.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/handlebars-4.0.5.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/lodash.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/lodash.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/backbone-min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/backbone-min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/swagger-ui.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/swagger-ui.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/highlight.9.1.0.pack.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/highlight.9.1.0.pack.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/highlight.9.1.0.pack_extended.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/highlight.9.1.0.pack_extended.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/jsoneditor.min.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/jsoneditor.min.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/marked.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/marked.js' %>" type='text/javascript'></script>
<script src='/assets/swagger-ui/lib/swagger-oauth.js' type='text/javascript'></script> <script src="<%= asset_path 'swagger-ui/lib/swagger-oauth.js' %>" type='text/javascript'></script>
<!-- Some basic translations --> <!-- Some basic translations -->
<!-- <script src='/assets/swagger-ui/lang/translator.js' type='text/javascript'></script> --> <!-- <script src="<%= asset_path 'swagger-ui/lang/translator.js' %>" type='text/javascript'></script> -->
<!-- <script src='/assets/swagger-ui/lang/ru.js' type='text/javascript'></script> --> <!-- <script src="<%= asset_path 'swagger-ui/lang/ru.js' %>" type='text/javascript'></script> -->
<!-- <script src='/assets/swagger-ui/lang/en.js' type='text/javascript'></script> --> <!-- <script src="<%= asset_path 'swagger-ui/lang/en.js' %>" type='text/javascript'></script> -->
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
@@ -84,7 +84,7 @@
<body class="swagger-section"> <body class="swagger-section">
<div id='header'> <div id='header'>
<div class="swagger-ui-wrap"> <div class="swagger-ui-wrap">
<a id="logo" href="http://swagger.io"><img class="logo__img" alt="swagger" height="30" width="30" src="/assets/swagger-ui/images/logo_small.png" /><span class="logo__title">swagger</span></a> <a id="logo" href="http://swagger.io"><img class="logo__img" alt="swagger" height="30" width="30" src="<%= asset_path 'swagger-ui/images/logo_small.png' %>" /><span class="logo__title">swagger</span></a>
<form id='api_selector'> <form id='api_selector'>
<div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/></div> <div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/></div>
<div id='auth_container'></div> <div id='auth_container'></div>

View File

@@ -1,5 +1,5 @@
module Rswag module Rswag
module Ui module Ui
VERSION = '1.0.1' VERSION = '1.2.0'
end end
end end

View File

@@ -880,7 +880,7 @@
padding: 6px 8px; padding: 6px 8px;
} }
.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber { .swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber {
background-image: url('../images/throbber.gif'); background-image: url(<%= asset_path('swagger-ui/images/throbber.gif') %>);
width: 128px; width: 128px;
height: 16px; height: 16px;
display: block; display: block;
@@ -1222,7 +1222,7 @@
height: 18px; height: 18px;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
background: url(../images/explorer_icons.png) no-repeat; background: url(<%= asset_path('swagger-ui/images/explorer_icons.png') %>) no-repeat;
} }
.swagger-section .authorize__btn_operation_login { .swagger-section .authorize__btn_operation_login {
background-position: 0 0; background-position: 0 0;

View File

@@ -880,7 +880,7 @@
padding: 6px 8px; padding: 6px 8px;
} }
.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber { .swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber {
background-image: url('../images/throbber.gif'); background-image: url(<%= asset_path('swagger-ui/images/throbber.gif') %>);
width: 128px; width: 128px;
height: 16px; height: 16px;
display: block; display: block;
@@ -1222,7 +1222,7 @@
height: 18px; height: 18px;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
background: url(../images/explorer_icons.png) no-repeat; background: url(<%= asset_path('swagger-ui/images/explorer_icons.png') %>) no-repeat;
} }
.swagger-section .authorize__btn_operation_login { .swagger-section .authorize__btn_operation_login {
background-position: 0 0; background-position: 0 0;
@@ -1354,7 +1354,7 @@
height: 18px; height: 18px;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
background: url(../images/explorer_icons.png) no-repeat; background: url(<%= asset_path('swagger-ui/images/explorer_icons.png') %>) no-repeat;
} }
.swagger-section .api-ic .api_information_panel { .swagger-section .api-ic .api_information_panel {
position: relative; position: relative;

View File

@@ -2,7 +2,7 @@
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
background: transparent url(../images/logo.png) no-repeat left center; background: transparent url(<%= asset_path('swagger-ui/images/logo.png') %>) no-repeat left center;
padding: 20px 0 20px 40px; padding: 20px 0 20px 40px;
} }
#text-head { #text-head {
@@ -64,7 +64,7 @@ h1 {
width: 1500px; width: 1500px;
margin: auto; margin: auto;
margin-top: 0; margin-top: 0;
background-image: url('../images/shield.png'); background-image: url(<%= asset_path('swagger-ui/images/shield.png') %>);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: -40px -20px; background-position: -40px -20px;
margin-bottom: 210px; margin-bottom: 210px;

View File

@@ -1,14 +0,0 @@
/* Google Font's Droid Sans */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 400;
src: local('Droid Sans'), local('DroidSans'), url('../fonts/DroidSans.ttf'), format('truetype');
}
/* Google Font's Droid Sans Bold */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 700;
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url('../fonts/DroidSans-Bold.ttf'), format('truetype');
}

View File

@@ -0,0 +1,14 @@
/* Google Font's Droid Sans */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 400;
src: local('Droid Sans'), local('DroidSans'), url(<%= asset_path('swagger-ui/fonts/DroidSans.ttf') %>), format('truetype');
}
/* Google Font's Droid Sans Bold */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 700;
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(<%= asset_path('swagger-ui/fonts/DroidSans-Bold.ttf') %>), format('truetype');
}

View File

@@ -1,3 +1,3 @@
module Rswag module Rswag
VERSION = '1.0.1' VERSION = '1.2.0'
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

@@ -0,0 +1,67 @@
TestApp::Application.configure do
# Settings specified here will take precedence over those in config/application.rb
# Code is not reloaded between requests
config.cache_classes = true
# Full error reports are disabled and caching is turned on
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = true
# Compress JavaScripts and CSS
config.assets.compress = true
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false
# Generate digests for assets URLs
config.assets.digest = true
# Defaults to nil and saved in location specified by config.assets.prefix
# config.assets.manifest = YOUR_PATH
# Specifies the header that your server uses for sending files
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# See everything in the log (default is :info)
# config.log_level = :debug
# Prepend all log lines with the following tags
# config.log_tags = [ :subdomain, :uuid ]
# Use a different logger for distributed setups
# config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
# Use a different cache store in production
# config.cache_store = :mem_cache_store
# Enable serving of images, stylesheets, and JavaScripts from an asset server
# config.action_controller.asset_host = "http://assets.example.com"
# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
# config.assets.precompile += %w( search.js )
# Disable delivery errors, bad email addresses will be ignored
# config.action_mailer.raise_delivery_errors = false
# Enable threaded mode
# config.threadsafe!
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found)
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify
# Log the query plan for queries taking more than this (works
# with SQLite, MySQL, and PostgreSQL)
# config.active_record.auto_explain_threshold_in_seconds = 0.5
end

View File

@@ -0,0 +1,12 @@
require 'rails_helper'
feature 'swagger-ui', js: true do
scenario 'browsing api-docs' do
visit '/api-docs'
expect(page).to have_content('GET /blogs Searches blogs')
expect(page).to have_content('POST /blogs Creates a blog')
expect(page).to have_content('GET /blogs/{id} Retrieves a blog')
end
end

View File

@@ -41,16 +41,27 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
end end
path '/blogs/{id}' do path '/blogs/{id}' do
parameter name: :id, :in => :path, :type => :string
get 'Retrieves a blog' do get 'Retrieves a blog' do
tags 'Blogs' tags 'Blogs'
description 'Retrieves a specific blog by id' description 'Retrieves a specific blog by id'
operationId 'getBlog' operationId 'getBlog'
produces 'application/json' produces 'application/json'
parameter name: :id, :in => :path, :type => :string
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

@@ -50,4 +50,6 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace! config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via: # arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name") # config.filter_gems_from_backtrace("gem name")
Capybara.javascript_driver = :webkit
end end

View File

@@ -51,9 +51,9 @@ RSpec.configure do |config|
in: :query in: :query
} }
}, },
security: { security: [
api_key: [] { api_key: [] }
} ]
} }
} }
end end

View File

@@ -68,6 +68,14 @@
} }
}, },
"/blogs/{id}": { "/blogs/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"required": true
}
],
"get": { "get": {
"summary": "Retrieves a blog", "summary": "Retrieves a blog",
"tags": [ "tags": [
@@ -78,19 +86,29 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"required": true
}
],
"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": {
@@ -145,9 +163,11 @@
"in": "query" "in": "query"
} }
}, },
"security": { "security": [
"api_key": [ {
"api_key": [
] ]
} }
]
} }