56 Commits

Author SHA1 Message Date
domaindrivendev
732cab994c simplify validation blocks and use correct scope 2017-07-21 22:26:14 -07:00
domaindrivendev
7f0e437f8b For rswag, param names MUST be unique 2017-07-21 21:04:46 -07:00
domaindrivendev
700b227231 Merge branch 'thg303-add-formData-support' 2017-07-21 14:25:51 -07:00
domaindrivendev
b16198377b Merge branch 'add-formData-support' of https://github.com/thg303/rswag into thg303-add-formData-support 2017-07-21 14:25:29 -07:00
domaindrivendev
e18a001e9b Move rackify headers to RequestFactory + other minor refactors 2017-07-21 13:49:01 -07:00
domaindrivendev
6ee53b08ae Merge branch 'refactor-request-factory' 2017-07-20 23:42:49 -07:00
domaindrivendev
97c2a39cfa Refactor request_factory & response_validator 2017-07-20 23:42:40 -07:00
domaindrivendev
f7c29267fe Merge pull request #78 from bertha-carl/fully-validate
Can we use JSON::Validator.fully_validate() to get better error messages?
2017-07-20 10:09:01 -07:00
Hannes Probst
8ede7f78f1 use fully_validate to get more specific error msg 2017-07-18 15:45:36 +02:00
domaindrivendev
8b937e3585 Merge pull request #76 from ahobson/bugfix-optional-query-params
Support optional query parameters
2017-07-13 16:35:48 -07:00
Andrew Hobson
1515ce4fcb Support optional query parameters 2017-07-11 16:28:45 -04:00
domaindrivendev
741f0dc240 Merge branch 'ci-updates' 2017-07-10 23:55:51 -07:00
domaindrivendev
3404fa72aa Publish gems via CI on tagged builds of master 2017-07-10 23:52:51 -07:00
Ali Qanavatian
44840ab836 Merge branch 'master' into add-formData-support 2017-07-04 09:45:33 +04:30
domaindrivendev
521ab14eaf Merge pull request #72 from domaindrivendev/minimal-dependencies
Reduces dependencies to just the required Rails components
2017-06-28 09:18:32 -07:00
domaindrivendev
8f5cb1aa12 Reduce depencencies to require Rails components only 2017-06-28 09:01:07 -07:00
domaindrivendev
519eb74cdd Merge pull request #71 from domaindrivendev/basic-auth-take2
Basic auth take2
2017-06-28 00:16:38 -07:00
domaindrivendev
de7ec5f15d Leverage security definitions for headers in example requests 2017-06-26 17:52:00 -07:00
domaindrivendev
e40c5fc26e Prep for 1.3.0 2017-06-26 17:39:34 -07:00
domaindrivendev
1de29136ef Merge pull request #64 from jume-dev/swagger-ui-url-patch
Add proper swagger ui url
2017-06-26 17:39:04 -07:00
domaindrivendev
88449944e1 Merge pull request #47 from DMiPartners/fixdepnotice
get no deprecation; add some docs
2017-06-26 17:32:32 -07:00
domaindrivendev
e03a6d333f Merge branch 'master' into fixdepnotice 2017-06-26 17:26:28 -07:00
domaindrivendev
215aaddb7d Merge pull request #69 from lazebny/rswag-ui-issue-with-precompilation
Fixed issue with precomilation
2017-06-19 07:17:15 -07:00
Vadim Lazebny
925b754233 Fixed issue with precomilation 2017-06-19 15:27:21 +03:00
Robert Kranz
69c7decce1 Add proper swagger ui url
As this only supports swagger ui 2, the url should link accordingly
2017-06-01 16:01:15 +02:00
domaindrivendev
353be669e4 Removed some deprecation warnings 2017-05-19 11:11:13 -07:00
domaindrivendev
75d676d753 Merge pull request #59 from grk/rails-5-1
Relax Rails dependency to allow 5.1
2017-05-19 10:39:29 -07:00
domaindrivendev
1b3a976313 Merge branch 'master' into rails-5-1 2017-05-19 10:35:00 -07:00
domaindrivendev
2617fdc48d Prep for v1.2.1 release 2017-05-19 10:32:22 -07:00
Grzesiek Kolodziejczyk
724ede0076 Remove unnecessary whitespace 2017-05-18 11:48:14 +02:00
Grzesiek Kolodziejczyk
8aeb9a6c70 Relax Rails dependency to allow 5.1 2017-05-18 11:47:40 +02:00
ali.q
182ee093f4 add formData support 2017-05-11 05:06:46 +04:30
domaindrivendev
25d8adaf8b Merge pull request #57 from horiaradu/master
Response body value validation
2017-04-29 12:46:31 -07:00
Horia Radu
f195c82759 yield entire response instead of only the body 2017-04-29 21:17:53 +03:00
Horia Radu
37f86f6d94 yield entire response instead of only the body 2017-04-29 12:06:57 +03:00
Horia Radu
51c9f4e5e6 Response body value validation
Add the possibility to pass a block to the run_test!
method in order to add expectations on your response
2017-04-28 11:46:52 +03:00
Dante Piombino
233c5cc735 get no deprecation; add some docs 2017-02-16 16:31:45 -05:00
domaindrivendev
cf9170101b Merge pull request #42 from PavelBezpalov/master
Add way for examples generation from responses
2017-01-19 08:52:40 -08:00
Pavel Bezpalov
98d5b982c4 Add way for examples generation from responses 2017-01-19 11:46:56 +02:00
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
56 changed files with 1083 additions and 666 deletions

3
.gitignore vendored
View File

@@ -2,3 +2,6 @@
**/*/log **/*/log
**/*/*.gem **/*/*.gem
**/*/*.sqlite3 **/*/*.sqlite3
**/*/public/assets
*.swp
Gemfile.lock

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
2.3.0

View File

@@ -1,10 +1,64 @@
nguage: ruby language: ruby
rvm: rvm:
- 2.2.5 - 2.2.5
env: env:
- "RAILS_VERSION=3.2.22" - RAILS_VERSION=5.1.2
- "RAILS_VERSION=4.2.0" - RAILS_VERSION=4.2.0
- "RAILS_VERSION=5.0.0" - RAILS_VERSION=3.2.22
cache: bundler
install: bundle update cache:
directories:
- /home/travis/.rvm/gems/ruby-2.2.5
install: bundle install
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
script: ./ci/test.sh script: ./ci/test.sh
jobs:
include:
- stage: publish components
script: 'cd rswag-api'
deploy:
gemspec: rswag-api.gemspec
provider: rubygems
api_key: $RUBYGEMS_API_KEY
on:
branch: master
tags: true
- stage: publish components
script: 'cd rswag-specs'
deploy:
gemspec: rswag-specs.gemspec
provider: rubygems
api_key: $RUBYGEMS_API_KEY
on:
branch: master
tags: true
- stage: publish components
script: 'cd rswag-ui'
deploy:
gemspec: rswag-ui.gemspec
provider: rubygems
api_key: $RUBYGEMS_API_KEY
on:
branch: master
tags: true
- stage: publish rswag
script: 'cd rswag'
deploy:
gemspec: rswag.gemspec
provider: rubygems
api_key: $RUBYGEMS_API_KEY
on:
branch: master
tags: true

11
Gemfile
View File

