Merge branch 'origin/master' into feat/add-basic-auth

This commit is contained in:
Kabiru Mwenja 2020-01-06 10:00:46 +03:00
commit 6e706c04d7
No known key found for this signature in database
GPG Key ID: 6D6C5615C3E00288
47 changed files with 620 additions and 92 deletions

17
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,17 @@
## Describe the bug
A clear and concise description of what the bug is.
## Steps to Test or Reproduce
Please provide an example repo or the steps to reproduce the behavior.
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Additional context
Add any other context about the problem here.
## Rswag Version
The version of rswag are you using.

View File

@ -0,0 +1,11 @@
## Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is.
## Describe the solution you'd like
A clear and concise description of what you want to happen.
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,15 @@
## Problem
A clear and concise description of what the problem is.
## Solution
A clear and concise description of what the solution is.
### Related Issues
Links to any related issues.
### Checklist
- [ ] Added tests
- [ ] Changelog updated
### Steps to Test or Reproduce
Outline the steps to test or reproduce the PR here.

View File

@ -1 +1 @@
2.3.1 2.6.3

View File

@ -1,30 +1,35 @@
language: ruby language: ruby
dist: bionic
services:
- xvfb
rvm: rvm:
- 2.2.5 - 2.6.3
env: env:
- RAILS_VERSION=6.0.0
- RAILS_VERSION=5.2.0 - RAILS_VERSION=5.2.0
- RAILS_VERSION=4.2.0
- RAILS_VERSION=3.2.22 addons:
apt:
packages:
- libqtwebkit-dev
- libqtwebkit4
cache: cache:
directories: directories:
- /home/travis/.rvm/gems/ruby-2.2.5 - /home/travis/.rvm/gems/ruby-2.6.3
install: ./ci/build.sh install: ./ci/build.sh
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
script: ./ci/test.sh script: ./ci/test.sh
jobs: jobs:
include: include:
- stage: publish components - stage: publish components
script: 'cd rswag-api' script: 'cd rswag-api'
if: tag IS present
deploy: deploy:
gemspec: rswag-api.gemspec gemspec: rswag-api.gemspec
provider: rubygems provider: rubygems
@ -35,6 +40,7 @@ jobs:
- stage: publish components - stage: publish components
script: 'cd rswag-specs' script: 'cd rswag-specs'
if: tag IS present
deploy: deploy:
gemspec: rswag-specs.gemspec gemspec: rswag-specs.gemspec
provider: rubygems provider: rubygems
@ -45,6 +51,7 @@ jobs:
- stage: publish components - stage: publish components
script: 'cd rswag-ui' script: 'cd rswag-ui'
if: tag IS present
deploy: deploy:
gemspec: rswag-ui.gemspec gemspec: rswag-ui.gemspec
provider: rubygems provider: rubygems
@ -56,6 +63,7 @@ jobs:
- stage: publish rswag - stage: publish rswag
script: 'cd rswag' script: 'cd rswag'
if: tag IS present
deploy: deploy:
gemspec: rswag.gemspec gemspec: rswag.gemspec
provider: rubygems provider: rubygems

39
CHANGELOG.md Normal file
View File

