Merge pull request #286 from BookOfGreg/openapi/merge

Support for some openapi3 features
This commit is contained in:
Greg Myers 2020-04-05 01:05:17 +01:00 committed by GitHub
commit 42fdf6d482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1417 additions and 449 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@
**/*/node_modules **/*/node_modules
*.swp *.swp
Gemfile.lock Gemfile.lock
/.idea/
**/.byebug_history

View File

@ -7,12 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
### Changed ### Changed
- Update swagger-ui version to 3.23.11 [#239](https://github.com/rswag/rswag/pull/239)
### Deprecated ### Deprecated
### Removed ### Removed
### Fixed ### Fixed
### Security ### Security
## [2.3.0] - 2020-04-05
### Added
- Support for OpenAPI 3.0 ! [#286](https://github.com/rswag/rswag/pull/286)
- Custom headers in rswag-api [#187](https://github.com/rswag/rswag/pull/187)
- Allow document: false rspec metatag [#255](https://github.com/rswag/rswag/pull/255)
- Add parameterized pattern for spec files [#254](https://github.com/rswag/rswag/pull/254)
- Support Basic Auth on rswag-ui [#167](https://github.com/rswag/rswag/pull/167)
### Changed
- Update swagger-ui version to 3.23.11 [#239](https://github.com/rswag/rswag/pull/239)
- Rails constraint moved from < 6.1 to < 7 [#253](https://github.com/rswag/rswag/pull/253)
- Swaggerize now outputs base RSpec text on completion to avoid silent failures [#293](https://github.com/rswag/rswag/pull/293)
## [2.2.0] - 2019-11-01 ## [2.2.0] - 2019-11-01
### Added ### Added
- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) - New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251)

View File

@ -30,6 +30,11 @@ cd -
``` ```
## Test ## Test
Initialize the rswag-ui repo with assets.
```
ci/build.sh
```
Make sure the tests pass: Make sure the tests pass:
``` ```
./ci/test.sh ./ci/test.sh

21
Gemfile
View File

@ -1,10 +1,12 @@
source "https://rubygems.org" # frozen_string_literal: true
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'] || '5.1.2' rails_version = ENV['RAILS_VERSION'] || '5.2.4.2'
gem 'rails', "#{rails_version}" gem 'rails', rails_version.to_s
case rails_version.split('.').first case rails_version.split('.').first
when '3' when '3'
@ -24,18 +26,23 @@ gem 'rswag-api', path: './rswag-api'
gem 'rswag-ui', path: './rswag-ui' gem 'rswag-ui', path: './rswag-ui'
group :test do group :test do
gem 'test-unit'
gem 'rspec-rails'
gem 'generator_spec'
gem 'capybara' gem 'capybara'
gem 'geckodriver-helper' gem 'geckodriver-helper'
gem 'generator_spec'
gem 'rspec-rails'
gem 'selenium-webdriver' gem 'selenium-webdriver'
gem 'rswag-specs', path: './rswag-specs' gem 'rswag-specs', path: './rswag-specs'
gem 'test-unit'
end
group :development do
gem 'rswag-specs', path: './rswag-specs'
gem 'rubocop'
end end
group :assets do group :assets do
gem 'uglifier'
gem 'therubyracer' gem 'therubyracer'
gem 'uglifier'
end end
gem 'byebug' gem 'byebug'

372
README.md
View File

@ -3,10 +3,13 @@ rswag
[![Build Status](https://travis-ci.org/rswag/rswag.svg?branch=master)](https://travis-ci.org/rswag/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) [![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. OpenApi 3.0 and Swagger 2.0 compatible!
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. 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.
Api Rswag creates [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.
And that's not all ... And that's not all ...
Once you have an API that can describe itself in Swagger, you've opened the treasure chest of Swagger-based tools including a client generator that can be targeted to a wide range of popular platforms. See [swagger-codegen](https://github.com/swagger-api/swagger-codegen) for more details. Once you have an API that can describe itself in Swagger, you've opened the treasure chest of Swagger-based tools including a client generator that can be targeted to a wide range of popular platforms. See [swagger-codegen](https://github.com/swagger-api/swagger-codegen) for more details.
@ -15,10 +18,49 @@ 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/rswag/rswag/tree/master)|2.0|3.18.2| |[master](https://github.com/rswag/rswag/tree/master)|3.0.3|3.23.11|
|[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2| |[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2|
|[1.6.0](https://github.com/rswag/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|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
**Table of Contents**
- [rswag](#rswag)
- [Compatibility](#compatibility)
- [Getting Started](#getting-started)
- [The rspec DSL](#the-rspec-dsl)
- [Paths, Operations and Responses](#paths-operations-and-responses)
- [Null Values](#null-values)
- [Support for oneOf, anyOf or AllOf schemas](#support-for-oneof-anyof-or-allof-schemas)
- [Global Metadata](#global-metadata)
- [Supporting multiple versions of API](#supporting-multiple-versions-of-api)
- [Formatting the description literals:](#formatting-the-description-literals)
- [Specifying/Testing API Security](#specifyingtesting-api-security)
- [Configuration & Customization](#configuration--customization)
- [Output Location for Generated Swagger Files](#output-location-for-generated-swagger-files)
- [Input Location for Rspec Tests](#input-location-for-rspec-tests)
- [Referenced Parameters and Schema Definitions](#referenced-parameters-and-schema-definitions)
- [Response headers](#response-headers)
- [Response examples](#response-examples)
- [Enable auto generation examples from responses](#enable-auto-generation-examples-from-responses)
- [Running tests without documenting](#running-tests-without-documenting)
- [rswag helper methods](#rswag-helper-methods)
- [rswag response examples](#rswag-response-examples)
- [Route Prefix for Swagger JSON Endpoints](#route-prefix-for-swagger-json-endpoints)
- [Root Location for Swagger Files](#root-location-for-swagger-files)
- [Dynamic Values for Swagger JSON](#dynamic-values-for-swagger-json)
- [Custom Headers for Swagger Files](#custom-headers-for-swagger-files)
- [Enable Swagger Endpoints for swagger-ui](#enable-swagger-endpoints-for-swagger-ui)
- [Enable Simple Basic Auth for swagger-ui](#enable-simple-basic-auth-for-swagger-ui)
- [Route Prefix for the swagger-ui](#route-prefix-for-the-swagger-ui)
- [Customizing the swagger-ui](#customizing-the-swagger-ui)
- [Serve UI Assets Directly from your Web Server](#serve-ui-assets-directly-from-your-web-server)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Getting Started ## ## Getting Started ##
1. Add this line to your applications _Gemfile_: 1. Add this line to your applications _Gemfile_:
@ -56,6 +98,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
``` ```
3. Create an integration spec to describe and test your API. 3. Create an integration spec to describe and test your API.
There is also a generator which can help get you started `rails generate rspec:swagger API::MyController`
```ruby ```ruby
# spec/integration/blogs_spec.rb # spec/integration/blogs_spec.rb
@ -67,7 +110,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
post 'Creates a blog' do post 'Creates a blog' do
tags 'Blogs' tags 'Blogs'
consumes 'application/json', 'application/xml' consumes 'application/json'
parameter name: :blog, in: :body, schema: { parameter name: :blog, in: :body, schema: {
type: :object, type: :object,
properties: { properties: {
@ -94,7 +137,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
get 'Retrieves a blog' do get 'Retrieves a blog' do
tags 'Blogs' tags 'Blogs'
produces 'application/json', 'application/xml' produces 'application/json', 'application/xml'
parameter name: :id, :in => :path, :type => :string parameter name: :id, in: :path, type: :string
response '200', 'blog found' do response '200', 'blog found' do
schema type: :object, schema type: :object,
@ -123,8 +166,6 @@ 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)
@ -134,6 +175,11 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
This common command is also aliased as `rake rswag`. This common command is also aliased as `rake rswag`.
Or if you installed your gems separately:
```
RAILS_ENV=test rails 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 ##
@ -173,7 +219,7 @@ end
### Null Values ### ### 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. This library is currently using JSON::Draft4 for validation of response models. Nullable properties can be supported with the non-standard property 'x-nullable' to a definition to allow null/nil values to pass. Or you can add the new standard ```nullable``` property to a definition.
```ruby ```ruby
describe 'Blogs API' do describe 'Blogs API' do
path '/blogs' do path '/blogs' do
@ -184,8 +230,8 @@ describe 'Blogs API' do
schema type: :object, schema type: :object,
properties: { properties: {
id: { type: :integer }, id: { type: :integer },
title: { type: :string }, title: { type: :string, nullable: true }, # preferred syntax
content: { type: :string, 'x-nullable': true } content: { type: :string, 'x-nullable': true } # legacy syntax, but still works
} }
.... ....
end end
@ -193,12 +239,45 @@ describe 'Blogs API' do
end end
end end
``` ```
*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> ### Support for oneOf, anyOf or AllOf schemas ###
Open API 3.0 now supports more flexible schema validation with the ```oneOf```, ```anyOf``` and ```allOf``` directives. rswag will handle these definitions and validate them properly.
Notice the ```schema``` inside the ```response``` section. Placing a ```schema``` method inside the response will validate (and fail the tests)
if during the integration test run the endpoint response does not match the response schema. This test validation can handle anyOf and allOf as well. See below:
```ruby
path '/blogs/flexible' do
post 'Creates a blog flexible body' do
tags 'Blogs'
description 'Creates a flexible blog from provided data'
operationId 'createFlexibleBlog'
consumes 'application/json'
produces 'application/json'
parameter name: :blog, in: :body, schema: {
oneOf: [
{ '$ref' => '#/components/schemas/blog' },
{ '$ref' => '#/components/schemas/flexible_blog' }
]
}
response '201', 'flexible blog created' do
schema oneOf: [{ '$ref' => '#/components/schemas/blog' }, { '$ref' => '#/components/schemas/flexible_blog' }]
run_test!
end
end
end
```
This automatic schema validation is a powerful feature of rswag.
### 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. In Open API 3.0 the pathing and server definitions have changed a bit [Swagger host/basePath](https://swagger.io/docs/specification/api-host-and-base-path/):
```ruby ```ruby
# spec/swagger_helper.rb # spec/swagger_helper.rb
@ -207,23 +286,41 @@ RSpec.configure do |config|
config.swagger_docs = { config.swagger_docs = {
'v1/swagger.json' => { 'v1/swagger.json' => {
swagger: '2.0', openapi: '3.0.1',
info: { info: {
title: 'API V1', title: 'API V1',
version: 'v1', version: 'v1',
description: 'This is the first version of my API' description: 'This is the first version of my API'
}, },
basePath: '/api/v1' servers: [
{
url: 'https://{defaultHost}',
variables: {
defaultHost: {
default: 'www.example.com'
}
}
}
]
}, },
'v2/swagger.yaml' => { 'v2/swagger.yaml' => {
openapi: '3.0.0', openapi: '3.0.1',
info: { info: {
title: 'API V2', title: 'API V2',
version: 'v2', version: 'v2',
description: 'This is the second version of my API' description: 'This is the second version of my API'
}, },
basePath: '/api/v2' servers: [
{
url: 'https://{defaultHost}',
variables: {
defaultHost: {
default: 'www.example.com'
}
}
}
]
} }
} }
end end
@ -265,7 +362,9 @@ you should use the folowing syntax, making sure there are no whitespaces at the
### 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, :bearer, :apiKey and :oauth2 and :openIdConnect scheme types. See [the spec](https://swagger.io/docs/specification/authentication/) for more info, as this underwent major changes between Swagger 2.0 and Open API 3.0
```ruby ```ruby
# spec/swagger_helper.rb # spec/swagger_helper.rb
@ -274,12 +373,14 @@ RSpec.configure do |config|
config.swagger_docs = { config.swagger_docs = {
'v1/swagger.json' => { 'v1/swagger.json' => {
... ... # note the new Open API 3.0 compliant security structure here, under "components"
securityDefinitions: { components: {
basic: { securitySchemes: {
type: :basic basic_auth: {
type: :http,
scheme: :basic
}, },
apiKey: { api_key: {
type: :apiKey, type: :apiKey,
name: 'api_key', name: 'api_key',
in: :query in: :query
@ -287,6 +388,7 @@ RSpec.configure do |config|
} }
} }
} }
}
end end
# spec/integration/blogs_spec.rb # spec/integration/blogs_spec.rb
@ -296,7 +398,7 @@ describe 'Blogs API' do
post 'Creates a blog' do post 'Creates a blog' do
tags 'Blogs' tags 'Blogs'
security [ basic: [] ] security [ basic_auth: [] ]
... ...
response '201', 'blog created' do response '201', 'blog created' do
@ -311,9 +413,35 @@ describe 'Blogs API' do
end end
end end
end end
# example of documenting an endpoint that handles basic auth and api key based security
describe 'Auth examples API' do
path '/auth-tests/basic-and-api-key' do
post 'Authenticates with basic auth and api key' do
tags 'Auth Tests'
operationId 'testBasicAndApiKey'
security [{ basic_auth: [], api_key: [] }]
response '204', 'Valid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
let(:api_key) { 'foobar' }
run_test!
end
response '401', 'Invalid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
let(:api_key) { 'barfoo' }
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 __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 ##
@ -322,7 +450,7 @@ 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 your 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 your 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 ###
@ -337,7 +465,7 @@ RSpec.configure do |config|
end 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 ### ### Input Location for Rspec Tests ###
@ -350,21 +478,25 @@ 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.
Again, this is a structure that changed since swagger 2.0. Notice the new "schemas" section for these.
Rather than repeating the schema in every operation spec, you can define it globally and provide a reference to it in each spec:
```ruby ```ruby
# spec/swagger_helper.rb # spec/swagger_helper.rb
config.swagger_docs = { config.swagger_docs = {
'v1/swagger.json' => { 'v1/swagger.json' => {
swagger: '2.0', openapi: '3.0.0',
info: { info: {
title: 'API V1' title: 'API V1'
}, },
definitions: { components: {
schemas: {
errors_object: { errors_object: {
type: 'object', type: 'object',
properties: { properties: {
errors: { '$ref' => '#/definitions/errors_map' } errors: { '$ref' => '#/components/schemas/errors_map' }
} }
}, },
errors_map: { errors_map: {
@ -373,6 +505,17 @@ config.swagger_docs = {
type: 'array', type: 'array',
items: { type: 'string' } items: { type: 'string' }
} }
},
blog: {
type: 'object',
properties: {
id: { type: 'integer' },
title: { type: 'string' },
content: { type: 'string', nullable: true },
thumbnail: { type: 'string', nullable: true }
},
required: %w[id title]
}
} }
} }
} }
@ -386,7 +529,7 @@ describe 'Blogs API' do
post 'Creates a blog' do post 'Creates a blog' do
response 422, 'invalid request' do response 422, 'invalid request' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/components/schemas/errors_object'
... ...
end end
@ -398,14 +541,15 @@ describe 'Blogs API' do
post 'Creates a comment' do post 'Creates a comment' do
response 422, 'invalid request' do response 422, 'invalid request' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/components/schemas/errors_object'
... ...
end end
``` ```
### Response headers ### ### 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: 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 ```ruby
# spec/integration/comments_spec.rb # spec/integration/comments_spec.rb
@ -425,7 +569,7 @@ end
### Response examples ### ### Response examples ###
You can provide custom response examples to the generated swagger file by calling the method `examples` inside the response block: You can provide custom response examples to the generated swagger file by calling the method `examples` inside the response block:
However, auto generated example responses are now enabled by default in rswag. See below.
```ruby ```ruby
# spec/integration/blogs_spec.rb # spec/integration/blogs_spec.rb
describe 'Blogs API' do describe 'Blogs API' do
@ -444,16 +588,26 @@ describe 'Blogs API' do
end end
``` ```
### Enable generation examples from responses ###
### Enable auto generation examples from responses ###
To enable examples generation from responses add callback above run_test! like: To enable examples generation from responses add callback above run_test! like:
```ruby
```
after do |example| after do |example|
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
end end
``` ```
You need to disable --dry-run option for Rspec > 3 You need to disable --dry-run option for Rspec > 3
<!-- This is now enabled by default in rswag.
You need to set the ``` config.swagger_dry_run = false``` value in the spec/spec_helper.rb file.
This is one of the more powerful features of rswag. When rswag runs your integration test suite via ```bundle exec rspec```, it will capture the request and response bodies and output those values in the examples section.
These integration tests are usually written with ```let``` variables for post body parameters, and since its an integration test the service is returning actual values.
We might as well re-use these values and embed them into the generated swagger to provide a more real world example for request/response examples. -->
Add to config/environments/test.rb: Add to config/environments/test.rb:
```ruby ```ruby
RSpec.configure do |config| RSpec.configure do |config|
@ -461,7 +615,7 @@ RSpec.configure do |config|
end end
``` ```
### Running tests without documenting ### #### 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: If you want to use Rswag for testing without adding it to you swagger docs, you can provide the document tag:
```ruby ```ruby
@ -489,6 +643,136 @@ describe 'Blogs API', document: false do
end end
``` ```
##### rswag helper methods #####
<!--
There are some helper methods to help with documenting request bodies.
```ruby
describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
let(:api_key) { 'fake_key' }
path '/blogs' do
post 'Creates a blog' do
tags 'Blogs'
description 'Creates a new blog from provided data'
operationId 'createBlog'
consumes 'application/json'
produces 'application/json'
request_body_json schema: { '$ref' => '#/components/schemas/blog' },
examples: :blog
request_body_text_plain
request_body_xml schema: { '$ref' => '#/components/schemas/blog' }
let(:blog) { { blog: { title: 'foo', content: 'bar' } } }
response '201', 'blog created' do
schema '$ref' => '#/components/schemas/blog'
run_test!
end
response '422', 'invalid request' do
schema '$ref' => '#/components/schemas/errors_object'
let(:blog) { { blog: { title: 'foo' } } }
run_test! do |response|
expect(response.body).to include("can't be blank")
end
end
end
end
end
```
In the above example, we see methods ```request_body_json``` ```request_body_plain``` ```request_body_xml```.
These methods can be used to describe json, plain text and xml body. They are just wrapper methods to setup posting JSON, plain text or xml into your endpoint.
The simplest most common usage is for json formatted body to use the schema: to specify the location of the schema for the request body
and the examples: :blog which will create a named example "blog" under the "requestBody / content / application/json / examples" section.
Again, documenting request response examples changed in Open API 3.0. The example above would generate a swagger.json snippet that looks like this:
```json
...
{"requestBody": {
"required": true,
"content": {
"application/json": {
"examples": {
"blog": { // takes the name from examples: :blog above
"value": { //this is open api 3.0 structure -> https://swagger.io/docs/specification/adding-examples/
"blog": { // here is the actual JSON payload that is submitted to the service, and shows up in swagger UI as an example
"title": "foo",
"content": "bar"
}
}
}
},
"schema": {
"$ref": "#/components/schemas/blog"
}
},
"test/plain": {
"schema": {
"type": "string"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/blog"
}
}
}
},
}
```
*NOTE:* for this example request body to work in the tests properly, you need to ``let`` a variable named *blog*.
The variable with the matching name (blog in this case) is eval-ed and captured to be placed in the examples section.
This ```let``` value is used in the integration test to run the test AND captured and injected into the requestBody section.
##### rswag response examples #####
In the same way that requestBody examples can be captured and injected into the swagger output, response examples can also be captured.
Using the above example, when the integration test is run - the swagger would include the following snippet providing more useful real world examples
capturing the response from the execution of the integration test. Again 3.0 swagger changed the structure of how these are documented.
```json
... "responses": {
"201": {
"description": "blog created",
"content": {
"application/json": {
"example": {
"id": 1,
"title": "foo",
"content": "bar",
"thumbnail": null
},
"schema": {
"$ref": "#/components/schemas/blog"
}
}
}
},
"422": {
"description": "invalid request",
"content": {
"application/json": {
"example": {
"errors": {
"content": [
"can't be blank"
]
}
},
"schema": {
"$ref": "#/components/schemas/errors_object"
}
}
}
}
}
```
-->
### 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_:
@ -509,7 +793,7 @@ GET http://<hostname>/your-custom-prefix/v1/swagger.json
### Root Location for Swagger Files ### ### Root Location for Swagger Files ###
You can adjust this in the _rswag-api.rb_ initializer that's installed with __rspec-api__: You can adjust this in the _rswag_api.rb_ initializer that's installed with __rspec-api__:
```ruby ```ruby
Rswag::Api.configure do |c| Rswag::Api.configure do |c|
@ -604,3 +888,15 @@ bundle exec rake rswag:ui:copy_assets[public/api-docs]
``` ```
__NOTE:__: The provided subfolder MUST correspond to the UI mount prefix - "api-docs" by default. __NOTE:__: The provided subfolder MUST correspond to the UI mount prefix - "api-docs" by default.
Notes to test swagger output locally with swagger editor
```
docker pull swaggerapi/swagger-editor
```
```
docker run -d -p 80:8080 swaggerapi/swagger-editor
```
This will run the swagger editor in the docker daemon and can be accessed
at ```http://localhost```. From here, you can use the UI to load the generated swagger.json to validate the output.

View File

@ -5,4 +5,4 @@ Example:
rails generate rswag:api:install rails generate rswag:api:install
This will create: This will create:
config/initializers/rswag-api.rb config/initializers/rswag_api.rb

View File

@ -7,7 +7,7 @@ module Rswag
source_root File.expand_path('../templates', __FILE__) source_root File.expand_path('../templates', __FILE__)
def add_initializer def add_initializer
template('rswag-api.rb', 'config/initializers/rswag-api.rb') template('rswag_api.rb', 'config/initializers/rswag_api.rb')
end end
def add_routes def add_routes

View File

@ -1,17 +1,19 @@
$:.push File.expand_path("../lib", __FILE__) # frozen_string_literal: true
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
# 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 = ENV['TRAVIS_TAG'] || '0.0.0' s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ["domaindrivendev@gmail.com"] s.email = ['domaindrivendev@gmail.com']
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = 'https://github.com/rswag/rswag'
s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints" s.summary = 'A Rails Engine that exposes Swagger files as JSON endpoints'
s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints" s.description = 'Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints'
s.license = "MIT" s.license = 'MIT'
s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile"] s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile']
s.add_dependency 'railties', '>= 3.1', '< 7.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'
end end

View File

@ -1,6 +1,7 @@
require 'generator_spec' require 'generator_spec'
require 'generators/rswag/api/install/install_generator' require 'generators/rswag/api/install/install_generator'
module Rswag module Rswag
module Api module Api
@ -17,7 +18,7 @@ module Rswag
end end
it 'installs the Rails initializer' do it 'installs the Rails initializer' do
assert_file('config/initializers/rswag-api.rb') assert_file('config/initializers/rswag_api.rb')
end end
# Don't know how to test this # Don't know how to test this
@ -25,3 +26,4 @@ module Rswag
end end
end end
end end

View File

@ -1,5 +1,5 @@
{ {
"swagger": "2.0", "openapi": "3.0.1",
"info": { "info": {
"title": "API V1", "title": "API V1",
"version": "v1" "version": "v1"

View File

@ -97,7 +97,7 @@ module Rswag
it 'locates files at the provided swagger_root' do it 'locates files at the provided swagger_root' do
expect(response.length).to eql(3) expect(response.length).to eql(3)
expect(response[1]).to include( 'Content-Type' => 'application/json') expect(response[1]).to include( 'Content-Type' => 'application/json')
expect(response[2].join).to include('"swagger":"2.0"') expect(response[2].join).to include('"openapi":"3.0.1"')
end end
end end

View File

@ -20,8 +20,4 @@ RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('lib/**/*.rb')
end end
Bundler::GemHelper.install_tasks Bundler::GemHelper.install_tasks

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
require 'rswag/route_parser' require 'rswag/route_parser'
require 'rails/generators' require 'rails/generators'
module Rspec module Rspec
class SwaggerGenerator < ::Rails::Generators::NamedBase class SwaggerGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__) source_root File.expand_path('templates', __dir__)
def setup def setup
@routes = Rswag::RouteParser.new(controller_path).routes @routes = Rswag::RouteParser.new(controller_path).routes

View File

@ -1,10 +1,11 @@
# frozen_string_literal: true
require 'rails/generators' require 'rails/generators'
module Rswag module Rswag
module Specs module Specs
class InstallGenerator < Rails::Generators::Base class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__) source_root File.expand_path('templates', __dir__)
def add_swagger_helper def add_swagger_helper
template('swagger_helper.rb', 'spec/swagger_helper.rb') template('swagger_helper.rb', 'spec/swagger_helper.rb')

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.configure do |config| RSpec.configure do |config|
@ -19,7 +21,17 @@ RSpec.configure do |config|
title: 'API V1', title: 'API V1',
version: 'v1' version: 'v1'
}, },
paths: {} paths: {},
servers: [
{
url: 'https://{defaultHost}',
variables: {
defaultHost: {
default: 'www.example.com'
}
}
}
]
} }
} }

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Rswag module Rswag
class RouteParser class RouteParser
attr_reader :controller attr_reader :controller
@ -9,7 +11,7 @@ module Rswag
def routes def routes
::Rails.application.routes.routes.select do |route| ::Rails.application.routes.routes.select do |route|
route.defaults[:controller] == controller route.defaults[:controller] == controller
end.reduce({}) do |tree, route| end.each_with_object({}) do |tree, route|
path = path_from(route) path = path_from(route)
verb = verb_from(route) verb = verb_from(route)
tree[path] ||= { params: params_from(route), actions: {} } tree[path] ||= { params: params_from(route), actions: {} }
@ -28,7 +30,7 @@ module Rswag
def verb_from(route) def verb_from(route)
verb = route.verb verb = route.verb
if verb.kind_of? String if verb.is_a? String
verb.downcase verb.downcase
else else
verb.source.gsub(/[$^]/, '').downcase verb.source.gsub(/[$^]/, '').downcase

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rspec/core' require 'rspec/core'
require 'rswag/specs/example_group_helpers' require 'rswag/specs/example_group_helpers'
require 'rswag/specs/example_helpers' require 'rswag/specs/example_helpers'
@ -6,7 +8,6 @@ require 'rswag/specs/railtie' if defined?(Rails::Railtie)
module Rswag module Rswag
module Specs module Specs
# Extend RSpec with a swagger-based DSL # Extend RSpec with a swagger-based DSL
::RSpec.configure do |c| ::RSpec.configure do |c|
c.add_setting :swagger_root c.add_setting :swagger_root

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
module Rswag module Rswag
module Specs module Specs
class Configuration class Configuration
def initialize(rspec_config) def initialize(rspec_config)
@rspec_config = rspec_config @rspec_config = rspec_config
end end
@ -12,6 +12,7 @@ module Rswag
if @rspec_config.swagger_root.nil? if @rspec_config.swagger_root.nil?
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
end end
@rspec_config.swagger_root @rspec_config.swagger_root
end end
end end
@ -21,6 +22,7 @@ module Rswag
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
end end
@rspec_config.swagger_docs @rspec_config.swagger_docs
end end
end end
@ -35,6 +37,7 @@ module Rswag
@swagger_format ||= begin @swagger_format ||= begin
@rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty? @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) raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
@rspec_config.swagger_format @rspec_config.swagger_format
end end
end end
@ -42,8 +45,14 @@ module Rswag
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]
swagger_docs[name] swagger_docs[name]
end end
def get_swagger_doc_version(name)
doc = get_swagger_doc(name)
doc[:openapi] || doc[:swagger]
end
end end
class ConfigurationError < StandardError; end class ConfigurationError < StandardError; end

View File

@ -1,20 +1,21 @@
# frozen_string_literal: true
module Rswag module Rswag
module Specs module Specs
module ExampleGroupHelpers module ExampleGroupHelpers
def path(template, metadata = {}, &block)
def path(template, metadata={}, &block)
metadata[:path_item] = { template: template } metadata[:path_item] = { template: template }
describe(template, metadata, &block) describe(template, metadata, &block)
end end
[ :get, :post, :patch, :put, :delete, :head, :options, :trace ].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)
end end
end end
[ :operationId, :deprecated, :security ].each do |attr_name| [:operationId, :deprecated, :security].each do |attr_name|
define_method(attr_name) do |value| define_method(attr_name) do |value|
metadata[:operation][attr_name] = value metadata[:operation][attr_name] = value
end end
@ -23,13 +24,14 @@ module Rswag
# NOTE: 'description' requires special treatment because ExampleGroup already # NOTE: 'description' requires special treatment because ExampleGroup already
# defines a method with that name. Provide an override that supports the existing # defines a method with that name. Provide an override that supports the existing
# functionality while also setting the appropriate metadata if applicable # functionality while also setting the appropriate metadata if applicable
def description(value=nil) def description(value = nil)
return super() if value.nil? return super() if value.nil?
metadata[:operation][:description] = value metadata[:operation][:description] = value
end end
# These are array properties - note the splat operator # These are array properties - note the splat operator
[ :tags, :consumes, :produces, :schemes ].each do |attr_name| [:tags, :consumes, :produces, :schemes].each do |attr_name|
define_method(attr_name) do |*value| define_method(attr_name) do |*value|
metadata[:operation][attr_name] = value metadata[:operation][attr_name] = value
end end
@ -40,7 +42,7 @@ module Rswag
attributes[:required] = true attributes[:required] = true
end end
if metadata.has_key?(:operation) if metadata.key?(:operation)
metadata[:operation][:parameters] ||= [] metadata[:operation][:parameters] ||= []
metadata[:operation][:parameters] << attributes metadata[:operation][:parameters] << attributes
else else
@ -49,7 +51,7 @@ module Rswag
end end
end end
def response(code, description, metadata={}, &block) def response(code, description, metadata = {}, &block)
metadata[:response] = { code: code, description: description } metadata[:response] = { code: code, description: description }
context(description, metadata, &block) context(description, metadata, &block)
end end
@ -60,6 +62,7 @@ module Rswag
def header(name, attributes) def header(name, attributes)
metadata[:response][:headers] ||= {} metadata[:response][:headers] ||= {}
metadata[:response][:headers][name] = attributes metadata[:response][:headers][name] = attributes
end end
@ -68,6 +71,7 @@ module Rswag
# rspec-core ExampleGroup # rspec-core ExampleGroup
def examples(example = nil) def examples(example = nil)
return super() if example.nil? return super() if example.nil?
metadata[:response][:examples] = example metadata[:response][:examples] = example
end end

View File

@ -1,10 +1,11 @@
# frozen_string_literal: true
require 'rswag/specs/request_factory' require 'rswag/specs/request_factory'
require 'rswag/specs/response_validator' require 'rswag/specs/response_validator'
module Rswag module Rswag
module Specs module Specs
module ExampleHelpers module ExampleHelpers
def submit_request(metadata) def submit_request(metadata)
request = RequestFactory.new.build_request(metadata, self) request = RequestFactory.new.build_request(metadata, self)
@ -19,10 +20,8 @@ module Rswag
send( send(
request[:verb], request[:verb],
request[:path], request[:path],
{
params: request[:payload], params: request[:payload],
headers: request[:headers] headers: request[:headers]
}
) )
end end
end end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
require 'json-schema' require 'json-schema'
module Rswag module Rswag
module Specs module Specs
class ExtendedSchema < JSON::Schema::Draft4 class ExtendedSchema < JSON::Schema::Draft4
def initialize def initialize
super super
@attributes['type'] = ExtendedTypeAttribute @attributes['type'] = ExtendedTypeAttribute
@ -13,9 +14,9 @@ module Rswag
end end
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
def self.validate(current_schema, data, fragments, processor, validator, options = {})
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
def self.validate(current_schema, data, fragments, processor, validator, options={})
return if data.nil? && current_schema.schema['x-nullable'] == true
super super
end end
end end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
module Rswag module Rswag
module Specs module Specs
class Railtie < ::Rails::Railtie class Railtie < ::Rails::Railtie
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', __dir__)
end end
generators do generators do

View File

@ -1,11 +1,13 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/conversions'
require 'json' require 'json'
require 'byebug'
module Rswag module Rswag
module Specs module Specs
class RequestFactory class RequestFactory
def initialize(config = ::Rswag::Specs.config) def initialize(config = ::Rswag::Specs.config)
@config = config @config = config
end end
@ -38,8 +40,8 @@ module Rswag
def derive_security_params(metadata, swagger_doc) def derive_security_params(metadata, swagger_doc)
requirements = metadata[:operation][:security] || swagger_doc[:security] || [] requirements = metadata[:operation][:security] || swagger_doc[:security] || []
scheme_names = requirements.flat_map { |r| r.keys } scheme_names = requirements.flat_map(&:keys)
schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values schemes = security_version(scheme_names, swagger_doc)
schemes.map do |scheme| schemes.map do |scheme|
param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
@ -47,13 +49,55 @@ module Rswag
end end
end end
def security_version(scheme_names, swagger_doc)
if doc_version(swagger_doc).start_with?('2')
(swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
else # Openapi3
if swagger_doc.key?(:securityDefinitions)
ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)')
swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] }
swagger_doc.delete(:securityDefinitions)
end
components = swagger_doc[:components] || {}
(components[:securitySchemes] || {}).slice(*scheme_names).values
end
end
def resolve_parameter(ref, swagger_doc) def resolve_parameter(ref, swagger_doc)
key = ref.sub('#/parameters/', '').to_sym key = key_version(ref, swagger_doc)
definitions = swagger_doc[:parameters] definitions = definition_version(swagger_doc)
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
definitions[key] definitions[key]
end end
def key_version(ref, swagger_doc)
if doc_version(swagger_doc).start_with?('2')
ref.sub('#/parameters/', '').to_sym
else # Openapi3
if ref.start_with?('#/parameters/')
ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/')
ref.sub('#/parameters/', '').to_sym
else
ref.sub('#/components/parameters/', '').to_sym
end
end
end
def definition_version(swagger_doc)
if doc_version(swagger_doc).start_with?('2')
swagger_doc[:parameters]
else # Openapi3
if swagger_doc.key?(:parameters)
ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)')
swagger_doc[:parameters]
else
components = swagger_doc[:components] || {}
components[:parameters]
end
end
end
def add_verb(request, metadata) def add_verb(request, metadata)
request[:verb] = metadata[:operation][:verb] request[:verb] = metadata[:operation][:verb]
end end
@ -61,21 +105,21 @@ module Rswag
def add_path(request, metadata, swagger_doc, parameters, example) def add_path(request, metadata, swagger_doc, parameters, example)
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
request[:path] = template.tap do |template| request[:path] = template.tap do |path_template|
parameters.select { |p| p[:in] == :path }.each do |p| parameters.select { |p| p[:in] == :path }.each do |p|
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) path_template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
end end
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
template.concat(i == 0 ? '?' : '&') path_template.concat(i.zero? ? '?' : '&')
template.concat(build_query_string_part(p, example.send(p[:name]))) path_template.concat(build_query_string_part(p, example.send(p[:name])))
end end
end end
end end
def build_query_string_part(param, value) def build_query_string_part(param, value)
name = param[:name] name = param[:name]
return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array return "#{name}=#{value}" unless param[:type].to_sym == :array
case param[:collectionFormat] case param[:collectionFormat]
when :ssv when :ssv
@ -94,20 +138,20 @@ module Rswag
def add_headers(request, metadata, swagger_doc, parameters, example) def add_headers(request, metadata, swagger_doc, parameters, example)
tuples = parameters tuples = parameters
.select { |p| p[:in] == :header } .select { |p| p[:in] == :header }
.map { |p| [ p[:name], example.send(p[:name]).to_s ] } .map { |p| [p[:name], example.send(p[:name]).to_s] }
# Accept header # Accept header
produces = metadata[:operation][:produces] || swagger_doc[:produces] produces = metadata[:operation][:produces] || swagger_doc[:produces]
if produces if produces
accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
tuples << [ 'Accept', accept ] tuples << ['Accept', accept]
end end
# Content-Type header # Content-Type header
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
if consumes if consumes
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
tuples << [ 'Content-Type', content_type ] tuples << ['Content-Type', content_type]
end end
# Rails test infrastructure requires rackified headers # Rails test infrastructure requires rackified headers
@ -123,14 +167,14 @@ module Rswag
] ]
end end
request[:headers] = Hash[ rackified_tuples ] request[:headers] = Hash[rackified_tuples]
end end
def add_payload(request, parameters, example) def add_payload(request, parameters, example)
content_type = request[:headers]['CONTENT_TYPE'] content_type = request[:headers]['CONTENT_TYPE']
return if content_type.nil? return if content_type.nil?
if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type) if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
request[:payload] = build_form_payload(parameters, example) request[:payload] = build_form_payload(parameters, example)
else else
request[:payload] = build_json_payload(parameters, example) request[:payload] = build_json_payload(parameters, example)
@ -144,14 +188,18 @@ module Rswag
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
tuples = parameters tuples = parameters
.select { |p| p[:in] == :formData } .select { |p| p[:in] == :formData }
.map { |p| [ p[:name], example.send(p[:name]) ] } .map { |p| [p[:name], example.send(p[:name])] }
Hash[ tuples ] Hash[tuples]
end end
def build_json_payload(parameters, example) def build_json_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body }.first body_param = parameters.select { |p| p[:in] == :body }.first
body_param ? example.send(body_param[:name]).to_json : nil body_param ? example.send(body_param[:name]).to_json : nil
end end
def doc_version(doc)
doc[:openapi] || doc[:swagger] || '3'
end
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/slice'
require 'json-schema' require 'json-schema'
require 'json' require 'json'
@ -6,7 +8,6 @@ require 'rswag/specs/extended_schema'
module Rswag module Rswag
module Specs module Specs
class ResponseValidator class ResponseValidator
def initialize(config = ::Rswag::Specs.config) def initialize(config = ::Rswag::Specs.config)
@config = config @config = config
end end
@ -41,12 +42,30 @@ module Rswag
response_schema = metadata[:response][:schema] response_schema = metadata[:response][:schema]
return if response_schema.nil? return if response_schema.nil?
version = @config.get_swagger_doc_version(metadata[:swagger_doc])
schemas = definitions_or_component_schemas(swagger_doc, version)
validation_schema = response_schema validation_schema = response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(swagger_doc.slice(:definitions)) .merge(schemas)
errors = JSON::Validator.fully_validate(validation_schema, body) errors = JSON::Validator.fully_validate(validation_schema, body)
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
end end
def definitions_or_component_schemas(swagger_doc, version)
if version.start_with?('2')
swagger_doc.slice(:definitions)
else # Openapi3
if swagger_doc.key?(:definitions)
ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)')
swagger_doc.slice(:definitions)
else
components = swagger_doc[:components] || {}
{ components: { schemas: components[:schemas] } }
end
end
end
end end
class UnexpectedResponse < StandardError; end class UnexpectedResponse < StandardError; end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/deep_merge'
require 'rspec/core/formatters/base_text_formatter' require 'rspec/core/formatters/base_text_formatter'
require 'swagger_helper' require 'swagger_helper'
@ -20,26 +22,58 @@ module Rswag
def example_group_finished(notification) def example_group_finished(notification)
# NOTE: rspec 2.x support # NOTE: rspec 2.x support
if RSPEC_VERSION > 2 metadata = if RSPEC_VERSION > 2
metadata = notification.group.metadata notification.group.metadata
else else
metadata = notification.metadata notification.metadata
end end
# !metadata[:document] won't work, since nil means we should generate # !metadata[:document] won't work, since nil means we should generate
# docs. # docs.
return if metadata[:document] == false return if metadata[:document] == false
return unless metadata.has_key?(:response) return unless metadata.key?(:response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
unless doc_version(swagger_doc).start_with?('2')
# This is called multiple times per file!
# metadata[:operation] is also re-used between examples within file
# therefore be careful NOT to modify its content here.
upgrade_request_type!(metadata)
upgrade_servers!(swagger_doc)
upgrade_oauth!(swagger_doc)
upgrade_response_produces!(swagger_doc, metadata)
end
swagger_doc.deep_merge!(metadata_to_swagger(metadata)) swagger_doc.deep_merge!(metadata_to_swagger(metadata))
end end
def stop(notification=nil) def stop(_notification = nil)
@config.swagger_docs.each do |url_path, doc| @config.swagger_docs.each do |url_path, doc|
unless doc_version(doc).start_with?('2')
doc[:paths]&.each_pair do |_k, v|
v.each_pair do |_verb, value|
is_hash = value.is_a?(Hash)
if is_hash && value.dig(:parameters)
schema_param = value.dig(:parameters)&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
mime_list = value.dig(:consumes)
if value && schema_param && mime_list
value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content)
mime_list.each do |mime|
value[:requestBody][:content][mime] = { schema: schema_param[:schema] }
end
end
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
end
remove_invalid_operation_keys!(value)
end
end
end
file_path = File.join(@config.swagger_root, url_path) file_path = File.join(@config.swagger_root, url_path)
dirname = File.dirname(file_path) dirname = File.dirname(file_path)
FileUtils.mkdir_p dirname unless File.exists?(dirname) FileUtils.mkdir_p dirname unless File.exist?(dirname)
File.open(file_path, 'w') do |file| File.open(file_path, 'w') do |file|
file.write(pretty_generate(doc)) file.write(pretty_generate(doc))
@ -62,25 +96,108 @@ module Rswag
def yaml_prepare(doc) def yaml_prepare(doc)
json_doc = JSON.pretty_generate(doc) json_doc = JSON.pretty_generate(doc)
clean_doc = JSON.parse(json_doc) JSON.parse(json_doc)
end end
def metadata_to_swagger(metadata) def metadata_to_swagger(metadata)
response_code = metadata[:response][:code] response_code = metadata[:response][:code]
response = metadata[:response].reject { |k,v| k == :code } response = metadata[:response].reject { |k, _v| k == :code }
verb = metadata[:operation][:verb] verb = metadata[:operation][:verb]
operation = metadata[:operation] operation = metadata[:operation]
.reject { |k,v| k == :verb } .reject { |k, _v| k == :verb }
.merge(responses: { response_code => response }) .merge(responses: { response_code => response })
path_template = metadata[:path_item][:template] path_template = metadata[:path_item][:template]
path_item = metadata[:path_item] path_item = metadata[:path_item]
.reject { |k,v| k == :template } .reject { |k, _v| k == :template }
.merge(verb => operation) .merge(verb => operation)
{ paths: { path_template => path_item } } { paths: { path_template => path_item } }
end end
def doc_version(doc)
doc[:openapi] || doc[:swagger] || '3'
end
def upgrade_response_produces!(swagger_doc, metadata)
# Accept header
mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces])
target_node = metadata[:response]
upgrade_content!(mime_list, target_node)
metadata[:response].delete(:schema)
end
def upgrade_content!(mime_list, target_node)
target_node.merge!(content: {})
schema = target_node[:schema]
return if mime_list.empty? || schema.nil?
mime_list.each do |mime_type|
# TODO upgrade to have content-type specific schema
target_node[:content][mime_type] = { schema: schema }
end
end
def upgrade_request_type!(metadata)
# No deprecation here as it seems valid to allow type as a shorthand
operation_nodes = metadata[:operation][:parameters] || []
path_nodes = metadata[:path_item][:parameters] || []
header_node = metadata[:response][:headers] || {}
(operation_nodes + path_nodes + [header_node]).each do |node|
if node && node[:type] && node[:schema].nil?
node[:schema] = { type: node[:type] }
node.delete(:type)
end
end
end
def upgrade_servers!(swagger_doc)
return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: schemes, host, and basePath are replaced in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)')
swagger_doc[:servers] = { urls: [] }
swagger_doc[:schemes].each do |scheme|
swagger_doc[:servers][:urls] << scheme + '://' + swagger_doc[:host] + swagger_doc[:basePath]
end
swagger_doc.delete(:schemes)
swagger_doc.delete(:host)
swagger_doc.delete(:basePath)
end
def upgrade_oauth!(swagger_doc)
# find flow in securitySchemes (securityDefinitions will have been re-written)
schemes = swagger_doc.dig(:components, :securitySchemes)
return unless schemes&.any? { |_k, v| v.key?(:flow) }
schemes.each do |name, v|
next unless v.key?(:flow)
ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions flow is replaced in OpenAPI3! Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)")
flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
if flow == 'accessCode'
ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions accessCode is replaced in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)")
flow = 'authorizationCode'
end
if flow == 'application'
ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions application is replaced in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)")
flow = 'clientCredentials'
end
flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
end
swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
end
end
def remove_invalid_operation_keys!(value)
is_hash = value.is_a?(Hash)
value.delete(:consumes) if is_hash && value.dig(:consumes)
value.delete(:produces) if is_hash && value.dig(:produces)
end
end end
end end
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'rspec/core/rake_task' require 'rspec/core/rake_task'
namespace :rswag do namespace :rswag do
namespace :specs do namespace :specs 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 = ENV.fetch( t.pattern = ENV.fetch(
@ -12,12 +13,12 @@ namespace :rswag do
# 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
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']
end end
end end
end end
end end
task :rswag => ['rswag:specs:swaggerize'] task rswag: ['rswag:specs:swaggerize']

View File

@ -1,17 +1,19 @@
$:.push File.expand_path("../lib", __FILE__) # frozen_string_literal: true
$LOAD_PATH.push File.expand_path('lib', __dir__)
# 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 = ENV['TRAVIS_TAG'] || '0.0.0' s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ["domaindrivendev@gmail.com"] s.email = ['domaindrivendev@gmail.com']
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = 'https://github.com/rswag/rswag'
s.summary = "A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files" s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files'
s.description = "Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs" s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs'
s.license = "MIT" s.license = 'MIT'
s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ] s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile']
s.add_dependency 'activesupport', '>= 3.1', '< 7.0' s.add_dependency 'activesupport', '>= 3.1', '< 7.0'
s.add_dependency 'railties', '>= 3.1', '< 7.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'generator_spec' require 'generator_spec'
require 'generators/rspec/swagger_generator' require 'generators/rspec/swagger_generator'
require 'tmpdir' require 'tmpdir'
@ -9,12 +11,11 @@ module Rspec
before(:all) do before(:all) do
prepare_destination prepare_destination
fixtures_dir = File.expand_path('../fixtures', __FILE__) fixtures_dir = File.expand_path('fixtures', __dir__)
FileUtils.cp_r("#{fixtures_dir}/spec", destination_root) FileUtils.cp_r("#{fixtures_dir}/spec", destination_root)
end end
after(:all) do after(:all) do
end end
it 'installs the swagger_helper for rspec' do it 'installs the swagger_helper for rspec' do
@ -31,11 +32,11 @@ module Rspec
def fake_routes def fake_routes
{ {
"/posts/{post_id}/comments/{id}" => { '/posts/{post_id}/comments/{id}' => {
:params => ["post_id", "id"], params: ['post_id', 'id'],
:actions => { actions: {
"get" => { :summary=>"show comment" }, 'get' => { summary: 'show comment' },
"patch" => { :summary=>"update_comments comment" } 'patch' => { summary: 'update_comments comment' }
} }
} }
} }

View File

@ -1,16 +1,17 @@
# frozen_string_literal: true
require 'generator_spec' require 'generator_spec'
require 'generators/rswag/specs/install/install_generator' require 'generators/rswag/specs/install/install_generator'
module Rswag module Rswag
module Specs module Specs
RSpec.describe InstallGenerator do RSpec.describe InstallGenerator do
include GeneratorSpec::TestCase include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__) destination File.expand_path('tmp', __dir__)
before(:all) do before(:all) do
prepare_destination prepare_destination
fixtures_dir = File.expand_path('../fixtures', __FILE__) fixtures_dir = File.expand_path('fixtures', __dir__)
FileUtils.cp_r("#{fixtures_dir}/spec", destination_root) FileUtils.cp_r("#{fixtures_dir}/spec", destination_root)
run_generator run_generator

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'rswag/specs/configuration' require 'rswag/specs/configuration'
module Rswag module Rswag
module Specs module Specs
RSpec.describe Configuration do RSpec.describe Configuration do
subject { described_class.new(rspec_config) } subject { described_class.new(rspec_config) }

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'rswag/specs/example_group_helpers' require 'rswag/specs/example_group_helpers'
module Rswag module Rswag
module Specs module Specs
RSpec.describe ExampleGroupHelpers do RSpec.describe ExampleGroupHelpers do
subject { double('example_group') } subject { double('example_group') }
@ -34,31 +35,6 @@ module Rswag
end end
end end
describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do
before do
subject.tags('Blogs', 'Admin')
subject.description('Some description')
subject.operationId('createBlog')
subject.consumes('application/json', 'application/xml')
subject.produces('application/json', 'application/xml')
subject.schemes('http', 'https')
subject.deprecated(true)
end
let(:api_metadata) { { operation: {} } }
it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match(
tags: [ 'Blogs', 'Admin' ],
description: 'Some description',
operationId: 'createBlog',
consumes: [ 'application/json', 'application/xml' ],
produces: [ 'application/json', 'application/xml' ],
schemes: [ 'http', 'https' ],
deprecated: true
)
end
end
describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do
before do before do
subject.tags('Blogs', 'Admin') subject.tags('Blogs', 'Admin')
@ -74,12 +50,12 @@ module Rswag
it "adds to the 'operation' metadata" do it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match( expect(api_metadata[:operation]).to match(
tags: [ 'Blogs', 'Admin' ], tags: ['Blogs', 'Admin'],
description: 'Some description', description: 'Some description',
operationId: 'createBlog', operationId: 'createBlog',
consumes: [ 'application/json', 'application/xml' ], consumes: ['application/json', 'application/xml'],
produces: [ 'application/json', 'application/xml' ], produces: ['application/json', 'application/xml'],
schemes: [ 'http', 'https' ], schemes: ['http', 'https'],
deprecated: true, deprecated: true,
security: { api_key: [] } security: { api_key: [] }
) )
@ -87,14 +63,13 @@ module Rswag
end end
describe '#parameter(attributes)' do describe '#parameter(attributes)' do
context "when called at the 'path' level" do context "when called at the 'path' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet
it "adds to the 'path_item parameters' metadata" do it "adds to the 'path_item parameters' metadata" do
expect(api_metadata[:path_item][:parameters]).to match( expect(api_metadata[:path_item][:parameters]).to match(
[ name: :blog, in: :body, schema: { type: 'object' } ] [name: :blog, in: :body, schema: { type: 'object' }]
) )
end end
end end
@ -105,7 +80,7 @@ module Rswag
it "adds to the 'operation parameters' metadata" do it "adds to the 'operation parameters' metadata" do
expect(api_metadata[:operation][:parameters]).to match( expect(api_metadata[:operation][:parameters]).to match(
[ name: :blog, in: :body, schema: { type: 'object' } ] [name: :blog, in: :body, schema: { type: 'object' }]
) )
end end
end end
@ -116,7 +91,7 @@ module Rswag
it "automatically sets the 'required' flag" do it "automatically sets the 'required' flag" do
expect(api_metadata[:operation][:parameters]).to match( expect(api_metadata[:operation][:parameters]).to match(
[ name: :id, in: :path, required: true ] [name: :id, in: :path, required: true]
) )
end end
end end
@ -126,7 +101,7 @@ module Rswag
let(:api_metadata) { { operation: {} } } let(:api_metadata) { { operation: {} } }
it "does not require the 'in' parameter key" do it "does not require the 'in' parameter key" do
expect(api_metadata[:operation][:parameters]).to match([ name: :id ]) expect(api_metadata[:operation][:parameters]).to match([name: :id])
end end
end end
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'rswag/specs/example_helpers' require 'rswag/specs/example_helpers'
module Rswag module Rswag
module Specs module Specs
RSpec.describe ExampleHelpers do RSpec.describe ExampleHelpers do
subject { double('example') } subject { double('example') }
@ -15,6 +16,7 @@ module Rswag
let(:config) { double('config') } let(:config) { double('config') }
let(:swagger_doc) do let(:swagger_doc) do
{ {
swagger: '2.0',
securityDefinitions: { securityDefinitions: {
api_key: { api_key: {
type: :apiKey, type: :apiKey,
@ -24,13 +26,14 @@ module Rswag
} }
} }
end end
let(:metadata) do 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' ], 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' },
@ -58,7 +61,7 @@ module Rswag
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' } { 'CONTENT_TYPE' => 'application/json' }
) )
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'rswag/specs/request_factory' require 'rswag/specs/request_factory'
module Rswag module Rswag
module Specs module Specs
RSpec.describe RequestFactory do RSpec.describe RequestFactory do
subject { RequestFactory.new(config) } subject { RequestFactory.new(config) }
@ -10,7 +11,7 @@ module Rswag
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end end
let(:config) { double('config') } let(:config) { double('config') }
let(:swagger_doc) { {} } let(:swagger_doc) { { swagger: '2.0' } }
let(:example) { double('example') } let(:example) { double('example') }
let(:metadata) do let(:metadata) do
{ {
@ -53,7 +54,7 @@ module Rswag
allow(example).to receive(:q2).and_return('bar') allow(example).to receive(:q2).and_return('bar')
end end
it "builds the query string from example values" do it 'builds the query string from example values' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end end
end end
@ -63,40 +64,40 @@ module Rswag
metadata[:operation][:parameters] = [ metadata[:operation][:parameters] = [
{ name: 'things', in: :query, type: :array, collectionFormat: collection_format } { name: 'things', in: :query, type: :array, collectionFormat: collection_format }
] ]
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(:collection_format) { :csv } let(:collection_format) { :csv }
it "formats as comma separated values" do it 'formats as comma separated values' do
expect(request[:path]).to eq('/blogs?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(:collection_format) { :ssv } let(:collection_format) { :ssv }
it "formats as space separated values" do it 'formats as space separated values' do
expect(request[:path]).to eq('/blogs?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(:collection_format) { :tsv } let(:collection_format) { :tsv }
it "formats as tab separated values" do it 'formats as tab separated values' do
expect(request[:path]).to eq('/blogs?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(:collection_format) { :pipes } let(:collection_format) { :pipes }
it "formats as pipe separated values" do it 'formats as pipe separated values' do
expect(request[:path]).to eq('/blogs?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(:collection_format) { :multi } let(:collection_format) { :multi }
it "formats as multiple parameter instances" do it 'formats as multiple parameter instances' do
expect(request[:path]).to eq('/blogs?things=foo&things=bar') expect(request[:path]).to eq('/blogs?things=foo&things=bar')
end end
end end
@ -104,7 +105,7 @@ module Rswag
context "'header' parameters" do context "'header' parameters" do
before do before do
metadata[:operation][:parameters] = [ { name: 'Api-Key', in: :header, type: :string } ] metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }]
allow(example).to receive(:'Api-Key').and_return('foobar') allow(example).to receive(:'Api-Key').and_return('foobar')
end end
@ -127,9 +128,9 @@ module Rswag
end end
end end
context "consumes content" do context 'consumes content' do
before do before do
metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ] metadata[:operation][:consumes] = ['application/json', 'application/xml']
end end
context "no 'Content-Type' provided" do context "no 'Content-Type' provided" do
@ -150,18 +151,18 @@ module Rswag
context 'JSON payload' do context 'JSON payload' do
before do before do
metadata[:operation][:parameters] = [ { name: 'comment', in: :body, schema: { type: 'object' } } ] metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }]
allow(example).to receive(:comment).and_return(text: 'Some comment') allow(example).to receive(:comment).and_return(text: 'Some comment')
end end
it "serializes first 'body' parameter to JSON string" do it "serializes first 'body' parameter to JSON string" do
expect(request[:payload]).to eq("{\"text\":\"Some comment\"}") expect(request[:payload]).to eq('{"text":"Some comment"}')
end end
end end
context 'form payload' do context 'form payload' do
before do before do
metadata[:operation][:consumes] = [ 'multipart/form-data' ] metadata[:operation][:consumes] = ['multipart/form-data']
metadata[:operation][:parameters] = [ metadata[:operation][:parameters] = [
{ name: 'f1', in: :formData, type: :string }, { name: 'f1', in: :formData, type: :string },
{ name: 'f2', in: :formData, type: :string } { name: 'f2', in: :formData, type: :string }
@ -181,7 +182,7 @@ module Rswag
context 'produces content' do context 'produces content' do
before do before do
metadata[:operation][:produces] = [ 'application/json', 'application/xml' ] metadata[:operation][:produces] = ['application/json', 'application/xml']
end end
context "no 'Accept' value provided" do context "no 'Accept' value provided" do
@ -192,7 +193,7 @@ module Rswag
context "explicit 'Accept' value provided" do context "explicit 'Accept' value provided" do
before do before do
allow(example).to receive(:'Accept').and_return('application/xml') allow(example).to receive(:Accept).and_return('application/xml')
end end
it "sets 'HTTP_ACCEPT' header to example value" do it "sets 'HTTP_ACCEPT' header to example value" do
@ -202,9 +203,10 @@ module Rswag
end end
context 'basic auth' do context 'basic auth' do
context 'swagger 2.0' do
before do before do
swagger_doc[:securityDefinitions] = { basic: { type: :basic } } swagger_doc[:securityDefinitions] = { basic: { type: :basic } }
metadata[:operation][:security] = [ basic: [] ] metadata[:operation][:security] = [basic: []]
allow(example).to receive(:Authorization).and_return('Basic foobar') allow(example).to receive(:Authorization).and_return('Basic foobar')
end end
@ -213,10 +215,41 @@ module Rswag
end end
end end
context 'openapi 3.0.1' do
let(:swagger_doc) { { openapi: '3.0.1' } }
before do
swagger_doc[:components] = { securitySchemes: { 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 'openapi 3.0.1 upgrade notice' do
let(:swagger_doc) { { openapi: '3.0.1' } }
before do
allow(ActiveSupport::Deprecation).to receive(:warn)
swagger_doc[:securityDefinitions] = { basic: { type: :basic } }
metadata[:operation][:security] = [basic: []]
allow(example).to receive(:Authorization).and_return('Basic foobar')
end
it 'warns the user to upgrade' do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
expect(ActiveSupport::Deprecation).to have_received(:warn)
.with('Rswag::Specs: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)')
expect(swagger_doc[:components]).to have_key(:securitySchemes)
end
end
end
context 'apiKey' do context 'apiKey' do
before do before do
swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } } swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } }
metadata[:operation][:security] = [ apiKey: [] ] metadata[:operation][:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar') allow(example).to receive(:api_key).and_return('foobar')
end end
@ -256,8 +289,8 @@ module Rswag
context 'oauth2' do context 'oauth2' do
before do before do
swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: [ 'read:blogs' ] } } swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: ['read:blogs'] } }
metadata[:operation][:security] = [ oauth2: [ 'read:blogs' ] ] metadata[:operation][:security] = [oauth2: ['read:blogs']]
allow(example).to receive(:Authorization).and_return('Bearer foobar') allow(example).to receive(:Authorization).and_return('Bearer foobar')
end end
@ -272,34 +305,35 @@ module Rswag
basic: { type: :basic }, basic: { type: :basic },
api_key: { type: :apiKey, name: 'api_key', in: :query } api_key: { type: :apiKey, name: 'api_key', in: :query }
} }
metadata[:operation][:security] = [ { basic: [], api_key: [] } ] metadata[:operation][:security] = [{ basic: [], api_key: [] }]
allow(example).to receive(:Authorization).and_return('Basic foobar') allow(example).to receive(:Authorization).and_return('Basic foobar')
allow(example).to receive(:api_key).and_return('foobar') allow(example).to receive(:api_key).and_return('foobar')
end end
it "sets both params to example values" do it 'sets both params to example values' do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
expect(request[:path]).to eq('/blogs?api_key=foobar') expect(request[:path]).to eq('/blogs?api_key=foobar')
end end
end end
context "path-level parameters" do context 'path-level parameters' do
before do before do
metadata[:operation][:parameters] = [ { name: 'q1', in: :query, type: :string } ] metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }]
metadata[:path_item][:parameters] = [ { name: 'q2', 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(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar') allow(example).to receive(:q2).and_return('bar')
end end
it "populates operation and path level parameters " do it 'populates operation and path level parameters' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end end
end end
context 'referenced parameters' do context 'referenced parameters' do
context 'swagger 2.0' do
before do before do
swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
metadata[:operation][:parameters] = [ { '$ref' => '#/parameters/q1' } ] metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }]
allow(example).to receive(:q1).and_return('foo') allow(example).to receive(:q1).and_return('foo')
end end
@ -308,6 +342,38 @@ module Rswag
end end
end end
context 'openapi 3.0.1' do
let(:swagger_doc) { { openapi: '3.0.1' } }
before do
swagger_doc[:components] = { parameters: { q1: { name: 'q1', in: :query, type: :string } } }
metadata[:operation][:parameters] = [{ '$ref' => '#/components/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
context 'openapi 3.0.1 upgrade notice' do
let(:swagger_doc) { { openapi: '3.0.1' } }
before do
allow(ActiveSupport::Deprecation).to receive(:warn)
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 'warns the user to upgrade' do
expect(request[:path]).to eq('/blogs?q1=foo')
expect(ActiveSupport::Deprecation).to have_received(:warn)
.with('Rswag::Specs: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/')
expect(ActiveSupport::Deprecation).to have_received(:warn)
.with('Rswag::Specs: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)')
end
end
end
context 'global basePath' do context 'global basePath' do
before { swagger_doc[:basePath] = '/api' } before { swagger_doc[:basePath] = '/api' }
@ -316,18 +382,18 @@ module Rswag
end end
end end
context "global consumes" do context 'global consumes' do
before { swagger_doc[:consumes] = [ 'application/xml' ] } before { swagger_doc[:consumes] = ['application/xml'] }
it "defaults 'CONTENT_TYPE' to global value(s)" do it "defaults 'CONTENT_TYPE' to global value(s)" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end end
end end
context "global security requirements" do context 'global security requirements' do
before do before do
swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } } swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } }
swagger_doc[:security] = [ apiKey: [] ] swagger_doc[:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar') allow(example).to receive(:api_key).and_return('foobar')
end end

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
require 'rswag/specs/response_validator' require 'rswag/specs/response_validator'
module Rswag module Rswag
module Specs module Specs
RSpec.describe ResponseValidator do RSpec.describe ResponseValidator do
subject { ResponseValidator.new(config) } subject { ResponseValidator.new(config) }
before do before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
allow(config).to receive(:get_swagger_doc_version).and_return('2.0')
end end
let(:config) { double('config') } let(:config) { double('config') }
let(:swagger_doc) { {} } let(:swagger_doc) { {} }
@ -20,7 +22,7 @@ module Rswag
schema: { schema: {
type: :object, type: :object,
properties: { text: { type: :string } }, properties: { text: { type: :string } },
required: [ 'text' ] required: ['text']
} }
} }
} }
@ -32,43 +34,89 @@ module Rswag
OpenStruct.new( OpenStruct.new(
code: '200', code: '200',
headers: { 'X-Rate-Limit-Limit' => '10' }, headers: { 'X-Rate-Limit-Limit' => '10' },
body: "{\"text\":\"Some comment\"}" body: '{"text":"Some comment"}'
) )
end end
context "response matches metadata" do context 'response matches metadata' do
it { expect { call }.to_not raise_error } it { expect { call }.to_not raise_error }
end end
context "response code differs from metadata" do context 'response code differs from metadata' do
before { response.code = '400' } before { response.code = '400' }
it { expect { call }.to raise_error /Expected response code/ } it { expect { call }.to raise_error(/Expected response code/) }
end end
context "response headers differ from metadata" do context 'response headers differ from metadata' do
before { response.headers = {} } before { response.headers = {} }
it { expect { call }.to raise_error /Expected response header/ } it { expect { call }.to raise_error(/Expected response header/) }
end end
context "response body differs from metadata" do context 'response body differs from metadata' do
before { response.body = "{\"foo\":\"Some comment\"}" } before { response.body = '{"foo":"Some comment"}' }
it { expect { call }.to raise_error /Expected response body/ } it { expect { call }.to raise_error(/Expected response body/) }
end end
context 'referenced schemas' do context 'referenced schemas' do
context 'swagger 2.0' do
before do before do
swagger_doc[:definitions] = { swagger_doc[:definitions] = {
'blog' => { 'blog' => {
type: :object, type: :object,
properties: { foo: { type: :string } }, properties: { foo: { type: :string } },
required: [ 'foo' ] required: ['foo']
} }
} }
metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } metadata[:response][:schema] = { '$ref' => '#/definitions/blog' }
end end
it 'uses the referenced schema to validate the response body' do it 'uses the referenced schema to validate the response body' do
expect { call }.to raise_error /Expected response body/ expect { call }.to raise_error(/Expected response body/)
end
end
context 'openapi 3.0.1' do
context 'components/schemas' do
before do
allow(ActiveSupport::Deprecation).to receive(:warn)
allow(config).to receive(:get_swagger_doc_version).and_return('3.0.1')
swagger_doc[:components] = {
schemas: {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
required: ['foo']
}
}
}
metadata[:response][:schema] = { '$ref' => '#/components/schemas/blog' }
end
it 'uses the referenced schema to validate the response body' do
expect { call }.to raise_error(/Expected response body/)
end
end
context 'deprecated definitions' do
before do
allow(ActiveSupport::Deprecation).to receive(:warn)
allow(config).to receive(:get_swagger_doc_version).and_return('3.0.1')
swagger_doc[:definitions] = {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
required: ['foo']
}
}
metadata[:response][:schema] = { '$ref' => '#/definitions/blog' }
end
it 'warns the user to upgrade' do
expect { call }.to raise_error(/Expected response body/)
expect(ActiveSupport::Deprecation).to have_received(:warn)
.with('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)')
end
end
end end
end end
end end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
require 'rswag/specs/swagger_formatter' require 'rswag/specs/swagger_formatter'
require 'ostruct' require 'ostruct'
module Rswag module Rswag
module Specs module Specs
RSpec.describe SwaggerFormatter do RSpec.describe SwaggerFormatter do
subject { described_class.new(output, config) } subject { described_class.new(output, config) }
@ -13,44 +14,153 @@ module Rswag
end end
let(:config) { double('config') } let(:config) { double('config') }
let(:output) { double('output').as_null_object } let(:output) { double('output').as_null_object }
let(:swagger_root) { File.expand_path('../tmp/swagger', __FILE__) } let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) }
describe '#example_group_finished(notification)' do describe '#example_group_finished(notification)' do
before do before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
subject.example_group_finished(notification) subject.example_group_finished(notification)
end end
let(:swagger_doc) { {} }
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
let(:api_metadata) do let(:api_metadata) do
{ {
path_item: { template: '/blogs' }, path_item: { template: '/blogs', parameters: [{ type: :string }] },
operation: { verb: :post, summary: 'Creates a blog' }, operation: { verb: :post, summary: 'Creates a blog', parameters: [{ type: :string }] },
response: { code: '201', description: 'blog created' }, response: { code: '201', description: 'blog created', headers: { type: :string }, schema: { '$ref' => '#/definitions/blog' } },
document: document document: document
} }
end end
context 'with the document tag set to false' do context 'with the document tag set to false' do
let(:swagger_doc) { { swagger: '2.0' } }
let(:document) { false } let(:document) { false }
it 'does not update the swagger doc' do it 'does not update the swagger doc' do
expect(swagger_doc).to be_empty expect(swagger_doc).to match({ swagger: '2.0' })
end end
end end
context 'with the document tag set to anything but false' do context 'with the document tag set to anything but false' do
let(:swagger_doc) { { swagger: '2.0' } }
# anything works, including its absence when specifying responses. # anything works, including its absence when specifying responses.
let(:document) { nil } 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(
swagger: '2.0',
paths: { paths: {
'/blogs' => { '/blogs' => {
parameters: [{ type: :string }],
post: { post: {
parameters: [{ type: :string }],
summary: 'Creates a blog', summary: 'Creates a blog',
responses: { responses: {
'201' => { description: 'blog created' } '201' => {
description: 'blog created',
headers: { type: :string },
schema: { '$ref' => '#/definitions/blog' }
}
}
}
}
}
)
end
end
context 'with metadata upgrades for 3.0' do
let(:swagger_doc) do
{
openapi: '3.0.1',
basePath: '/foo',
schemes: ['http', 'https'],
host: 'api.example.com',
produces: ['application/vnd.my_mime', 'application/json'],
components: {
securitySchemes: {
myClientCredentials: {
type: :oauth2,
flow: :application,
token_url: :somewhere
},
myAuthorizationCode: {
type: :oauth2,
flow: :accessCode,
token_url: :somewhere
},
myImplicit: {
type: :oauth2,
flow: :implicit,
token_url: :somewhere
}
}
}
}
end
let(:document) { nil }
it 'converts query and path params, type: to schema: { type: }' do
expect(swagger_doc.slice(:paths)).to match(
paths: {
'/blogs' => {
parameters: [{ schema: { type: :string } }],
post: {
parameters: [{ schema: { type: :string } }],
summary: 'Creates a blog',
responses: {
'201' => {
content: {
'application/vnd.my_mime' => {
schema: { '$ref' => '#/definitions/blog' }
},
'application/json' => {
schema: { '$ref' => '#/definitions/blog' }
}
},
description: 'blog created',
headers: { schema: { type: :string } }
}
}
}
}
}
)
end
it 'converts basePath, schemas and host to urls' do
expect(swagger_doc.slice(:servers)).to match(
servers: {
urls: ['http://api.example.com/foo', 'https://api.example.com/foo']
}
)
end
it 'upgrades oauth flow to flows' do
expect(swagger_doc.slice(:components)).to match(
components: {
securitySchemes: {
myClientCredentials: {
type: :oauth2,
flows: {
'clientCredentials' => {
token_url: :somewhere
}
}
},
myAuthorizationCode: {
type: :oauth2,
flows: {
'authorizationCode' => {
token_url: :somewhere
}
}
},
myImplicit: {
type: :oauth2,
flows: {
'implicit' => {
token_url: :somewhere
}
} }
} }
} }
@ -62,19 +172,21 @@ module Rswag
describe '#stop' do describe '#stop' do
before do before do
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
allow(config).to receive(:swagger_docs).and_return( allow(config).to receive(:swagger_docs).and_return(
'v1/swagger.json' => { info: { version: 'v1' } }, 'v1/swagger.json' => doc_1,
'v2/swagger.json' => { info: { version: 'v2' } } 'v2/swagger.json' => doc_2
) )
allow(config).to receive(:swagger_format).and_return(swagger_format) allow(config).to receive(:swagger_format).and_return(swagger_format)
subject.stop(notification) subject.stop(notification)
end end
let(:notification) { double('notification') } let(:doc_1) { { info: { version: 'v1' } } }
context 'with default format' do let(:doc_2) { { info: { version: 'v2' } } }
let(:swagger_format) { :json } let(:swagger_format) { :json }
let(:notification) { double('notification') }
context 'with default format' do
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")
@ -93,8 +205,77 @@ module Rswag
end end
end end
context 'with oauth3 upgrades' do
let(:doc_2) do
{
paths: {
'/path/' => {
get: {
summary: 'Retrieve Nested Paths',
tags: ['nested Paths'],
produces: ['application/json'],
consumes: ['application/xml', 'application/json'],
parameters: [{
in: :body,
schema: { foo: :bar }
}, {
in: :headers
}]
}
}
}
}
end
it 'removes remaining consumes/produces' do
expect(doc_2[:paths]['/path/'][:get].keys).to eql([:summary, :tags, :parameters, :requestBody])
end
it 'duplicates params in: :body to requestBody from consumes list' do
expect(doc_2[:paths]['/path/'][:get][:parameters]).to eql([{ in: :headers }])
expect(doc_2[:paths]['/path/'][:get][:requestBody]).to eql(content: {
'application/xml' => { schema: { foo: :bar } },
'application/json' => { schema: { foo: :bar } }
})
end
end
context 'with oauth3 formData' do
let(:doc_2) do
{
paths: {
'/path/' => {
post: {
summary: 'Retrieve Nested Paths',
tags: ['nested Paths'],
produces: ['application/json'],
consumes: ['multipart/form-data'],
parameters: [{
in: :formData,
schema: { type: :file }
},{
in: :headers
}]
}
}
}
}
end
it 'removes remaining consumes/produces' do
expect(doc_2[:paths]['/path/'][:post].keys).to eql([:summary, :tags, :parameters, :requestBody])
end
it 'duplicates params in: :formData to requestBody from consumes list' do
expect(doc_2[:paths]['/path/'][:post][:parameters]).to eql([{ in: :headers }])
expect(doc_2[:paths]['/path/'][:post][:requestBody]).to eql(content: {
'multipart/form-data' => { schema: { type: :file } }
})
end
end
after do after do
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Rails module Rails
module VERSION module VERSION
MAJOR = 3 MAJOR = 3

View File

@ -1,2 +1,4 @@
# frozen_string_literal: true
# NOTE: For the specs in this gem, all configuration is completely mocked out # NOTE: For the specs in this gem, all configuration is completely mocked out
# The file just needs to be present because it gets required by the swagger_formatter # The file just needs to be present because it gets required by the swagger_formatter

View File

@ -1,17 +1,19 @@
$:.push File.expand_path("../lib", __FILE__) # frozen_string_literal: true
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
# 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 = ENV['TRAVIS_TAG'] || '0.0.0' s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ["domaindrivendev@gmail.com"] s.email = ['domaindrivendev@gmail.com']
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = 'https://github.com/rswag/rswag'
s.summary = "A Rails Engine that includes swagger-ui and powers it from configured Swagger endpoints" s.summary = 'A Rails Engine that includes swagger-ui and powers it from configured Swagger endpoints'
s.description = "Expose beautiful API documentation, that's powered by Swagger JSON endpoints, including a UI to explore and test operations" s.description = 'Expose beautiful API documentation, powered by Swagger JSON endpoints, including a UI to explore and test operations'
s.license = "MIT" s.license = 'MIT'
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', '< 7.0' s.add_dependency 'actionpack', '>=3.1', '< 7.0'
s.add_dependency 'railties', '>= 3.1', '< 7.0' s.add_dependency 'railties', '>= 3.1', '< 7.0'

View File

@ -1,17 +1,19 @@
$:.push File.expand_path("../lib", __FILE__) # frozen_string_literal: true
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
# 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 = ENV['TRAVIS_TAG'] || '0.0.0' s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris"] s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ["domaindrivendev@gmail.com"] s.email = ['domaindrivendev@gmail.com']
s.homepage = "https://github.com/domaindrivendev/rswag" s.homepage = 'https://github.com/rswag/rswag'
s.summary = "Swagger tooling for Rails API's" s.summary = 'Swagger tooling for Rails APIs'
s.description = "Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests" s.description = 'Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests'
s.license = "MIT" s.license = 'MIT'
s.files = Dir["{lib}/**/*"] + [ "MIT-LICENSE" ] s.files = Dir['{lib}/**/*'] + [ 'MIT-LICENSE' ]
s.add_dependency 'rswag-specs', ENV['TRAVIS_TAG'] || '0.0.0' s.add_dependency 'rswag-specs', ENV['TRAVIS_TAG'] || '0.0.0'
s.add_dependency 'rswag-api', ENV['TRAVIS_TAG'] || '0.0.0' s.add_dependency 'rswag-api', ENV['TRAVIS_TAG'] || '0.0.0'

View File

@ -22,7 +22,7 @@ module Rswag
end end
it 'installs initializer for rswag-api' do it 'installs initializer for rswag-api' do
assert_file('config/rswag-api.rb') assert_file('config/rswag_api.rb')
end end
it 'installs initializer for rswag-ui' do it 'installs initializer for rswag-ui' do

View File

@ -1,4 +0,0 @@
exit
env['PATH_INFO']
env['SCRIPT_NAME']
env

View File

@ -5,3 +5,8 @@
require File.expand_path('../config/application', __FILE__) require File.expand_path('../config/application', __FILE__)
TestApp::Application.load_tasks TestApp::Application.load_tasks
RSpec::Core::RakeTask.new('swaggerize') do |t|
t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ]
end

View File

@ -8,6 +8,25 @@ class BlogsController < ApplicationController
respond_with @blog respond_with @blog
end end
# POST /blogs/flexible
def flexible_create
# contrived example to play around with new anyOf and oneOf
# request body definition for 3.0
blog_params = params.require(:blog).permit(:title, :content, :headline, :text)
@blog = Blog.create(blog_params)
respond_with @blog
end
# POST /blogs/alternate
def alternate_create
# contrived example to show different :examples in the requestBody section
@blog = Blog.create(params.require(:blog).permit(:title, :content))
respond_with @blog
end
# Put /blogs/1 # Put /blogs/1
def upload def upload
@blog = Blog.find_by_id(params[:id]) @blog = Blog.find_by_id(params[:id])

View File

@ -1,11 +1,16 @@
# frozen_string_literal: true
class Blog < ActiveRecord::Base class Blog < ActiveRecord::Base
validates :content, presence: true validates :content, presence: true
def as_json(options) alias_attribute :headline, :title
alias_attribute :text, :content
def as_json(_options)
{ {
id: id, id: id,
title: title, title: title,
content: nil, content: content,
thumbnail: thumbnail thumbnail: thumbnail
} }
end end

View File

@ -11,6 +11,7 @@ Bundler.require(*Rails.groups)
module TestApp module TestApp
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 5.2
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded. # -- all .rb files in that directory are automatically loaded.

View File

@ -1,10 +1,12 @@
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# Your secret key for verifying the integrity of signed cookies. if Rails.version.first.to_i < 5
# If you change this key, all old signed cookies will become invalid! # Your secret key for verifying the integrity of signed cookies.
# Make sure the secret is at least 30 characters and all random, # If you change this key, all old signed cookies will become invalid!
# no regular words or you'll be exposed to dictionary attacks. # Make sure the secret is at least 30 characters and all random,
TestApp::Application.config.secret_token = '60f36cd33756d73f362053f1d45256ae50d75440b634ae73b070a6e35a2df38692f59e28e5ecbd1f9f2e850255f6d29a468bc59ac4484c2b7f0548ddbfc1b870' # no regular words or you'll be exposed to dictionary attacks.
TestApp::Application.config.secret_token = '60f36cd33756d73f362053f1d45256ae50d75440b634ae73b070a6e35a2df38692f59e28e5ecbd1f9f2e850255f6d29a468bc59ac4484c2b7f0548ddbfc1b870'
# See http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#config-secrets-yml else
TestApp::Application.config.secret_key_base = 'f6a820cc8aa76094583cd68ef46a735e25e3278648086355f8bd24721f036959c728c06a28dcecfe695f17ae2db44dfa1424f22b81377f2a1496d4e19f6f7faa' # See http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#config-secrets-yml
TestApp::Application.config.secret_key_base = 'f6a820cc8aa76094583cd68ef46a735e25e3278648086355f8bd24721f036959c728c06a28dcecfe695f17ae2db44dfa1424f22b81377f2a1496d4e19f6f7faa'
end

View File

@ -1,4 +1,7 @@
TestApp::Application.routes.draw do TestApp::Application.routes.draw do
post '/blogs/flexible', to: 'blogs#flexible_create'
post '/blogs/alternate', to: 'blogs#alternate_create'
resources :blogs resources :blogs
put '/blogs/:id/upload', to: 'blogs#upload' put '/blogs/:id/upload', to: 'blogs#upload'

View File

@ -2,11 +2,11 @@
# 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.
# #
# This file is the source Rails uses to define your schema when running `rails # Note that this schema.rb definition is the authoritative source for your
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to # database schema. If you need to create the application database on another
# be faster and is potentially less error prone than running all of your # system, you should be using db:schema:load, not running all the migrations
# migrations from scratch. Old migrations may fail to apply correctly if those # from scratch. The latter is a flawed and unsustainable approach (the more migrations
# migrations use external dependencies or application code. # you'll amass, the slower it'll run and the greater likelihood for issues).
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.

View File

@ -1,12 +1,13 @@
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.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
tags 'Auth Tests' tags 'Auth Tests'
operationId 'testBasicAuth' operationId 'testBasicAuth'
security [ basic_auth: [] ] security [basic_auth: []]
response '204', 'Valid credentials' do response '204', 'Valid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" } let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
@ -24,7 +25,7 @@ RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json'
post 'Authenticates with an api key' do post 'Authenticates with an api key' do
tags 'Auth Tests' tags 'Auth Tests'
operationId 'testApiKey' operationId 'testApiKey'
security [ api_key: [] ] security [api_key: []]
response '204', 'Valid credentials' do response '204', 'Valid credentials' do
let(:api_key) { 'foobar' } let(:api_key) { 'foobar' }
@ -42,7 +43,7 @@ RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json'
post 'Authenticates with basic auth and api key' do post 'Authenticates with basic auth and api key' do
tags 'Auth Tests' tags 'Auth Tests'
operationId 'testBasicAndApiKey' operationId 'testBasicAndApiKey'
security [ { basic_auth: [], api_key: [] } ] security [{ basic_auth: [], api_key: [] }]
response '204', 'Valid credentials' do response '204', 'Valid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" } let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }

View File

@ -15,6 +15,7 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
let(:blog) { { title: 'foo', content: 'bar' } } let(:blog) { { title: 'foo', content: 'bar' } }
response '201', 'blog created' do response '201', 'blog created' do
# schema '$ref' => '#/definitions/blog'
run_test! run_test!
end end
@ -48,6 +49,30 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
end end
end end
path '/blogs/flexible' do
post 'Creates a blog flexible body' do
tags 'Blogs'
description 'Creates a flexible blog from provided data'
operationId 'createFlexibleBlog'
consumes 'application/json'
produces 'application/json'
parameter name: :flexible_blog, in: :body, schema: {
oneOf: [
{ '$ref' => '#/definitions/blog' },
{ '$ref' => '#/definitions/flexible_blog' }
]
}
let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } }
response '201', 'flexible blog created' do
schema oneOf: [{ '$ref' => '#/definitions/blog' }, { '$ref' => '#/definitions/flexible_blog' }]
run_test!
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
@ -71,7 +96,7 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
id: 1, id: 1,
title: 'Hello world!', title: 'Hello world!',
content: 'Hello world and hello universe. Thank you all very much!!!', content: 'Hello world and hello universe. Thank you all very much!!!',
thumbnail: "thumbnail.png" thumbnail: 'thumbnail.png'
} }
let(:id) { blog.id } let(:id) { blog.id }

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# This file is copied to spec/ when you run 'rails generate rspec:install' # This file is copied to spec/ when you run 'rails generate rspec:install'
ENV['RAILS_ENV'] ||= 'test' ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__) require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production # Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production? abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'spec_helper' require 'spec_helper'
require 'rspec/rails' require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point! # Add additional requires below this line. Rails is not loaded until this point!

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
require 'rake' require 'rake'
@ -5,11 +7,11 @@ 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
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
end end
it 'generates Swagger JSON files from integration specs' do it 'generates Swagger JSON files from integration specs' do
expect { Rake::Task['rswag:specs:swaggerize'].invoke }.not_to raise_exception Rake::Task['rswag:specs:swaggerize'].invoke
expect(File).to exist("#{swagger_root}/v1/swagger.json") expect(File).to exist("#{swagger_root}/v1/swagger.json")
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This file was generated by the `rails generate rspec:install` command. Conventionally, all # This file was generated by the `rails generate rspec:install` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause # The generated `.rspec` file contains `--require spec_helper` which will cause
@ -50,54 +52,4 @@ RSpec.configure do |config|
config.after(:suite) do config.after(:suite) do
File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png") File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png")
end end
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
=begin
# This allows you to limit a spec run to individual examples or groups
# you care about by tagging them with `:focus` metadata. When nothing
# is tagged with `:focus`, all examples get run. RSpec also provides
# aliases for `it`, `describe`, and `context` that include `:focus`
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
config.filter_run_when_matching :focus
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
if config.files_to_run.one?
# Use the documentation formatter for detailed output,
# unless a formatter has already been configured
# (e.g. via a command-line flag).
config.default_formatter = 'doc'
end
# Print the 10 slowest examples and example groups at the
# end of the spec run, to help surface which specs are running
# particularly slow.
config.profile_examples = 10
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
=end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.configure do |config| RSpec.configure do |config|
@ -5,7 +7,7 @@ RSpec.configure do |config|
# 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.to_s + '/swagger'
config.swagger_dry_run = false
# 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:to_swagger' rake task, the complete Swagger will # When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will
# be generated at the provided relative path under swagger_root # be generated at the provided relative path under swagger_root
@ -14,12 +16,22 @@ RSpec.configure do |config|
# 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.json' => {
swagger: '2.0', openapi: '3.0.0',
info: { info: {
title: 'API V1', title: 'API V1',
version: 'v1' version: 'v1'
}, },
paths: {}, paths: {},
servers: [
{
url: 'https://{defaultHost}',
variables: {
defaultHost: {
default: 'www.example.com'
}
}
}
],
definitions: { definitions: {
errors_object: { errors_object: {
type: 'object', type: 'object',
@ -40,9 +52,19 @@ RSpec.configure do |config|
id: { type: 'integer' }, id: { type: 'integer' },
title: { type: 'string' }, title: { type: 'string' },
content: { type: 'string', 'x-nullable': true }, content: { type: 'string', 'x-nullable': true },
thumbnail: { type: 'string'} thumbnail: { type: 'string', 'x-nullable': true}
}, },
required: [ 'id', 'title', 'content', 'thumbnail' ] required: [ 'id', 'title' ]
},
flexible_blog: {
type: 'object',
properties: {
id: { type: 'integer' },
headline: { type: 'string' },
text: { type: 'string', nullable: true },
thumbnail: { type: 'string', nullable: true }
},
required: ['id', 'headline']
} }
}, },
securityDefinitions: { securityDefinitions: {

View File

@ -1,5 +1,5 @@
{ {
"swagger": "2.0", "openapi": "3.0.0",
"info": { "info": {
"title": "API V1", "title": "API V1",
"version": "v1" "version": "v1"
@ -21,10 +21,14 @@
], ],
"responses": { "responses": {
"204": { "204": {
"description": "Valid credentials" "description": "Valid credentials",
"content": {
}
}, },
"401": { "401": {
"description": "Invalid credentials" "description": "Invalid credentials",
"content": {
}
} }
} }
} }
@ -45,10 +49,14 @@
], ],
"responses": { "responses": {
"204": { "204": {
"description": "Valid credentials" "description": "Valid credentials",
"content": {
}
}, },
"401": { "401": {
"description": "Invalid credentials" "description": "Invalid credentials",
"content": {
}
} }
} }
} }
@ -72,10 +80,14 @@
], ],
"responses": { "responses": {
"204": { "204": {
"description": "Valid credentials" "description": "Valid credentials",
"content": {
}
}, },
"401": { "401": {
"description": "Invalid credentials" "description": "Invalid credentials",
"content": {
}
} }
} }
} }
@ -88,32 +100,35 @@
], ],
"description": "Creates a new blog from provided data", "description": "Creates a new blog from provided data",
"operationId": "createBlog", "operationId": "createBlog",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [ "parameters": [
{
"name": "blog",
"in": "body",
"schema": {
"$ref": "#/definitions/blog"
}
}
], ],
"responses": { "responses": {
"201": { "201": {
"description": "blog created" "description": "blog created",
"content": {
}
}, },
"422": { "422": {
"description": "invalid request", "description": "invalid request",
"content": {
"application/json": {
"schema": { "schema": {
"$ref": "#/definitions/errors_object" "$ref": "#/definitions/errors_object"
} }
} }
} }
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/definitions/blog"
}
}
}
}
}, },
"get": { "get": {
"summary": "Searches blogs", "summary": "Searches blogs",
@ -122,19 +137,68 @@
], ],
"description": "Searches blogs by keywords", "description": "Searches blogs by keywords",
"operationId": "searchBlogs", "operationId": "searchBlogs",
"produces": [
"application/json"
],
"parameters": [ "parameters": [
{ {
"name": "keywords", "name": "keywords",
"in": "query", "in": "query",
"schema": {
"type": "string" "type": "string"
} }
}
], ],
"responses": { "responses": {
"406": { "406": {
"description": "unsupported accept header" "description": "unsupported accept header",
"content": {
}
}
}
}
},
"/blogs/flexible": {
"post": {
"summary": "Creates a blog flexible body",
"tags": [
"Blogs"
],
"description": "Creates a flexible blog from provided data",
"operationId": "createFlexibleBlog",
"parameters": [
],
"responses": {
"201": {
"description": "flexible blog created",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/definitions/blog"
},
{
"$ref": "#/definitions/flexible_blog"
}
]
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/definitions/blog"
},
{
"$ref": "#/definitions/flexible_blog"
}
]
}
}
} }
} }
} }
@ -144,8 +208,10 @@
{ {
"name": "id", "name": "id",
"in": "path", "in": "path",
"type": "string", "required": true,
"required": true "schema": {
"type": "string"
}
} }
], ],
"get": { "get": {
@ -155,9 +221,6 @@
], ],
"description": "Retrieves a specific blog by id", "description": "Retrieves a specific blog by id",
"operationId": "getBlog", "operationId": "getBlog",
"produces": [
"application/json"
],
"responses": { "responses": {
"200": { "200": {
"description": "blog found", "description": "blog found",
@ -172,9 +235,6 @@
"type": "string" "type": "string"
} }
}, },
"schema": {
"$ref": "#/definitions/blog"
},
"examples": { "examples": {
"application/json": { "application/json": {
"id": 1, "id": 1,
@ -182,10 +242,19 @@
"content": "Hello world and hello universe. Thank you all very much!!!", "content": "Hello world and hello universe. Thank you all very much!!!",
"thumbnail": "thumbnail.png" "thumbnail": "thumbnail.png"
} }
},
"content": {
"application/json": {
"schema": {
"$ref": "#/definitions/blog"
}
}
} }
}, },
"404": { "404": {
"description": "blog not found" "description": "blog not found",
"content": {
}
} }
} }
} }
@ -195,8 +264,10 @@
{ {
"name": "id", "name": "id",
"in": "path", "in": "path",
"type": "string", "required": true,
"required": true "schema": {
"type": "string"
}
} }
], ],
"put": { "put": {
@ -206,25 +277,38 @@
], ],
"description": "Upload a thumbnail for specific blog by id", "description": "Upload a thumbnail for specific blog by id",
"operationId": "uploadThumbnailBlog", "operationId": "uploadThumbnailBlog",
"consumes": [
"multipart/form-data"
],
"parameters": [ "parameters": [
{
"name": "file",
"in": "formData",
"type": "file",
"required": true
}
], ],
"responses": { "responses": {
"200": { "200": {
"description": "blog updated" "description": "blog updated",
"content": {
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "file"
}
}
} }
} }
} }
} }
}, },
"servers": [
{
"url": "https://{defaultHost}",
"variables": {
"defaultHost": {
"default": "www.example.com"
}
}
}
],
"definitions": { "definitions": {
"errors_object": { "errors_object": {
"type": "object", "type": "object",
@ -257,18 +341,41 @@
"x-nullable": true "x-nullable": true
}, },
"thumbnail": { "thumbnail": {
"type": "string" "type": "string",
"x-nullable": true
} }
}, },
"required": [ "required": [
"id", "id",
"title", "title"
"content", ]
"thumbnail" },
"flexible_blog": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"headline": {
"type": "string"
},
"text": {
"type": "string",
"nullable": true
},
"thumbnail": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"headline"
] ]
} }
}, },
"securityDefinitions": { "components": {
"securitySchemes": {
"basic_auth": { "basic_auth": {
"type": "basic" "type": "basic"
}, },
@ -278,4 +385,5 @@
"in": "query" "in": "query"
} }
} }
}
} }