@@ -2,7 +2,7 @@ source "https://rubygems.org"
# Allow the rails version to come from an ENV setting so Travis can test multiple versions. # Allow the rails version to come from an ENV setting so Travis can test multiple versions.
# See http://www.schneems.com/post/50991826838/testing-against-multiple-rails-versions/ # See http://www.schneems.com/post/50991826838/testing-against-multiple-rails-versions/
rails_version = ENV['RAILS_VERSION'] || '3.2.22' rails_version = ENV['RAILS_VERSION'] || '5.1.2'
gem 'rails', "#{rails_version}" gem 'rails', "#{rails_version}"
@@ -15,6 +15,7 @@ end
gem 'sqlite3' gem 'sqlite3'
gem 'rswag-specs', path: './rswag-specs'
gem 'rswag-api', path: './rswag-api' gem 'rswag-api', path: './rswag-api'
gem 'rswag-ui', path: './rswag-ui' gem 'rswag-ui', path: './rswag-ui'
@@ -25,5 +26,11 @@ group :test do
gem 'test-unit' gem 'test-unit'
gem 'rspec-rails' gem 'rspec-rails'
gem 'generator_spec' gem 'generator_spec'
gem 'rswag-specs', path: './rswag-specs' gem 'capybara'
gem 'capybara-webkit'
end
group :assets do
gem 'uglifier'
gem 'therubyracer'
end end

View File

@@ -1,146 +0,0 @@
PATH
remote: ./rswag-api
specs:
rswag-api (1.0.1)
rails (>= 3.1, < 5.1)
PATH
remote: ./rswag-specs
specs:
rswag-specs (1.0.1)
json-schema (~> 2.2)
rails (>= 3.1, < 5.1)
rspec-rails (>= 2.14, < 4)
PATH
remote: ./rswag-ui
specs:
rswag-ui (1.0.1)
rails (>= 3.1, < 5.1)
GEM
remote: https://rubygems.org/
specs:
actionmailer (3.2.22)
actionpack (= 3.2.22)
mail (~> 2.5.4)
actionpack (3.2.22)
activemodel (= 3.2.22)
activesupport (= 3.2.22)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.4)
rack (~> 1.4.5)
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.2.1)
activemodel (3.2.22)
activesupport (= 3.2.22)
builder (~> 3.0.0)
activerecord (3.2.22)
activemodel (= 3.2.22)
activesupport (= 3.2.22)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activeresource (3.2.22)
activemodel (= 3.2.22)
activesupport (= 3.2.22)
activesupport (3.2.22)
i18n (~> 0.6, >= 0.6.4)
multi_json (~> 1.0)
addressable (2.4.0)
arel (3.0.3)
builder (3.0.4)
diff-lcs (1.2.5)
erubis (2.7.0)
generator_spec (0.9.3)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
hike (1.2.3)
i18n (0.7.0)
journey (1.0.4)
json (1.8.3)
json-schema (2.7.0)
addressable (>= 2.4)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.25.1)
multi_json (1.12.1)
polyglot (0.3.5)
power_assert (0.3.1)
rack (1.4.7)
rack-cache (1.6.1)
rack (>= 0.4)
rack-ssl (1.3.4)
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (3.2.22)
actionmailer (= 3.2.22)
actionpack (= 3.2.22)
activerecord (= 3.2.22)
activeresource (= 3.2.22)
activesupport (= 3.2.22)
bundler (~> 1.0)
railties (= 3.2.22)
railties (3.2.22)
actionpack (= 3.2.22)
activesupport (= 3.2.22)
rack-ssl (~> 1.3.2)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
rake (11.3.0)
rdoc (3.12.2)
json (~> 1.4)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-rails (3.5.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
sprockets (2.2.3)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.12)
strong_parameters (0.2.3)
actionpack (~> 3.0)
activemodel (~> 3.0)
activesupport (~> 3.0)
railties (~> 3.0)
test-unit (3.2.1)
power_assert
thor (0.19.1)
tilt (1.4.1)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.51)
PLATFORMS
ruby
DEPENDENCIES
generator_spec
rails (= 3.2.22)
rspec-rails
rswag-api!
rswag-specs!
rswag-ui!
sqlite3
strong_parameters
test-unit

156
README.md
View File