@ -0,0 +1,39 @@
# rswag
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
## [2.2.0] - 2019-11-01
### Added
- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251)
### Changed
- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251)
## [2.1.1] - 2019-10-18
### Fixed
- Fix incorrect require reference for swagger_generator [#248](https://github.com/rswag/rswag/issues/248)
## [2.1.0] - 2019-10-17
### Added
- New Spec Generator [#75](https://github.com/rswag/rswag/pull/75)
- Support for Options and Trace verbs; You must use a framework that supports this, for Options Rails 6.1+ Rails 6 does not support Trace. [#237](https://github.com/rswag/rswag/pull/75)
### Changed
- Update swagger-ui to 3.18.2 [#240](https://github.com/rswag/rswag/pull/240)
## [2.0.6] - 2019-10-03
### Added
- Support for Rails 6 [#228](https://github.com/rswag/rswag/pull/228)
- Support for Windows paths [#176](https://github.com/rswag/rswag/pull/176)
### Changed
- Show response body when error code is not expected [#117](https://github.com/rswag/rswag/pull/177)
## [2.0.5] - 2018-07-10

59
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,59 @@
# Contributing
## Fork, then clone the repo:
```
git clone git@github.com:rswag/rswag.git
cd rswag
```
## Build
Set up your machine:
```
./ci/build.sh
```
Or manually
```
bundle
cd test-app
bundle exec rake db:setup
cd -
cd rswag-ui
npm install
cd -
```
## Test
Make sure the tests pass:
```
./ci/test.sh
```
or manually
```
cd test-app
bundle exec rspec
```
Make your change. Add tests for your change. Make the tests pass:
```
bundle exec rspec
```
Push to your fork and [submit a Pull Request][pr].
[pr]: https://github.com/rswag/rswag/compare/
## Release
(for maintainers)
Update the changelog.md, putting the new version number in and moving the Unreleased marker.
Merge the changes into master you wish to release.
Add and push a new git tag, annotated tags preferred:
```
git tag -s 2.0.6 -m 'v2.0.6'
```
Travis will detect the tag and release all gems with that tag version number.

View File

@ -9,11 +9,16 @@ gem 'rails', "#{rails_version}"
case rails_version.split('.').first case rails_version.split('.').first
when '3' when '3'
gem 'strong_parameters' gem 'strong_parameters'
when '4', '5' when '4', '5', '6'
gem 'responders' gem 'responders'
end end
gem 'sqlite3' case rails_version.split('.').first
when '3', '4', '5'
gem 'sqlite3', '~> 1.3.6'
when '6'
gem 'sqlite3', '~> 1.4.1'
end
gem 'rswag-api', path: './rswag-api' gem 'rswag-api', path: './rswag-api'
gem 'rswag-ui', path: './rswag-ui' gem 'rswag-ui', path: './rswag-ui'

100
README.md
View File

@ -1,10 +1,11 @@
rswag rswag
========= =========
[![Build Status](https://travis-ci.org/domaindrivendev/rswag.svg?branch=master)](https://travis-ci.org/domaindrivendev/rswag) [![Build Status](https://travis-ci.org/rswag/rswag.svg?branch=master)](https://travis-ci.org/rswag/rswag)
[![Maintainability](https://api.codeclimate.com/v1/badges/1175b984edc4610f82ab/maintainability)](https://codeclimate.com/github/rswag/rswag/maintainability)
[Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests. [Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests.
Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed JSON. This toolchain makes it seamless to go from integration specs, which youre probably doing in some form already, to living documentation for your API consumers. Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which youre probably doing in some form already, to living documentation for your API consumers.
And that's not all ... And that's not all ...
@ -14,9 +15,9 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
|Rswag Version|Swagger (OpenAPI) Spec.|swagger-ui| |Rswag Version|Swagger (OpenAPI) Spec.|swagger-ui|
|----------|----------|----------| |----------|----------|----------|
|[master](https://github.com/domaindrivendev/rswag/tree/master)|2.0|3.17.3| |[master](https://github.com/rswag/rswag/tree/master)|2.0|3.18.2|
|[2.0.5](https://github.com/domaindrivendev/rswag/tree/2.0.4)|2.0|3.17.3| |[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2|
|[1.6.0](https://github.com/domaindrivendev/rswag/tree/1.6.0)|2.0|2.2.5| |[1.6.0](https://github.com/rswag/rswag/tree/1.6.0)|2.0|2.2.5|
## Getting Started ## ## Getting Started ##
@ -26,14 +27,15 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
gem 'rswag' gem 'rswag'
``` ```
or if you like to avoid loading rspec in other bundler groups. or if you like to avoid loading rspec in other bundler groups load the rswag-specs component separately.
Note: Adding it to the :development group is not strictly necessary, but without it, generators and rake tasks must be preceded by RAILS_ENV=test.
```ruby ```ruby
# Gemfile # Gemfile
gem 'rswag-api' gem 'rswag-api'
gem 'rswag-ui' gem 'rswag-ui'
group :test do group :development, :test do
gem 'rspec-rails' gem 'rspec-rails'
gem 'rswag-specs' gem 'rswag-specs'
end end
@ -48,7 +50,8 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
Or run the install generators for each package separately if you installed Rswag as separate gems, as indicated above: Or run the install generators for each package separately if you installed Rswag as separate gems, as indicated above:
```ruby ```ruby
rails g rswag:api:install rswag:ui:install rails g rswag:api:install
rails g rswag:ui:install
RAILS_ENV=test rails g rswag:specs:install RAILS_ENV=test rails g rswag:specs:install
``` ```
@ -120,12 +123,17 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
end end
``` ```
There is also a generator which can help get you started `rails generate rspec:swagger API::MyController`
4. Generate the Swagger JSON file(s) 4. Generate the Swagger JSON file(s)
```ruby ```ruby
rake rswag:specs:swaggerize rake rswag:specs:swaggerize
``` ```
This common command is also aliased as `rake rswag`.
5. Spin up your app and check out the awesome, auto-generated docs at _/api-docs_! 5. Spin up your app and check out the awesome, auto-generated docs at _/api-docs_!
## The rspec DSL ## ## The rspec DSL ##
@ -185,7 +193,7 @@ describe 'Blogs API' do
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. *Note:* OAI v3 has a nullable property. Rswag will work to support this soon. 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> <https://github.com/OAI/OpenAPI-Specification/issues/229#issuecomment-280376087>
### Global Metadata ### ### Global Metadata ###
@ -202,16 +210,18 @@ RSpec.configure do |config|
swagger: '2.0', swagger: '2.0',
info: { info: {
title: 'API V1', title: 'API V1',
version: 'v1' version: 'v1',
description: 'This is the first version of my API'
}, },
basePath: '/api/v1' basePath: '/api/v1'
}, },
'v2/swagger.json' => { 'v2/swagger.yaml' => {
swagger: '2.0', openapi: '3.0.0',
info: { info: {
title: 'API V2', title: 'API V2',
version: 'v2' version: 'v2',
description: 'This is the second version of my API'
}, },
basePath: '/api/v2' basePath: '/api/v2'
} }
@ -219,11 +229,12 @@ RSpec.configure do |config|
end end
``` ```
__NOTE__: By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If you're using multiple documents, you'll need to tag the individual specs with their target document name: #### Supporting multiple versions of API ####
By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If your API has multiple versions, you should be using separate documents to describe each of them. In order to assign a file with a given version of API, you'll need to add the ```swagger_doc``` tag to each spec specifying its target document name:
```ruby ```ruby
# spec/integration/v2/blogs_spec.rb # spec/integration/v2/blogs_spec.rb
describe 'Blogs API', swagger_doc: 'v2/swagger.json' do describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do
path '/blogs' do path '/blogs' do
... ...
@ -233,6 +244,25 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do
end end
``` ```
#### Formatting the description literals: ####
Swagger supports the Markdown syntax to format strings. This can be especially handy if you were to provide a long description of a given API version or endpoint. Use [this guide](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) for reference.
__NOTE:__ There is one difference between the official Markdown syntax and Swagger interpretation, namely tables. To create a table like this:
| Column1 | Collumn2 |
| ------- | -------- |
| cell1 | cell2 |
you should use the folowing syntax, making sure there are no whitespaces at the start of any of the lines:
```
&#13;
| Column1 | Collumn2 |&#13;
| ------- | -------- |&#13;
| cell1 | cell2 |&#13;
&#13;
```
### Specifying/Testing API Security ### ### 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. 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.
@ -309,6 +339,15 @@ end
__NOTE__: If you do change this, you'll also need to update the rswag-api.rb initializer (assuming you're using rswag-api). More on this later. __NOTE__: If you do change this, you'll also need to update the rswag-api.rb initializer (assuming you're using rswag-api). More on this later.
### Input Location for Rspec Tests ###
By default, rswag will search for integration tests in _spec/requests_, _spec/api_ and _spec/integration_. If you want to use tests from other locations, provide the PATTERN argument to rake:
```ruby
# search for tests in spec/swagger
rake rswag:specs:swaggerize PATTERN="spec/swagger/**/*_spec.rb"
```
### Referenced Parameters and Schema Definitions ### ### Referenced Parameters and Schema Definitions ###
Swagger allows you to describe JSON structures inline with your operation descriptions OR as referenced globals. For example, you might have a standard response structure for all failed operations. Rather than repeating the schema in every operation spec, you can define it globally and provide a reference to it in each spec: Swagger allows you to describe JSON structures inline with your operation descriptions OR as referenced globals. For example, you might have a standard response structure for all failed operations. Rather than repeating the schema in every operation spec, you can define it globally and provide a reference to it in each spec:
@ -421,6 +460,35 @@ RSpec.configure do |config|
config.swagger_dry_run = false config.swagger_dry_run = false
end end
``` ```
### Running tests without documenting ###
If you want to use Rswag for testing without adding it to you swagger docs, you can provide the document tag:
```ruby
describe 'Blogs API' do
path '/blogs/{blog_id}' do
get 'Retrieves a blog' do
# documentation is now disabled for this response only
response 200, 'blog found', document: false do
...
```
You can also reenable documentation for specific responses only:
```ruby
# documentation is now disabled
describe 'Blogs API', document: false do
path '/blogs/{blog_id}' do
get 'Retrieves a blog' do
# documentation is reenabled for this response only
response 200, 'blog found', document: true do
...
end
response 401, 'special case' do
...
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_:
@ -464,7 +532,7 @@ Rswag::Api.configure do |c|
end 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 Authorization header and remove operations based on user permissions.
### Enable Swagger Endpoints for swagger-ui ### ### Enable Swagger Endpoints for swagger-ui ###

View File

@ -1,4 +1,6 @@
require 'json' require 'json'
require 'yaml'
require 'rack/mime'
module Rswag module Rswag
module Api module Api
@ -14,13 +16,15 @@ module Rswag
filename = "#{@config.resolve_swagger_root(env)}/#{path}" filename = "#{@config.resolve_swagger_root(env)}/#{path}"
if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) if env['REQUEST_METHOD'] == 'GET' && File.file?(filename)
swagger = load_json(filename) swagger = parse_file(filename)
@config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil?
mime = Rack::Mime.mime_type(::File.extname(path), 'text/plain')
body = unload_swagger(filename, swagger)
return [ return [
'200', '200',
{ 'Content-Type' => 'application/json' }, { 'Content-Type' => mime },
[ JSON.dump(swagger) ] [ body ]
] ]
end end
@ -29,9 +33,29 @@ module Rswag
private private
def parse_file(filename)
if /\.ya?ml$/ === filename
load_yaml(filename)
else
load_json(filename)
end
end
def load_yaml(filename)
YAML.safe_load(File.read(filename))
end
def load_json(filename) def load_json(filename)
JSON.parse(File.read(filename)) JSON.parse(File.read(filename))
end end
def unload_swagger(filename, swagger)
if /\.ya?ml$/ === filename
YAML.dump(swagger)
else
JSON.dump(swagger)
end
end
end end
end end
end end

View File

@ -13,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 'railties', '>= 3.1', '< 6.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'
end end

View File

@ -0,0 +1,5 @@
swagger: '2.0'
info:
title: API V1
version: v1
paths: {}

View File

@ -76,6 +76,21 @@ module Rswag
expect(response[2].join).to include('"host":"tempuri.org"') expect(response[2].join).to include('"host":"tempuri.org"')
end end
end end
context 'when a path maps to a yaml swagger file' do
let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.yml') }
it 'returns a 200 status' do
expect(response.length).to eql(3)
expect(response.first).to eql('200')
end
it 'returns contents of the swagger file' do
expect(response.length).to eql(3)
expect(response[1]).to include( 'Content-Type' => 'text/yaml')
expect(response[2].join).to include('title: API V1')
end
end
end end
end end
end end

View File

@ -0,0 +1,9 @@
Description:
This creates an RSpec request spec to define Swagger documentation for a
controller. It will create a test for each of the controller's methods.
Example:
rails generate rspec:swagger V3::AccountsController
This will create:
spec/requests/v3/accounts_spec.rb

View File

@ -0,0 +1,22 @@
require 'rswag/route_parser'
require 'rails/generators'
module Rspec
class SwaggerGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
def setup
@routes = Rswag::RouteParser.new(controller_path).routes
end
def create_spec_file
template 'spec.rb', File.join('spec', 'requests', "#{controller_path}_spec.rb")
end
private
def controller_path
file_path.chomp('_controller')
end
end
end

View File

@ -0,0 +1,30 @@
require 'swagger_helper'
RSpec.describe '<%= controller_path %>', type: :request do
<% @routes.each do | template, path_item | %>
path '<%= template %>' do
<% unless path_item[:params].empty? -%>
# You'll want to customize the parameter types...
<% path_item[:params].each do |param| -%>
parameter name: '<%= param %>', in: :path, type: :string, description: '<%= param %>'
<% end -%>
<% end -%>
<% path_item[:actions].each do | action, details | %>
<%= action %>('<%= details[:summary] %>') do
response(200, 'successful') do
<% unless path_item[:params].empty? -%>
<% path_item[:params].each do |param| -%>
let(:<%= param %>) { '123' }
<% end -%>
<% end -%>
after do |example|
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
end
run_test!
end
end
<% end -%>
end
<% end -%>
end

View File

@ -4,7 +4,7 @@ RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated # Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need # NOTE: If you're using the rswag-api to serve API descriptions, you'll need
# to ensure that it's configured to serve Swagger from the same folder # to ensure that it's configured to serve Swagger from the same folder
config.swagger_root = Rails.root.to_s + '/swagger' config.swagger_root = Rails.root.join('swagger').to_s
# Define one or more Swagger documents and provide global metadata for each one # Define one or more Swagger documents and provide global metadata for each one
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
@ -13,8 +13,8 @@ RSpec.configure do |config|
# document below. You can override this behavior by adding a swagger_doc tag to the # document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = { config.swagger_docs = {
'v1/swagger.json' => { 'v1/swagger.yaml' => {
swagger: '2.0', openapi: '3.0.1',
info: { info: {
title: 'API V1', title: 'API V1',
version: 'v1' version: 'v1'
@ -22,4 +22,10 @@ RSpec.configure do |config|
paths: {} paths: {}
} }
} }
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
# The swagger_docs configuration option has the filename including format in
# the key, this may want to be changed to avoid putting yaml in json files.
# Defaults to json. Accepts ':json' and ':yaml'.
config.swagger_format = :yaml
end end

View File

@ -0,0 +1,58 @@
module Rswag
class RouteParser
attr_reader :controller
def initialize(controller)
@controller = controller
end
def routes
::Rails.application.routes.routes.select do |route|
route.defaults[:controller] == controller
end.reduce({}) do |tree, route|
path = path_from(route)
verb = verb_from(route)
tree[path] ||= { params: params_from(route), actions: {} }
tree[path][:actions][verb] = { summary: summary_from(route) }
tree
end
end
private
def path_from(route)
route.path.spec.to_s
.chomp('(.:format)') # Ignore any format suffix
.gsub(/:([^\/.?]+)/, '{\1}') # Convert :id to {id}
end
def verb_from(route)
verb = route.verb
if verb.kind_of? String
verb.downcase
else
verb.source.gsub(/[$^]/, '').downcase
end
end
def summary_from(route)
verb = route.requirements[:action]
noun = route.requirements[:controller].split('/').last.singularize
# Apply a few customizations to make things more readable
case verb
when 'index'
verb = 'list'
noun = noun.pluralize
when 'destroy'
verb = 'delete'
end
"#{verb} #{noun}"
end
def params_from(route)
route.segments - ['format']
end
end
end

View File

@ -12,6 +12,7 @@ module Rswag
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.add_setting :swagger_dry_run
c.add_setting :swagger_format
c.extend ExampleGroupHelpers, type: :request c.extend ExampleGroupHelpers, type: :request
c.include ExampleHelpers, type: :request c.include ExampleHelpers, type: :request
end end

View File

@ -31,6 +31,14 @@ module Rswag
end end
end end
def swagger_format
@swagger_format ||= begin
@rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty?
raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
@rspec_config.swagger_format
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

@ -7,7 +7,7 @@ module Rswag
describe(template, metadata, &block) describe(template, metadata, &block)
end end
[ :get, :post, :patch, :put, :delete, :head ].each do |verb| [ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb|
define_method(verb) do |summary, &block| define_method(verb) do |summary, &block|
api_metadata = { operation: { verb: verb, summary: summary } } api_metadata = { operation: { verb: verb, summary: summary } }
describe(verb, api_metadata, &block) describe(verb, api_metadata, &block)

View File

@ -5,6 +5,10 @@ module Rswag
rake_tasks do rake_tasks do
load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__) load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__)
end end
generators do
require 'generators/rspec/swagger_generator.rb'
end
end end
end end
end end

View File

@ -14,17 +14,19 @@ module Rswag
def validate!(metadata, response) def validate!(metadata, response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
validate_code!(metadata, response.code) validate_code!(metadata, response)
validate_headers!(metadata, response.headers) validate_headers!(metadata, response.headers)
validate_body!(metadata, swagger_doc, response.body) validate_body!(metadata, swagger_doc, response.body)
end end
private private
def validate_code!(metadata, code) def validate_code!(metadata, response)
expected = metadata[:response][:code].to_s expected = metadata[:response][:code].to_s
if code != expected if response.code != expected
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{expected}'" raise UnexpectedResponse,
"Expected response code '#{response.code}' to match '#{expected}'\n" \
"Response body: #{response.body}"
end end
end end

View File

@ -25,7 +25,11 @@ module Rswag
metadata = notification.metadata metadata = notification.metadata
end end
# !metadata[:document] won't work, since nil means we should generate
# docs.
return if metadata[:document] == false
return unless metadata.has_key?(:response) return unless metadata.has_key?(:response)
swagger_doc = @config.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
@ -37,7 +41,7 @@ module Rswag
FileUtils.mkdir_p dirname unless File.exists?(dirname) FileUtils.mkdir_p dirname unless File.exists?(dirname)
File.open(file_path, 'w') do |file| File.open(file_path, 'w') do |file|
file.write(JSON.pretty_generate(doc)) file.write(pretty_generate(doc))
end end
@output.puts "Swagger doc generated at #{file_path}" @output.puts "Swagger doc generated at #{file_path}"
@ -46,6 +50,20 @@ module Rswag
private private
def pretty_generate(doc)
if @config.swagger_format == :yaml
clean_doc = yaml_prepare(doc)
YAML.dump(clean_doc)
else # config errors are thrown in 'def swagger_format', no throw needed here
JSON.pretty_generate(doc)
end
end
def yaml_prepare(doc)
json_doc = JSON.pretty_generate(doc)
clean_doc = JSON.parse(json_doc)
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 }

View File

@ -5,7 +5,10 @@ namespace :rswag do
desc 'Generate Swagger JSON files from integration specs' desc 'Generate Swagger JSON files from integration specs'
RSpec::Core::RakeTask.new('swaggerize') do |t| RSpec::Core::RakeTask.new('swaggerize') do |t|
t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' t.pattern = ENV.fetch(
'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 && Rswag::Specs.config.swagger_dry_run if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run
@ -16,3 +19,5 @@ namespace :rswag do
end end
end end
end end
task :rswag => ['rswag:specs:swaggerize']

View File

@ -13,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 'activesupport', '>= 3.1', '< 6.0' s.add_dependency 'activesupport', '>= 3.1', '< 7.0'
s.add_dependency 'railties', '>= 3.1', '< 6.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'
s.add_dependency 'json-schema', '~> 2.2' s.add_dependency 'json-schema', '~> 2.2'
end end

View File

@ -0,0 +1,44 @@
require 'generator_spec'
require 'generators/rspec/swagger_generator'
require 'tmpdir'
module Rspec
describe SwaggerGenerator do
include GeneratorSpec::TestCase
destination Dir.mktmpdir
before(:all) do
prepare_destination
fixtures_dir = File.expand_path('../fixtures', __FILE__)
FileUtils.cp_r("#{fixtures_dir}/spec", destination_root)
end
after(:all) do
end
it 'installs the swagger_helper for rspec' do
allow_any_instance_of(Rswag::RouteParser).to receive(:routes).and_return(fake_routes)
run_generator ['Posts::CommentsController']
assert_file('spec/requests/posts/comments_spec.rb') do |content|
assert_match(/parameter name: 'post_id', in: :path, type: :string/, content)
assert_match(/patch\('update_comments comment'\)/, content)
end
end
private
def fake_routes
{
"/posts/{post_id}/comments/{id}" => {
:params => ["post_id", "id"],
:actions => {
"get" => { :summary=>"show comment" },
"patch" => { :summary=>"update_comments comment" }
}
}
}
end
end
end

View File

@ -4,7 +4,7 @@ require 'generators/rswag/specs/install/install_generator'
module Rswag module Rswag
module Specs module Specs
describe InstallGenerator do RSpec.describe InstallGenerator do
include GeneratorSpec::TestCase include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__) destination File.expand_path('../tmp', __FILE__)

View File

@ -3,10 +3,12 @@ require 'rswag/specs/configuration'
module Rswag module Rswag
module Specs module Specs
describe Configuration do RSpec.describe Configuration do
subject { described_class.new(rspec_config) } subject { described_class.new(rspec_config) }
let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } let(:rspec_config) do
OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs, swagger_format: swagger_format)
end
let(:swagger_root) { 'foobar' } let(:swagger_root) { 'foobar' }
let(:swagger_docs) do let(:swagger_docs) do
{ {
@ -14,6 +16,7 @@ module Rswag
'v2/swagger.json' => { info: { title: 'v2' } } 'v2/swagger.json' => { info: { title: 'v2' } }
} }
end end
let(:swagger_format) { :yaml }
describe '#swagger_root' do describe '#swagger_root' do
let(:response) { subject.swagger_root } let(:response) { subject.swagger_root }
@ -46,6 +49,26 @@ module Rswag
end end
end end
describe '#swagger_format' do
let(:response) { subject.swagger_format }
context 'provided in rspec config' do
it { expect(response).to be_an_instance_of(Symbol) }
end
context 'unsupported format provided' do
let(:swagger_format) { :xml }
it { expect { response }.to raise_error ConfigurationError }
end
context 'not provided' do
let(:swagger_format) { nil }
it { expect(response).to eq(:json) }
end
end
describe '#get_swagger_doc(tag=nil)' do describe '#get_swagger_doc(tag=nil)' do
let(:swagger_doc) { subject.get_swagger_doc(tag) } let(:swagger_doc) { subject.get_swagger_doc(tag) }

View File

@ -3,7 +3,7 @@ require 'rswag/specs/example_group_helpers'
module Rswag module Rswag
module Specs module Specs
describe ExampleGroupHelpers do RSpec.describe ExampleGroupHelpers do
subject { double('example_group') } subject { double('example_group') }
before do before do
@ -24,7 +24,7 @@ module Rswag
end end
end end
describe '#get|post|patch|put|delete|head(verb, summary)' do describe '#get|post|patch|put|delete|head|options|trace(verb, summary)' do
before { subject.post('Creates a blog') } before { subject.post('Creates a blog') }
it "delegates to 'describe' with 'operation' metadata" do it "delegates to 'describe' with 'operation' metadata" do

View File

@ -3,7 +3,7 @@ require 'rswag/specs/example_helpers'
module Rswag module Rswag
module Specs module Specs
describe ExampleHelpers do RSpec.describe ExampleHelpers do
subject { double('example') } subject { double('example') }
before do before do

View File

@ -3,7 +3,7 @@ require 'rswag/specs/request_factory'
module Rswag module Rswag
module Specs module Specs
describe RequestFactory do RSpec.describe RequestFactory do
subject { RequestFactory.new(config) } subject { RequestFactory.new(config) }
before do before do

View File

@ -3,7 +3,7 @@ require 'rswag/specs/response_validator'
module Rswag module Rswag
module Specs module Specs
describe ResponseValidator do RSpec.describe ResponseValidator do
subject { ResponseValidator.new(config) } subject { ResponseValidator.new(config) }
before do before do

View File

@ -4,7 +4,7 @@ require 'ostruct'
module Rswag module Rswag
module Specs module Specs
describe SwaggerFormatter do RSpec.describe SwaggerFormatter do
subject { described_class.new(output, config) } subject { described_class.new(output, config) }
# Mock out some infrastructure # Mock out some infrastructure
@ -26,10 +26,23 @@ module Rswag
{ {
path_item: { template: '/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' },
document: document
} }
end end
context 'with the document tag set to false' do
let(:document) { false }
it 'does not update the swagger doc' do
expect(swagger_doc).to be_empty
end
end
context 'with the document tag set to anything but false' do
# anything works, including its absence when specifying responses.
let(:document) { nil }
it 'converts to swagger and merges into the corresponding swagger doc' do it 'converts to swagger and merges into the corresponding swagger doc' do
expect(swagger_doc).to match( expect(swagger_doc).to match(
paths: { paths: {
@ -45,6 +58,7 @@ module Rswag
) )
end end
end end
end
describe '#stop' do describe '#stop' do
before do before do
@ -53,14 +67,30 @@ module Rswag
'v1/swagger.json' => { info: { version: 'v1' } }, 'v1/swagger.json' => { info: { version: 'v1' } },
'v2/swagger.json' => { info: { version: 'v2' } } 'v2/swagger.json' => { info: { version: 'v2' } }
) )
allow(config).to receive(:swagger_format).and_return(swagger_format)
subject.stop(notification) subject.stop(notification)
end end
let(:notification) { double('notification') } let(:notification) { double('notification') }
context 'with default format' do
let(:swagger_format) { :json }
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")
expect { JSON.parse(File.read("#{swagger_root}/v2/swagger.json")) }.not_to raise_error
end
end
context 'with yaml format' do
let(:swagger_format) { :yaml }
it 'writes the swagger_doc(s) as yaml' do
expect(File).to exist("#{swagger_root}/v1/swagger.json")
expect { JSON.parse(File.read("#{swagger_root}/v1/swagger.json")) }.to raise_error(JSON::ParserError)
# Psych::DisallowedClass would be raised if we do not pre-process ruby symbols
expect { YAML.safe_load(File.read("#{swagger_root}/v1/swagger.json")) }.not_to raise_error
end
end end
after do after do

View File

@ -2,11 +2,11 @@ Rswag::Ui.configure do |c|
# List the Swagger endpoints that you want to be documented through the swagger-ui # List the Swagger endpoints that you want to be documented through the swagger-ui
# The first parameter is the path (absolute or relative to the UI host) to the corresponding # The first parameter is the path (absolute or relative to the UI host) to the corresponding
# JSON endpoint and the second is a title that will be displayed in the document selector # endpoint and the second is a title that will be displayed in the document selector
# NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON endpoints, # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints,
# then the list below should correspond to the relative paths for those endpoints # then the list below should correspond to the relative paths for those endpoints
c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs'
# Add Basic Auth in case your API is private # Add Basic Auth in case your API is private
# c.basic_auth_enabled = true # c.basic_auth_enabled = true

View File

@ -5,9 +5,9 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"swagger-ui-dist": { "swagger-ui-dist": {
"version": "3.17.3", "version": "3.18.2",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.17.3.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.18.2.tgz",
"integrity": "sha1-37lkCMzEZ3UVX3NpGQxdSyAW/lw=" "integrity": "sha512-pWAEiKkgWUJvjmLW9AojudnutJ+NTn5g6OdNLj1iIJWwCkoy40K3Upwa24DqFbmIE4vLX4XplND61hp2L+s5vg=="
} }
} }
} }

View File

@ -3,6 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"swagger-ui-dist": "3.17.3" "swagger-ui-dist": "3.18.2"
} }
} }

View File

@ -13,6 +13,6 @@ Gem::Specification.new do |s|
s.files = Dir.glob("{lib,node_modules}/**/*") + ["MIT-LICENSE", "Rakefile" ] s.files = Dir.glob("{lib,node_modules}/**/*") + ["MIT-LICENSE", "Rakefile" ]
s.add_dependency 'actionpack', '>=3.1', '< 6.0' s.add_dependency 'actionpack', '>=3.1', '< 7.0'
s.add_dependency 'railties', '>= 3.1', '< 6.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'
end end

View File

@ -0,0 +1,2 @@
//= link_tree ../images
//= link_directory ../stylesheets .css

View File

View File

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
feature 'swagger-ui', js: true do RSpec.feature 'swagger-ui', js: true do
scenario 'browsing api-docs' do scenario 'browsing api-docs' do
visit '/api-docs' visit '/api-docs'

View File

@ -1,6 +1,6 @@
require 'swagger_helper' require 'swagger_helper'
describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do
path '/auth-tests/basic' do path '/auth-tests/basic' do
post 'Authenticates with basic auth' do post 'Authenticates with basic auth' do

View File

@ -1,6 +1,6 @@
require 'swagger_helper' require 'swagger_helper'
describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
let(:api_key) { 'fake_key' } let(:api_key) { 'fake_key' }
path '/blogs' do path '/blogs' do

View File

@ -1,7 +1,7 @@
require 'spec_helper' require 'spec_helper'
require 'rake' require 'rake'
describe 'rswag:specs:swaggerize' do RSpec.describe 'rswag:specs:swaggerize' do
let(:swagger_root) { Rails.root.to_s + '/swagger' } let(:swagger_root) { Rails.root.to_s + '/swagger' }
before do before do
TestApp::Application.load_tasks TestApp::Application.load_tasks