@@ -82,6 +82,11 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
let(:id) { 'invalid' } let(:id) { 'invalid' }
run_test! run_test!
end end
response '406', 'unsupported accept header' do
let(:'Accept') { 'application/foo' }
run_test!
end
end end
end end
end end
@@ -103,6 +108,17 @@ If you've used [Swagger](http://swagger.io/specification) before, then the synta
Take special note of the __run_test!__ method that's called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. For example, the "post" description in the example above specifies a "body" parameter called "blog". It also lists 2 different responses. For the success case (i.e. the 201 response), notice how "let" is used to set the blog parameter to a value that matches the provided schema. For the failure case (i.e. the 422 response), notice how it's set to a value that does not match the provided schema. When the test is executed, rswag also validates the actual response code and, where applicable, the response body against the provided [JSON Schema](http://json-schema.org/documentation.html). Take special note of the __run_test!__ method that's called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. For example, the "post" description in the example above specifies a "body" parameter called "blog". It also lists 2 different responses. For the success case (i.e. the 201 response), notice how "let" is used to set the blog parameter to a value that matches the provided schema. For the failure case (i.e. the 422 response), notice how it's set to a value that does not match the provided schema. When the test is executed, rswag also validates the actual response code and, where applicable, the response body against the provided [JSON Schema](http://json-schema.org/documentation.html).
If you want to do additional validation on the response, pass a block to the __run_test!__ method:
```ruby
response '201', 'blog created' do
run_test! do |response|
data = JSON.parse(response.body)
expect(data['title']).to eq('foo')
end
end
```
If you'd like your specs to be a little more explicit about what's going on here, you can replace the call to __run_test!__ with equivalent "before" and "it" blocks: If you'd like your specs to be a little more explicit about what's going on here, you can replace the call to __run_test!__ with equivalent "before" and "it" blocks:
```ruby ```ruby
@@ -119,6 +135,31 @@ response '201', 'blog created' do
end end
``` ```
### Null Values ###
This library is currently using JSON::Draft4 for validation of response models. It does not support null as a value. So you can add the property 'x-nullable' to a definition to allow null/nil values to pass.
```ruby
describe 'Blogs API' do
path '/blogs' do
post 'Creates a blog' do
...
response '200', 'blog found' do
schema type: :object,
properties: {
id: { type: :integer },
title: { type: :string },
content: { type: :string, 'x-nullable': true }
}
....
end
end
end
end
```
*Note:* the OAI v3 may be released soon(ish?) and include a nullable property. This may have an effect on the need/use of custom extension to the draft. Do not use this property if you don't understand the implications.
<https://github.com/OAI/OpenAPI-Specification/issues/229#issuecomment-280376087>
### Global Metadata ### ### Global Metadata ###
In addition to paths, operations and responses, Swagger also supports global API metadata. When you install rswag, a file called _swagger_helper.rb_ is added to your spec folder. This is where you define one or more Swagger documents and provide global metadata. Again, the format is based on Swagger so most of the global fields supported by the top level ["Swagger" object](http://swagger.io/specification/#swaggerObject) can be provided with each document definition. As an example, you could define a Swagger document for each version of your API and in each case specify a title, version string and URL basePath: In addition to paths, operations and responses, Swagger also supports global API metadata. When you install rswag, a file called _swagger_helper.rb_ is added to your spec folder. This is where you define one or more Swagger documents and provide global metadata. Again, the format is based on Swagger so most of the global fields supported by the top level ["Swagger" object](http://swagger.io/specification/#swaggerObject) can be provided with each document definition. As an example, you could define a Swagger document for each version of your API and in each case specify a title, version string and URL basePath:
@@ -164,6 +205,58 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do
end end
``` ```
### Specifying/Testing API Security ###
Swagger allows for the specification of different security schemes and their applicability to operations in an API. To leverage this in rswag, you define the schemes globally in _swagger_helper.rb_ and then use the "security" attribute at the operation level to specify which schemes, if any, are applicable to that operation. Swagger supports :basic, :apiKey and :oauth2 scheme types. See [the spec](http://swagger.io/specification/#security-definitions-object-109) for more info.
```ruby
# spec/swagger_helper.rb
RSpec.configure do |config|
config.swagger_root = Rails.root.to_s + '/swagger'
config.swagger_docs = {
'v1/swagger.json' => {
...
securityDefinitions: {
basic: {
type: :basic
},
apiKey: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
}
end
# spec/integration/blogs_spec.rb
describe 'Blogs API' do
path '/blogs' do
post 'Creates a blog' do
tags 'Blogs'
security [ basic: [] ]
...
response '201', 'blog created' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
run_test!
end
response '401', 'authentication failed' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('bogus:bogus')}" }
run_test!
end
end
end
end
```
__NOTE:__ Depending on the scheme types, you'll be required to assign a corresponding parameter value with each example. For example, :basic auth is required above and so the :Authorization (header) parameter must be set accordingly
## Configuration & Customization ## ## Configuration & Customization ##
The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install. The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install.
@@ -171,8 +264,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 ###
@@ -243,6 +336,63 @@ describe 'Blogs API' do
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
```
### Enable generation examples from responses ###
To enable examples generation from responses add callback above run_test! like:
```ruby
after do |example|
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
end
```
You need to disable --dry-run option for Rspec > 3
Add to application.rb:
```ruby
RSpec.configure do |config|
config.swagger_dry_run = false
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_:
@@ -314,7 +464,7 @@ end
### Customizing the swagger-ui ### ### Customizing the swagger-ui ###
The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator. The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui/tree/2.x#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator.
```ruby ```ruby
rails g rswag:ui:custom rails g rswag:ui:custom

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env bash
ROOT_PATH=$PWD
set -e # abort if anything fails
bundle check || bundle
echo '####################'
echo 'Build Gems'
echo '####################'
echo ''
echo '##### rswag-api #####'
cd $ROOT_PATH/rswag-api
gem build rswag-api.gemspec
echo '##### rswag-specs #####'
cd $ROOT_PATH/rswag-specs
gem build rswag-specs.gemspec
echo '##### rswag-ui #####'
cd $ROOT_PATH/rswag-ui
gem build rswag-ui.gemspec
echo '##### rswag #####'
cd $ROOT_PATH/rswag
gem build rswag.gemspec
echo '####################'
echo 'Push to RubyGems'
echo '####################'
echo ''
echo 'Type the version no, followed by [ENTER]:'
read version
echo '##### rswag-api #####'
cd $ROOT_PATH/rswag-api
gem push rswag-api-$version.gem
echo '##### rswag-specs #####'
cd $ROOT_PATH/rswag-specs
gem push rswag-specs-$version.gem
echo '##### rswag-ui #####'
cd $ROOT_PATH/rswag-ui
gem push rswag-ui-$version.gem
echo '##### rswag #####'
cd $ROOT_PATH/rswag
gem push rswag-$version.gem
# Cleanup
cd $ROOT_PATH
# Create git tag
git tag v$version
git push origin v$version

View File

@@ -1,4 +1,3 @@
require 'rswag/api/version'
require 'rswag/api/configuration' require 'rswag/api/configuration'
require 'rswag/api/engine' require 'rswag/api/engine'

View File

@@ -1,5 +0,0 @@
module Rswag
module Api
VERSION = '1.0.2'
end
end

View File

@@ -1,12 +1,9 @@
$:.push File.expand_path("../lib", __FILE__) $:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require 'rswag/api/version'
# Describe your gem and declare its dependencies: # Describe your gem and declare its dependencies:
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "rswag-api" s.name = "rswag-api"
s.version = Rswag::Api::VERSION s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ["Richie Morris"]
s.email = ["domaindrivendev@gmail.com"] s.email = ["domaindrivendev@gmail.com"]
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = "https://github.com/domaindrivendev/rswag"
@@ -16,5 +13,5 @@ Gem::Specification.new do |s|
s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile"] s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile"]
s.add_dependency "rails", ">= 3.1", "< 5.1" s.add_dependency 'railties', '>= 3.1', '< 6.0'
end end

View File

@@ -1,5 +1,4 @@
require 'rspec/core' require 'rspec/core'
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/configuration'
@@ -12,6 +11,7 @@ module Rswag
::RSpec.configure do |c| ::RSpec.configure do |c|
c.add_setting :swagger_root c.add_setting :swagger_root
c.add_setting :swagger_docs c.add_setting :swagger_docs
c.add_setting :swagger_dry_run
c.extend ExampleGroupHelpers, type: :request c.extend ExampleGroupHelpers, type: :request
c.include ExampleHelpers, type: :request c.include ExampleHelpers, type: :request
end end

View File

@@ -25,6 +25,12 @@ module Rswag
end end
end end
def swagger_dry_run
@swagger_dry_run ||= begin
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
end
end
def get_swagger_doc(name) def get_swagger_doc(name)
return swagger_docs.values.first if name.nil? return swagger_docs.values.first if name.nil?
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]

View File

@@ -61,7 +61,15 @@ module Rswag
metadata[:response][:headers][name] = attributes metadata[:response][:headers][name] = attributes
end end
def run_test! # 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!(&block)
# NOTE: rspec 2.x support # NOTE: rspec 2.x support
if RSPEC_VERSION < 3 if RSPEC_VERSION < 3
before do before do
@@ -69,7 +77,8 @@ module Rswag
end end
it "returns a #{metadata[:response][:code]} response" do it "returns a #{metadata[:response][:code]} response" do
assert_response_matches_metadata(example.metadata) assert_response_matches_metadata(metadata)
block.call(response) if block_given?
end end
else else
before do |example| before do |example|
@@ -77,7 +86,8 @@ module Rswag
end end
it "returns a #{metadata[:response][:code]} response" do |example| it "returns a #{metadata[:response][:code]} response" do |example|
assert_response_matches_metadata(example.metadata) assert_response_matches_metadata(example.metadata, &block)
example.instance_exec(response, &block) if block_given?
end end
end end
end end

View File

@@ -5,39 +5,30 @@ module Rswag
module Specs module Specs
module ExampleHelpers module ExampleHelpers
def submit_request(api_metadata) def submit_request(metadata)
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc]) request = RequestFactory.new.build_request(metadata, self)
factory = RequestFactory.new(api_metadata, global_metadata)
if RAILS_VERSION < 5 if RAILS_VERSION < 5
send( send(
api_metadata[:operation][:verb], request[:verb],
factory.build_fullpath(self), request[:path],
factory.build_body(self), request[:payload],
factory.build_headers(self) request[:headers]
) )
else else
send( send(
api_metadata[:operation][:verb], request[:verb],
factory.build_fullpath(self), request[:path],
{ {
params: factory.build_body(self), params: request[:payload],
headers: factory.build_headers(self) headers: request[:headers]
} }
) )
end end
end end
def assert_response_matches_metadata(api_metadata) def assert_response_matches_metadata(metadata)
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc]) ResponseValidator.new.validate!(metadata, response)
validator = ResponseValidator.new(api_metadata, global_metadata)
validator.validate!(response)
end
private
def rswag_config
::Rswag::Specs.config
end end
end end
end end

View File

@@ -0,0 +1,25 @@
require 'json-schema'
module Rswag
module Specs
class ExtendedSchema < JSON::Schema::Draft4
def initialize
super
@attributes['type'] = ExtendedTypeAttribute
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
@names = ['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

@@ -1,85 +1,83 @@
require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions'
require 'json' require 'json'
module Rswag module Rswag
module Specs module Specs
class RequestFactory class RequestFactory
def initialize(api_metadata, global_metadata) def initialize(config = ::Rswag::Specs.config)
@api_metadata = api_metadata @config = config
@global_metadata = global_metadata
end end
def build_fullpath(example) def build_request(metadata, example)
@api_metadata[:path_item][:template].dup.tap do |t| swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
t.prepend(@global_metadata[:basePath] || '') parameters = expand_parameters(metadata, swagger_doc, example)
parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
t.concat(build_query_string(example))
end
end
def build_query_string(example) {}.tap do |request|
query_string = parameters_in(:query) add_verb(request, metadata)
.map { |p| build_query_string_part(p, example.send(p[:name])) } add_path(request, metadata, swagger_doc, parameters, example)
.join('&') add_headers(request, metadata, swagger_doc, parameters, example)
add_payload(request, parameters, example)
query_string.empty? ? '' : "?#{query_string}"
end
def build_body(example)
body_parameter = parameters_in(:body).first
body_parameter.nil? ? '' : example.send(body_parameter[:name]).to_json
end
def build_headers(example)
headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ]
headers.tap do |h|
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
h['ACCEPT'] = produces.join(';') unless produces.nil?
h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
end end
end end
private private
def parameters_in(location) def expand_parameters(metadata, swagger_doc, example)
path_item_params = @api_metadata[:path_item][:parameters] || [] operation_params = metadata[:operation][:parameters] || []
operation_params = @api_metadata[:operation][:parameters] || [] path_item_params = metadata[:path_item][:parameters] || []
applicable_params = operation_params security_params = derive_security_params(metadata, swagger_doc)
operation_params
.concat(path_item_params) .concat(path_item_params)
.uniq { |p| p[:name] } # operation params should override path_item params .concat(security_params)
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
applicable_params .uniq { |p| p[:name] }
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
.concat(resolve_api_key_parameters)
.select { |p| p[:in] == location }
end end
def resolve_parameter(ref) def derive_security_params(metadata, swagger_doc)
defined_params = @global_metadata[:parameters] requirements = metadata[:operation][:security] || swagger_doc[:security]
scheme_names = requirements ? requirements.map { |r| r.keys.first } : []
applicable_schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
applicable_schemes.map do |scheme|
param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
param.merge(type: :string)
end
end
def resolve_parameter(ref, swagger_doc)
definitions = swagger_doc[:parameters]
key = ref.sub('#/parameters/', '') key = ref.sub('#/parameters/', '')
raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key] raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
defined_params[key] definitions[key]
end end
def resolve_api_key_parameters def add_verb(request, metadata)
@api_key_params ||= begin request[:verb] = metadata[:operation][:verb]
# First figure out the security requirement applicable to the operation end
global_requirements = (@global_metadata[:security] || [] ).map { |r| r.keys.first }
operation_requirements = (@api_metadata[:operation][:security] || [] ).map { |r| r.keys.first }
requirements = global_requirements | operation_requirements
# Then obtain the scheme definitions for those requirements def add_path(request, metadata, swagger_doc, parameters, example)
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements) template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
definitions.values.select { |d| d[:type] == :apiKey }
request[:path] = template.tap do |template|
parameters.select { |p| p[:in] == :path }.each do |p|
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
end
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
template.concat(i == 0 ? '?' : '&')
template.concat(build_query_string_part(p, example.send(p[:name])))
end
end end
end end
def build_query_string_part(param, value) def build_query_string_part(param, value)
return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array
name = param[:name] name = param[:name]
return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
case param[:collectionFormat] case param[:collectionFormat]
when :ssv when :ssv
"#{name}=#{value.join(' ')}" "#{name}=#{value.join(' ')}"
@@ -93,6 +91,68 @@ module Rswag
"#{name}=#{value.join(',')}" # csv is default "#{name}=#{value.join(',')}" # csv is default
end end
end end
def add_headers(request, metadata, swagger_doc, parameters, example)
tuples = parameters
.select { |p| p[:in] == :header }
.map { |p| [ p[:name], example.send(p[:name]).to_s ] }
# Accept header
produces = metadata[:operation][:produces] || swagger_doc[:produces]
if produces
accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first
tuples << [ 'Accept', accept ]
end
# Content-Type header
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
if consumes
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
tuples << [ 'Content-Type', content_type ]
end
# Rails test infrastructure requires rackified headers
rackified_tuples = tuples.map do |pair|
[
case pair[0]
when 'Accept' then 'HTTP_ACCEPT'
when 'Content-Type' then 'CONTENT_TYPE'
when 'Authorization' then 'HTTP_AUTHORIZATION'
else pair[0]
end,
pair[1]
]
end
request[:headers] = Hash[ rackified_tuples ]
end
def add_payload(request, parameters, example)
content_type = request[:headers]['CONTENT_TYPE']
return if content_type.nil?
if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type)
request[:payload] = build_form_payload(parameters, example)
else
request[:payload] = build_json_payload(parameters, example)
end
end
def build_form_payload(parameters, example)
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
# Rails test infrastructure allows us to send the values directly as a hash
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
tuples = parameters
.select { |p| p[:in] == :formData }
.map { |p| [ p[:name], example.send(p[:name]) ] }
Hash[ tuples ]
end
def build_json_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body }.first
body_param ? example.send(body_param[:name]).to_json : nil
end
end end
end end
end end

View File

@@ -1,36 +1,50 @@
require 'active_support/core_ext/hash/slice'
require 'json-schema' require 'json-schema'
require 'json'
require 'rswag/specs/extended_schema'
module Rswag module Rswag
module Specs module Specs
class ResponseValidator class ResponseValidator
def initialize(api_metadata, global_metadata) def initialize(config = ::Rswag::Specs.config)
@api_metadata = api_metadata @config = config
@global_metadata = global_metadata
end end
def validate!(response) def validate!(metadata, response)
validate_code!(response.code) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
validate_body!(response.body)
validate_code!(metadata, response.code)
validate_headers!(metadata, response.headers)
validate_body!(metadata, swagger_doc, response.body)
end end
private private
def validate_code!(code) def validate_code!(metadata, code)
if code.to_s != @api_metadata[:response][:code].to_s expected = metadata[:response][:code].to_s
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{@api_metadata[:response][:code]}'" if code != expected
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{expected}'"
end end
end end
def validate_body!(body) def validate_headers!(metadata, headers)
schema = @api_metadata[:response][:schema] expected = (metadata[:response][:headers] || {}).keys
return if schema.nil? expected.each do |name|
begin raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
JSON::Validator.validate!(schema.merge(@global_metadata), body)
rescue JSON::Schema::ValidationError => ex
raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
end end
end end
def validate_body!(metadata, swagger_doc, body)
response_schema = metadata[:response][:schema]
return if response_schema.nil?
validation_schema = response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(swagger_doc.slice(:definitions))
errors = JSON::Validator.fully_validate(validation_schema, body)
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
end
end end
class UnexpectedResponse < StandardError; end class UnexpectedResponse < StandardError; end

View File

@@ -1,5 +0,0 @@
module Rswag
module Specs
VERSION = '1.0.2'
end
end

View File

@@ -8,7 +8,7 @@ namespace :rswag do
t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
# NOTE: rspec 2.x support # NOTE: rspec 2.x support
if Rswag::Specs::RSPEC_VERSION > 2 if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run
t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ] t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
else else
t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ] t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ]

View File

@@ -1,12 +1,9 @@
$:.push File.expand_path("../lib", __FILE__) $:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require 'rswag/specs/version'
# Describe your gem and declare its dependencies: # Describe your gem and declare its dependencies:
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "rswag-specs" s.name = "rswag-specs"
s.version = Rswag::Specs::VERSION s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ["Richie Morris"]
s.email = ["domaindrivendev@gmail.com"] s.email = ["domaindrivendev@gmail.com"]
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = "https://github.com/domaindrivendev/rswag"
@@ -16,7 +13,7 @@ Gem::Specification.new do |s|
s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ] s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ]
s.add_dependency "rails", ">= 3.1", "< 5.1" s.add_dependency 'activesupport', '>= 3.1', '< 6.0'
s.add_dependency 'railties', '>= 3.1', '< 6.0'
s.add_dependency 'json-schema', '~> 2.2' s.add_dependency 'json-schema', '~> 2.2'
s.add_dependency 'rspec-rails', '>= 2.14', '< 4'
end end

View File

@@ -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

@@ -7,19 +7,30 @@ module Rswag
subject { double('example') } subject { double('example') }
before do before do
subject.extend ExampleHelpers subject.extend(ExampleHelpers)
# Mock out some infrastructure allow(Rswag::Specs).to receive(:config).and_return(config)
stub_const('Rails::VERSION::MAJOR', 3) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
rswag_config = double('rswag_config') stub_const('Rswag::Specs::RAILS_VERSION', 3)
allow(rswag_config).to receive(:get_swagger_doc).and_return(global_metadata)
allow(subject).to receive(:rswag_config).and_return(rswag_config)
end end
let(:api_metadata) do let(:config) { double('config') }
let(:swagger_doc) do
{
securityDefinitions: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
end
let(:metadata) do
{ {
path_item: { template: '/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',
consumes: [ 'application/json' ],
parameters: [ parameters: [
{ name: :blog_id, in: :path, type: 'integer' }, { name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' }, { name: 'id', in: :path, type: 'integer' },
@@ -32,19 +43,8 @@ module Rswag
} }
} }
end end
let(:global_metadata) do
{
securityDefinitions: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
end
describe '#submit_request(api_metadata)' do describe '#submit_request(metadata)' do
before do before do
allow(subject).to receive(:blog_id).and_return(1) allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2) allow(subject).to receive(:id).and_return(2)
@@ -52,14 +52,14 @@ module Rswag
allow(subject).to receive(:api_key).and_return('fookey') allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment') allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put) allow(subject).to receive(:put)
subject.submit_request(api_metadata) subject.submit_request(metadata)
end end
it "submits a request built from metadata and 'let' values" do it "submits a request built from metadata and 'let' values" do
expect(subject).to have_received(:put).with( expect(subject).to have_received(:put).with(
'/blogs/1/comments/2?q1=foo&api_key=fookey', '/blogs/1/comments/2?q1=foo&api_key=fookey',
"{\"text\":\"Some comment\"}", "{\"text\":\"Some comment\"}",
{} { 'CONTENT_TYPE' => 'application/json' }
) )
end end
end end

View File

@@ -4,222 +4,301 @@ module Rswag
module Specs module Specs
describe RequestFactory do describe RequestFactory do
subject { RequestFactory.new(api_metadata, global_metadata) } subject { RequestFactory.new(config) }
before do before do
allow(example).to receive(:blog_id).and_return(1) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
allow(example).to receive(:id).and_return('2')
end end
let(:api_metadata) do let(:config) { double('config') }
{ let(:swagger_doc) { {} }
path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: {
verb: :put,
summary: 'Updates a blog',
parameters: [
{ name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' }
]
}
}
end
let(:global_metadata) { {} }
let(:example) { double('example') } let(:example) { double('example') }
let(:metadata) do
{
path_item: { template: '/blogs' },
operation: { verb: :get }
}
end
describe '#build_fullpath(example)' do describe '#build_request(metadata, example)' do
let(:path) { subject.build_fullpath(example) } let(:request) { subject.build_request(metadata, example) }
context 'always' do it 'builds request hash for given example' do
it "builds a path using metadata and example values" do expect(request[:verb]).to eq(:get)
expect(path).to eq('/blogs/1/comments/2') expect(request[:path]).to eq('/blogs')
end
context "'path' parameters" do
before do
metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}'
metadata[:operation][:parameters] = [
{ name: 'blog_id', in: :path, type: :number },
{ name: 'id', in: :path, type: :number }
]
allow(example).to receive(:blog_id).and_return(1)
allow(example).to receive(:id).and_return(2)
end
it 'builds the path from example values' do
expect(request[:path]).to eq('/blogs/1/comments/2')
end end
end end
context "'query' parameters" do context "'query' parameters" do
before do before do
api_metadata[:operation][:parameters] << { name: 'q1', in: :query, type: 'string' } metadata[:operation][:parameters] = [
api_metadata[:operation][:parameters] << { name: 'q2', in: :query, type: 'string' } { name: 'q1', in: :query, type: :string },
{ name: 'q2', in: :query, type: :string }
]
allow(example).to receive(:q1).and_return('foo') allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar') allow(example).to receive(:q2).and_return('bar')
end end
it "appends a query string using metadata and example values" do it "builds the query string from example values" do
expect(path).to eq('/blogs/1/comments/2?q1=foo&q2=bar') expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end end
end end
context "'query' parameter of type 'array'" do context "'query' parameters of type 'array'" do
before do before do
api_metadata[:operation][:parameters] << { metadata[:operation][:parameters] = [
name: 'things', { name: 'things', in: :query, type: :array, collectionFormat: collection_format }
in: :query, ]
type: :array,
collectionFormat: collectionFormat
}
allow(example).to receive(:things).and_return([ 'foo', 'bar' ]) allow(example).to receive(:things).and_return([ 'foo', 'bar' ])
end end
context 'collectionFormat = csv' do context 'collectionFormat = csv' do
let(:collectionFormat) { :csv } let(:collection_format) { :csv }
it "formats as comma separated values" do it "formats as comma separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo,bar') expect(request[:path]).to eq('/blogs?things=foo,bar')
end end
end end
context 'collectionFormat = ssv' do context 'collectionFormat = ssv' do
let(:collectionFormat) { :ssv } let(:collection_format) { :ssv }
it "formats as space separated values" do it "formats as space separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo bar') expect(request[:path]).to eq('/blogs?things=foo bar')
end end
end end
context 'collectionFormat = tsv' do context 'collectionFormat = tsv' do
let(:collectionFormat) { :tsv } let(:collection_format) { :tsv }
it "formats as tab separated values" do it "formats as tab separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo\tbar') expect(request[:path]).to eq('/blogs?things=foo\tbar')
end end
end end
context 'collectionFormat = pipes' do context 'collectionFormat = pipes' do
let(:collectionFormat) { :pipes } let(:collection_format) { :pipes }
it "formats as pipe separated values" do it "formats as pipe separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo|bar') expect(request[:path]).to eq('/blogs?things=foo|bar')
end end
end end
context 'collectionFormat = multi' do context 'collectionFormat = multi' do
let(:collectionFormat) { :multi } let(:collection_format) { :multi }
it "formats as multiple parameter instances" do it "formats as multiple parameter instances" do
expect(path).to eq('/blogs/1/comments/2?things=foo&things=bar') expect(request[:path]).to eq('/blogs?things=foo&things=bar')
end end
end end
end end
context "global definition for 'api_key in query'" do context "'header' parameters" do
before do before do
global_metadata[:securityDefinitions] = { api_key: { type: :apiKey, name: 'api_key', in: :query } } metadata[:operation][:parameters] = [ { name: 'Api-Key', in: :header, type: :string } ]
allow(example).to receive(:api_key).and_return('fookey') allow(example).to receive(:'Api-Key').and_return('foobar')
end end
context 'global requirement' do it 'adds names and example values to headers' do
before { global_metadata[:security] = [ { api_key: [] } ] } expect(request[:headers]).to eq({ 'Api-Key' => 'foobar' })
it "appends the api_key using metadata and example value" do
expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
end end
end end
context 'operation-specific requirement' do context 'optional parameters not provided' do
before { api_metadata[:operation][:security] = [ { api_key: [] } ] } before do
metadata[:operation][:parameters] = [
it "appends the api_key using metadata and example value" do { name: 'q1', in: :query, type: :string, required: false },
expect(path).to eq('/blogs/1/comments/2?api_key=fookey') { name: 'Api-Key', in: :header, type: :string, required: false }
]
end end
it 'builds request hash without them' do
expect(request[:path]).to eq('/blogs')
expect(request[:headers]).to eq({})
end
end
context "consumes content" do
before do
metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
end
context "no 'Content-Type' provided" do
it "sets 'CONTENT_TYPE' header to first in list" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json')
end
end
context "explicit 'Content-Type' provided" do
before do
allow(example).to receive(:'Content-Type').and_return('application/xml')
end
it "sets 'CONTENT_TYPE' header to example value" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context 'JSON payload' do
before do
metadata[:operation][:parameters] = [ { name: 'comment', in: :body, schema: { type: 'object' } } ]
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it "serializes first 'body' parameter to JSON string" do
expect(request[:payload]).to eq("{\"text\":\"Some comment\"}")
end
end
context 'form payload' do
before do
metadata[:operation][:consumes] = [ 'multipart/form-data' ]
metadata[:operation][:parameters] = [
{ name: 'f1', in: :formData, type: :string },
{ name: 'f2', in: :formData, type: :string }
]
allow(example).to receive(:f1).and_return('foo blah')
allow(example).to receive(:f2).and_return('bar blah')
end
it 'sets payload to hash of names and example values' do
expect(request[:payload]).to eq(
'f1' => 'foo blah',
'f2' => 'bar blah'
)
end
end
end
context 'produces content' do
before do
metadata[:operation][:produces] = [ 'application/json', 'application/xml' ]
end
context "no 'Accept' value provided" do
it "sets 'HTTP_ACCEPT' header to first in list" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json')
end
end
context "explicit 'Accept' value provided" do
before do
allow(example).to receive(:'Accept').and_return('application/xml')
end
it "sets 'HTTP_ACCEPT' header to example value" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml')
end
end
end
context 'apiKey' do
before do
swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } }
metadata[:operation][:security] = [ apiKey: [] ]
allow(example).to receive(:api_key).and_return('foobar')
end
context 'in query' do
let(:key_location) { :query }
it 'adds name and example value to the query string' do
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
context 'in header' do
let(:key_location) { :header }
it 'adds name and example value to the headers' do
expect(request[:headers]).to eq('api_key' => 'foobar')
end
end
end
context 'basic auth' do
before do
swagger_doc[:securityDefinitions] = { basic: { type: :basic } }
metadata[:operation][:security] = [ basic: [] ]
allow(example).to receive(:Authorization).and_return('Basic foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
end
end
context 'oauth2' do
before do
swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: [ 'read:blogs' ] } }
metadata[:operation][:security] = [ oauth2: [ 'read:blogs' ] ]
allow(example).to receive(:Authorization).and_return('Bearer foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar')
end
end
context "path-level parameters" do
before do
metadata[:operation][:parameters] = [ { name: 'q1', in: :query, type: :string } ]
metadata[:path_item][:parameters] = [ { name: 'q2', in: :query, type: :string } ]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar')
end
it "populates operation and path level parameters " do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
end
context 'referenced parameters' do
before do
swagger_doc[:parameters] = { 'q1' => { name: 'q1', in: :query, type: :string } }
metadata[:operation][:parameters] = [ { '$ref' => '#/parameters/q1' } ]
allow(example).to receive(:q1).and_return('foo')
end
it 'uses the referenced metadata to build the request' do
expect(request[:path]).to eq('/blogs?q1=foo')
end end
end end
context 'global basePath' do context 'global basePath' do
before { global_metadata[:basePath] = '/foobar' } before { swagger_doc[:basePath] = '/api' }
it 'prepends the basePath' do it 'prepends to the path' do
expect(path).to eq('/foobar/blogs/1/comments/2') expect(request[:path]).to eq('/api/blogs')
end end
end end
context "defined at the 'path' level" do context "global consumes" do
before { swagger_doc[:consumes] = [ 'application/xml' ] }
it "defaults 'CONTENT_TYPE' to global value(s)" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context "global security requirements" do
before do before do
api_metadata[:path_item][:parameters] = [ { name: :blog_id, in: :path } ] swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } }
api_metadata[:operation][:parameters] = [ { name: :id, in: :path } ] swagger_doc[:security] = [ apiKey: [] ]
allow(example).to receive(:api_key).and_return('foobar')
end end
it "builds path from parameters defined at path and operation levels" do it 'applieds the scheme by default' do
expect(path).to eq('/blogs/1/comments/2') expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
end
describe '#build_body(example)' do
let(:body) { subject.build_body(example) }
context "no 'body' parameter" do
it "returns ''" do
expect(body).to eq('')
end
end
context "'body' parameter" do
before do
api_metadata[:operation][:parameters] << { name: 'comment', in: :body, schema: { type: 'object' } }
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it 'returns the example value as a json string' do
expect(body).to eq("{\"text\":\"Some comment\"}")
end
end
context "referenced 'body' parameter" do
before do
api_metadata[:operation][:parameters] << { '$ref' => '#/parameters/comment' }
global_metadata[:parameters] = {
'comment' => { name: 'comment', in: :body, schema: { type: 'object' } }
}
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it 'returns the example value as a json string' do
expect(body).to eq("{\"text\":\"Some comment\"}")
end
end
end
describe '#build_headers' do
let(:headers) { subject.build_headers(example) }
context "no 'header' params" do
it 'returns an empty hash' do
expect(headers).to eq({})
end
end
context "'header' params" do
before do
api_metadata[:operation][:parameters] << { name: 'Api-Key', in: :header, type: 'string' }
allow(example).to receive(:'Api-Key').and_return('foobar')
end
it 'returns a hash of names with example values' do
expect(headers).to eq({ 'Api-Key' => 'foobar' })
end
end
context 'consumes & produces' do
before do
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
api_metadata[:operation][:produces] = [ 'application/json', 'application/xml' ]
end
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'ACCEPT' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml'
)
end
end
context 'global consumes & produces' do
let(:global_metadata) do
{
consumes: [ 'application/json', 'application/xml' ],
produces: [ 'application/json', 'application/xml' ]
}
end
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'ACCEPT' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml'
)
end end
end end
end end

View File

@@ -4,66 +4,71 @@ module Rswag
module Specs module Specs
describe ResponseValidator do describe ResponseValidator do
subject { ResponseValidator.new(api_metadata, global_metadata) } subject { ResponseValidator.new(config) }
let(:api_metadata) { { response: { code: 200 } } }
let(:global_metadata) { {} }
describe '#validate!(response)' do
let(:call) { subject.validate!(response) }
context "no 'schema' provided" do
context 'response code matches' do
let(:response) { OpenStruct.new(code: 200, body: '') }
it { expect { call }.to_not raise_error }
end
context 'response code does not match' do
let(:response) { OpenStruct.new(code: 201, body: '') }
it { expect { call }.to raise_error UnexpectedResponse }
end
end
context "'schema' provided" do
before do before do
api_metadata[:response][:schema] = { allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
type: 'object', end
properties: { text: { type: 'string' } }, let(:config) { double('config') }
let(:swagger_doc) { {} }
let(:example) { double('example') }
let(:metadata) do
{
response: {
code: 200,
headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
schema: {
type: :object,
properties: { text: { type: :string } },
required: [ 'text' ] required: [ 'text' ]
} }
}
}
end end
context 'response code & body matches' do describe '#validate!(metadata, response)' do
let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some comment\"}") } let(:call) { subject.validate!(metadata, response) }
let(:response) do
OpenStruct.new(
code: '200',
headers: { 'X-Rate-Limit-Limit' => '10' },
body: "{\"text\":\"Some comment\"}"
)
end
context "response matches metadata" do
it { expect { call }.to_not raise_error } it { expect { call }.to_not raise_error }
end end
context 'response code matches & body does not' do context "response code differs from metadata" do
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some comment\"}") } before { response.code = '400' }
it { expect { call }.to raise_error UnexpectedResponse } it { expect { call }.to raise_error /Expected response code/ }
end
end end
context "referenced 'schema' provided" do context "response headers differ from metadata" do
before { response.headers = {} }
it { expect { call }.to raise_error /Expected response header/ }
end
context "response body differs from metadata" do
before { response.body = "{\"foo\":\"Some comment\"}" }
it { expect { call }.to raise_error /Expected response body/ }
end
context 'referenced schemas' do
before do before do
api_metadata[:response][:schema] = { '$ref' => '#/definitions/author' } swagger_doc[:definitions] = {
global_metadata[:definitions] = { 'blog' => {
author: { type: :object,
type: 'object', properties: { foo: { type: :string } },
properties: { name: { type: 'string' } }, required: [ 'foo' ]
required: [ 'name' ]
} }
} }
metadata[:response][:schema] = { '$ref' => '#/definitions/blog' }
end end
context 'response code & body matches' do it 'uses the referenced schema to validate the response body' do
let(:response) { OpenStruct.new(code: 200, body: "{\"name\":\"Some name\"}") } expect { call }.to raise_error /Expected response body/
it { expect { call }.to_not raise_error }
end
context 'response code matches & body does not' do
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some name\"}") }
it { expect { call }.to raise_error UnexpectedResponse }
end end
end end
end end

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,4 +1,3 @@
require 'rswag/ui/version'
require 'rswag/ui/configuration' require 'rswag/ui/configuration'
require 'rswag/ui/engine' require 'rswag/ui/engine'

View File

@@ -5,7 +5,14 @@ module Rswag
initializer 'rswag-ui.initialize' do |app| initializer 'rswag-ui.initialize' do |app|
if app.config.respond_to?(:assets) if app.config.respond_to?(:assets)
app.config.assets.precompile += [ 'swagger-ui/*' ] app.config.assets.precompile += [
'swagger-ui/css/*',
'swagger-ui/fonts/*',
'swagger-ui/images/*',
'swagger-ui/lang/*',
'swagger-ui/lib/*',
'swagger-ui/swagger-ui.min.js'
]
end end
end end
end end

View File

@@ -1,5 +0,0 @@
module Rswag
module Ui
VERSION = '1.0.2'
end
end

View File

@@ -1,12 +1,9 @@
$:.push File.expand_path("../lib", __FILE__) $:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require 'rswag/ui/version'
# Describe your gem and declare its dependencies: # Describe your gem and declare its dependencies:
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "rswag-ui" s.name = "rswag-ui"
s.version = Rswag::Ui::VERSION s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ["Richie Morris"]
s.email = ["domaindrivendev@gmail.com"] s.email = ["domaindrivendev@gmail.com"]
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = "https://github.com/domaindrivendev/rswag"
@@ -16,5 +13,6 @@ Gem::Specification.new do |s|
s.files = Dir["{app,config,lib,vendor}/**/*"] + ["MIT-LICENSE", "Rakefile" ] s.files = Dir["{app,config,lib,vendor}/**/*"] + ["MIT-LICENSE", "Rakefile" ]
s.add_dependency "rails", ">= 3.1", "< 5.1" s.add_dependency 'actionpack', '>=3.1', '< 6.0'
s.add_dependency 'railties', '>= 3.1', '< 6.0'
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,4 +1,3 @@
require 'rswag/version'
require 'rswag/specs' require 'rswag/specs'
require 'rswag/api' require 'rswag/api'
require 'rswag/ui' require 'rswag/ui'

View File

@@ -1,3 +0,0 @@
module Rswag
VERSION = '1.0.2'
end

View File

@@ -1,12 +1,9 @@
$:.push File.expand_path("../lib", __FILE__) $:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require 'rswag/version'
# Describe your gem and declare its dependencies: # Describe your gem and declare its dependencies:
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "rswag" s.name = "rswag"
s.version = Rswag::VERSION s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ["Richie Morris"]
s.email = ["domaindrivendev@gmail.com"] s.email = ["domaindrivendev@gmail.com"]
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = "https://github.com/domaindrivendev/rswag"
@@ -16,7 +13,7 @@ Gem::Specification.new do |s|
s.files = Dir["{lib}/**/*"] + [ "MIT-LICENSE" ] s.files = Dir["{lib}/**/*"] + [ "MIT-LICENSE" ]
s.add_dependency 'rswag-specs', Rswag::VERSION s.add_dependency 'rswag-specs', ENV['TRAVIS_TAG'] || '0.0.0'
s.add_dependency 'rswag-api', Rswag::VERSION s.add_dependency 'rswag-api', ENV['TRAVIS_TAG'] || '0.0.0'
s.add_dependency 'rswag-ui', Rswag::VERSION s.add_dependency 'rswag-ui', ENV['TRAVIS_TAG'] || '0.0.0'
end end

View File

@@ -1,7 +1,12 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead. # For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception protect_from_forgery with: :null_session
wrap_parameters format: [ :json ] wrap_parameters format: [ :json ]
respond_to :json
rescue_from 'ActionController::UnknownFormat' do |ex|
head :not_acceptable
end
end end

View File

@@ -0,0 +1,11 @@
class AuthTestsController < ApplicationController
# POST /auth-tests/basic
def basic
if authenticate_with_http_basic { |u, p| u == 'jsmith' && p == 'jspass' }
head :no_content
else
request_http_basic_authentication
end
end
end

View File

@@ -1,6 +1,6 @@
require 'fileutils'
class BlogsController < ApplicationController class BlogsController < ApplicationController
wrap_parameters Blog
respond_to :json
# POST /blogs # POST /blogs
def create def create
@@ -8,6 +8,14 @@ class BlogsController < ApplicationController
respond_with @blog respond_with @blog
end end
# Put /blogs/1
def upload
@blog = Blog.find_by_id(params[:id])
return head :not_found if @blog.nil?
@blog.thumbnail = save_uploaded_file params[:file]
head @blog.save ? :ok : :unprocsessible_entity
end
# GET /blogs # GET /blogs
def index def index
@blogs = Blog.all @blogs = Blog.all
@@ -17,7 +25,20 @@ 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
private
def save_uploaded_file(field)
return if field.nil?
file = File.join('tmp', field.original_filename)
FileUtils.cp field.tempfile.path, file
field.original_filename
end
end end

View File

@@ -5,7 +5,8 @@ class Blog < ActiveRecord::Base
{ {
id: id, id: id,
title: title, title: title,
content: content content: nil,
thumbnail: thumbnail
} }
end end
end end

View File

@@ -1,5 +1,5 @@
require 'rubygems' require 'rubygems'
gemfile = File.expand_path('../../../../Gemfile', __FILE__) gemfile = File.expand_path('../../../Gemfile', __FILE__)
if File.exist?(gemfile) if File.exist?(gemfile)
ENV['BUNDLE_GEMFILE'] = gemfile ENV['BUNDLE_GEMFILE'] = gemfile

View File

@@ -28,4 +28,6 @@ TestApp::Application.configure do
# Expands the lines which load the assets # Expands the lines which load the assets
config.assets.debug = true config.assets.debug = true
config.eager_load = false
end end

View File

@@ -32,4 +32,6 @@ TestApp::Application.configure do
# Print deprecation notices to the stderr # Print deprecation notices to the stderr
config.active_support.deprecation = :stderr config.active_support.deprecation = :stderr
config.eager_load = false
end end

View File

@@ -5,3 +5,6 @@
# Make sure the secret is at least 30 characters and all random, # Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks. # no regular words or you'll be exposed to dictionary attacks.
TestApp::Application.config.secret_token = '60f36cd33756d73f362053f1d45256ae50d75440b634ae73b070a6e35a2df38692f59e28e5ecbd1f9f2e850255f6d29a468bc59ac4484c2b7f0548ddbfc1b870' TestApp::Application.config.secret_token = '60f36cd33756d73f362053f1d45256ae50d75440b634ae73b070a6e35a2df38692f59e28e5ecbd1f9f2e850255f6d29a468bc59ac4484c2b7f0548ddbfc1b870'
# See http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#config-secrets-yml
TestApp::Application.config.secret_key_base = 'f6a820cc8aa76094583cd68ef46a735e25e3278648086355f8bd24721f036959c728c06a28dcecfe695f17ae2db44dfa1424f22b81377f2a1496d4e19f6f7faa'

View File

@@ -1,5 +1,8 @@
TestApp::Application.routes.draw do TestApp::Application.routes.draw do
resources :blogs, defaults: { :format => :json } resources :blogs
put '/blogs/:id/upload', to: 'blogs#upload'
post 'auth-tests/basic', to: 'auth_tests#basic'
mount Rswag::Api::Engine => 'api-docs' mount Rswag::Api::Engine => 'api-docs'
mount Rswag::Ui::Engine => 'api-docs' mount Rswag::Ui::Engine => 'api-docs'

View File

@@ -1,8 +1,15 @@
class CreateBlogs < ActiveRecord::Migration migration_class = if Gem::Version.new(Rails.version) >= Gem::Version.new("5.0")
ActiveRecord::Migration[4.2]
else
ActiveRecord::Migration
end
class CreateBlogs < migration_class
def change def change
create_table :blogs do |t| create_table :blogs do |t|
t.string :title t.string :title
t.text :content t.text :content
t.string :thumbnail
t.timestamps t.timestamps
end end

View File

@@ -1,4 +1,3 @@
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead # This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to # of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition. # incrementally modify your database, and then regenerate this schema definition.
@@ -9,15 +8,16 @@
# from scratch. The latter is a flawed and unsustainable approach (the more migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues). # you'll amass, the slower it'll run and the greater likelihood for issues).
# #
# It's strongly recommended to check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(:version => 20160218212104) do ActiveRecord::Schema.define(version: 20160218212104) do
create_table "blogs", :force => true do |t| create_table "blogs", force: :cascade do |t|
t.string "title" t.string "title"
t.text "content" t.text "content"
t.datetime "created_at", :null => false t.string "thumbnail"
t.datetime "updated_at", :null => false t.datetime "created_at"
t.datetime "updated_at"
end end
end 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

BIN
test-app/spec/fixtures/thumbnail.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,22 @@
require 'swagger_helper'
describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do
path '/auth-tests/basic' do
post 'Authenticates with basic auth' do
tags 'Auth Test'
operationId 'testBasicAuth'
security [ basic_auth: [] ]
response '204', 'Valid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
run_test!
end
response '401', 'Invalid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('foo:bar')}" }
run_test!
end
end
end
end

View File

@@ -9,10 +9,12 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
description 'Creates a new blog from provided data' description 'Creates a new blog from provided data'
operationId 'createBlog' operationId 'createBlog'
consumes 'application/json' consumes 'application/json'
parameter name: :blog, :in => :body, schema: { '$ref' => '#/definitions/blog' } produces 'application/json'
parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' }
let(:blog) { { title: 'foo', content: 'bar' } }
response '201', 'blog created' do response '201', 'blog created' do
let(:blog) { { title: 'foo', content: 'bar' } }
run_test! run_test!
end end
@@ -20,7 +22,9 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/definitions/errors_object'
let(:blog) { { title: 'foo' } } let(:blog) { { title: 'foo' } }
run_test! run_test! do |response|
expect(response.body).to include("can't be blank")
end
end end
end end
@@ -31,17 +35,24 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
produces 'application/json' produces 'application/json'
parameter name: :keywords, in: :query, type: 'string' parameter name: :keywords, in: :query, type: 'string'
let(:keywords) { 'foo bar' }
response '200', 'success' do response '200', 'success' do
schema type: 'array', items: { '$ref' => '#/definitions/blog' } schema type: 'array', items: { '$ref' => '#/definitions/blog' }
end
let(:keywords) { 'foo bar' } response '406', 'unsupported accept header' do
let(:'Accept') { 'application/foo' }
run_test! run_test!
end end
end end
end end
path '/blogs/{id}' do path '/blogs/{id}' do
parameter name: :id, :in => :path, :type => :string parameter name: :id, in: :path, type: :string
let(:id) { blog.id }
let(:blog) { Blog.create(title: 'foo', content: 'bar', thumbnail: 'thumbnail.png') }
get 'Retrieves a blog' do get 'Retrieves a blog' do
tags 'Blogs' tags 'Blogs'
@@ -50,9 +61,19 @@ 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'
let(:blog) { Blog.create(title: 'foo', content: 'bar') } examples 'application/json' => {
id: 1,
title: 'Hello world!',
content: 'Hello world and hello universe. Thank you all very much!!!',
thumbnail: "thumbnail.png"
}
let(:id) { blog.id } let(:id) { blog.id }
run_test! run_test!
end end
@@ -63,4 +84,24 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
end end
end end
end end
path '/blogs/{id}/upload' do
parameter name: :id, in: :path, type: :string
let(:id) { blog.id }
let(:blog) { Blog.create(title: 'foo', content: 'bar') }
put 'upload a blog thumbnail' do
tags 'Blogs'
description 'Upload a thumbnail for specific blog by id'
operationId 'uploadThumbnailBlog'
consumes 'multipart/form-data'
parameter name: :file, :in => :formData, :type => :file, required: true
response '200', 'blog updated' do
let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) }
run_test!
end
end
end
end end

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

@@ -47,6 +47,10 @@ RSpec.configure do |config|
# triggering implicit auto-inclusion in groups with matching metadata. # triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups config.shared_context_metadata_behavior = :apply_to_host_groups
config.after(:suite) do
File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png")
end
# The settings below are suggested to provide a good initial experience # The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content. # with RSpec, but feel free to customize to your heart's content.
=begin =begin

View File

@@ -39,12 +39,16 @@ RSpec.configure do |config|
properties: { properties: {
id: { type: 'integer' }, id: { type: 'integer' },
title: { type: 'string' }, title: { type: 'string' },
content: { type: 'string' } content: { type: 'string', 'x-nullable': true },
thumbnail: { type: 'string'}
}, },
required: [ 'id', 'title', 'content' ] required: [ 'id', 'title', 'content', 'thumbnail' ]
} }
}, },
securityDefinitions: { securityDefinitions: {
basic_auth: {
type: :basic
},
api_key: { api_key: {
type: :apiKey, type: :apiKey,
name: 'api_key', name: 'api_key',

View File

@@ -5,6 +5,30 @@
"version": "v1" "version": "v1"
}, },
"paths": { "paths": {
"/auth-tests/basic": {
"post": {
"summary": "Authenticates with basic auth",
"tags": [
"Auth Test"
],
"operationId": "testBasicAuth",
"security": [
{
"basic_auth": [
]
}
],
"responses": {
"204": {
"description": "Valid credentials"
},
"401": {
"description": "Invalid credentials"
}
}
}
},
"/blogs": { "/blogs": {
"post": { "post": {
"summary": "Creates a blog", "summary": "Creates a blog",
@@ -16,6 +40,9 @@
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [
"application/json"
],
"parameters": [ "parameters": [
{ {
"name": "blog", "name": "blog",
@@ -55,14 +82,8 @@
} }
], ],
"responses": { "responses": {
"200": { "406": {
"description": "success", "description": "unsupported accept header"
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/blog"
}
}
} }
} }
} }
@@ -89,8 +110,27 @@
"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!!!",
"thumbnail": "thumbnail.png"
}
} }
}, },
"404": { "404": {
@@ -98,6 +138,40 @@
} }
} }
} }
},
"/blogs/{id}/upload": {
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"required": true
}
],
"put": {
"summary": "upload a blog thumbnail",
"tags": [
"Blogs"
],
"description": "Upload a thumbnail for specific blog by id",
"operationId": "uploadThumbnailBlog",
"consumes": [
"multipart/form-data"
],
"parameters": [
{
"name": "file",
"in": "formData",
"type": "file",
"required": true
}
],
"responses": {
"200": {
"description": "blog updated"
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -128,17 +202,25 @@
"type": "string" "type": "string"
}, },
"content": { "content": {
"type": "string",
"x-nullable": true
},
"thumbnail": {
"type": "string" "type": "string"
} }
}, },
"required": [ "required": [
"id", "id",
"title", "title",
"content" "content",
"thumbnail"
] ]
} }
}, },
"securityDefinitions": { "securityDefinitions": {
"basic_auth": {
"type": "basic"
},
"api_key": { "api_key": {
"type": "apiKey", "type": "apiKey",
"name": "api_key", "name": "api_key",