diff --git a/.gitignore b/.gitignore index d7a22a8..181022d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ **/*/node_modules *.swp Gemfile.lock +/.idea/ +**/.byebug_history diff --git a/CHANGELOG.md b/CHANGELOG.md index aed1d80..a0af2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added ### Changed -- Update swagger-ui version to 3.23.11 [#239](https://github.com/rswag/rswag/pull/239) ### Deprecated ### Removed ### Fixed ### 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 ### Added - New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7db3926..84ae9df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,11 @@ cd - ``` ## Test +Initialize the rswag-ui repo with assets. +``` +ci/build.sh +``` + Make sure the tests pass: ``` ./ci/test.sh diff --git a/Gemfile b/Gemfile index b9f1fbe..38bb3ad 100644 --- a/Gemfile +++ b/Gemfile @@ -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. # 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 when '3' @@ -24,18 +26,23 @@ gem 'rswag-api', path: './rswag-api' gem 'rswag-ui', path: './rswag-ui' group :test do - gem 'test-unit' - gem 'rspec-rails' - gem 'generator_spec' gem 'capybara' gem 'geckodriver-helper' + gem 'generator_spec' + gem 'rspec-rails' gem 'selenium-webdriver' gem 'rswag-specs', path: './rswag-specs' + gem 'test-unit' +end + +group :development do + gem 'rswag-specs', path: './rswag-specs' + gem 'rubocop' end group :assets do - gem 'uglifier' gem 'therubyracer' + gem 'uglifier' end gem 'byebug' diff --git a/README.md b/README.md index 317eaeb..5a5c0c3 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,13 @@ rswag [![Build Status](https://travis-ci.org/rswag/rswag.svg?branch=master)](https://travis-ci.org/rswag/rswag) [![Maintainability](https://api.codeclimate.com/v1/badges/1175b984edc4610f82ab/maintainability)](https://codeclimate.com/github/rswag/rswag/maintainability) -[Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests. +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. +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 ... 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| |----------|----------|----------| -|[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| |[1.6.0](https://github.com/rswag/rswag/tree/1.6.0)|2.0|2.2.5| + + +**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) + + + + + ## Getting Started ## 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. +There is also a generator which can help get you started `rails generate rspec:swagger API::MyController` ```ruby # 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 tags 'Blogs' - consumes 'application/json', 'application/xml' + consumes 'application/json' parameter name: :blog, in: :body, schema: { type: :object, 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 tags 'Blogs' produces 'application/json', 'application/xml' - parameter name: :id, :in => :path, :type => :string + parameter name: :id, in: :path, type: :string response '200', 'blog found' do schema type: :object, @@ -123,8 +166,6 @@ Once you have an API that can describe itself in Swagger, you've opened the trea 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) @@ -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`. + 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_! ## The rspec DSL ## @@ -173,7 +219,7 @@ end ### Null Values ### -This library is currently using JSON::Draft4 for validation of response models. It does not support null as a value. So you can add the property 'x-nullable' to a definition to allow null/nil values to pass. +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 describe 'Blogs API' do path '/blogs' do @@ -184,8 +230,8 @@ describe 'Blogs API' do schema type: :object, properties: { id: { type: :integer }, - title: { type: :string }, - content: { type: :string, 'x-nullable': true } + title: { type: :string, nullable: true }, # preferred syntax + content: { type: :string, 'x-nullable': true } # legacy syntax, but still works } .... end @@ -193,12 +239,45 @@ describe 'Blogs API' do 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. - + +### 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 ### -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 # spec/swagger_helper.rb @@ -207,23 +286,41 @@ RSpec.configure do |config| config.swagger_docs = { 'v1/swagger.json' => { - swagger: '2.0', + openapi: '3.0.1', info: { title: 'API V1', version: 'v1', 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' => { - openapi: '3.0.0', + openapi: '3.0.1', info: { title: 'API V2', version: 'v2', description: 'This is the second version of my API' }, - basePath: '/api/v2' + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ] } } end @@ -265,7 +362,9 @@ you should use the folowing syntax, making sure there are no whitespaces at the ### 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 # spec/swagger_helper.rb @@ -274,15 +373,18 @@ RSpec.configure do |config| config.swagger_docs = { 'v1/swagger.json' => { - ... - securityDefinitions: { - basic: { - type: :basic - }, - apiKey: { - type: :apiKey, - name: 'api_key', - in: :query + ... # note the new Open API 3.0 compliant security structure here, under "components" + components: { + securitySchemes: { + basic_auth: { + type: :http, + scheme: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } } } } @@ -296,7 +398,7 @@ describe 'Blogs API' do post 'Creates a blog' do tags 'Blogs' - security [ basic: [] ] + security [ basic_auth: [] ] ... response '201', 'blog created' do @@ -311,9 +413,35 @@ describe 'Blogs API' do 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 ## @@ -322,7 +450,7 @@ The steps described above will get you up and running with minimal setup. Howeve |Gem|Description|Added/Updated| |---------|-----------|-------------| |__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_| ### Output Location for Generated Swagger Files ### @@ -337,7 +465,7 @@ RSpec.configure do |config| 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 ### @@ -350,28 +478,43 @@ rake rswag:specs:swaggerize PATTERN="spec/swagger/**/*_spec.rb" ### 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 # spec/swagger_helper.rb config.swagger_docs = { 'v1/swagger.json' => { - swagger: '2.0', + openapi: '3.0.0', info: { title: 'API V1' }, - definitions: { - errors_object: { - type: 'object', - properties: { - errors: { '$ref' => '#/definitions/errors_map' } - } - }, - errors_map: { - type: 'object', - additionalProperties: { - type: 'array', - items: { type: 'string' } + components: { + schemas: { + errors_object: { + type: 'object', + properties: { + errors: { '$ref' => '#/components/schemas/errors_map' } + } + }, + errors_map: { + type: 'object', + additionalProperties: { + type: 'array', + 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 response 422, 'invalid request' do - schema '$ref' => '#/definitions/errors_object' + schema '$ref' => '#/components/schemas/errors_object' ... end @@ -398,14 +541,15 @@ describe 'Blogs API' do post 'Creates a comment' do response 422, 'invalid request' do - schema '$ref' => '#/definitions/errors_object' + schema '$ref' => '#/components/schemas/errors_object' ... end ``` ### Response headers ### -In Rswag, you could use `header` method inside the response block to specify header objects for this response. Rswag will validate your response headers with those header objects and inject them into the generated swagger file: +In Rswag, you could use `header` method inside the response block to specify header objects for this response. +Rswag will validate your response headers with those header objects and inject them into the generated swagger file: ```ruby # spec/integration/comments_spec.rb @@ -425,7 +569,7 @@ end ### Response examples ### 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 # spec/integration/blogs_spec.rb describe 'Blogs API' do @@ -444,16 +588,26 @@ describe 'Blogs API' do end ``` -### Enable generation examples from responses ### + +### Enable auto generation examples from responses ### + To enable examples generation from responses add callback above run_test! like: -```ruby + +``` after do |example| example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } end ``` + You need to disable --dry-run option for Rspec > 3 + + Add to config/environments/test.rb: ```ruby RSpec.configure do |config| @@ -461,7 +615,7 @@ RSpec.configure do |config| 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: ```ruby @@ -489,6 +643,136 @@ describe 'Blogs API', document: false do end ``` +##### rswag helper methods ##### + ### 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_: @@ -509,7 +793,7 @@ GET http:///your-custom-prefix/v1/swagger.json ### 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 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. + + +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. + diff --git a/rswag-api/lib/generators/rswag/api/install/USAGE b/rswag-api/lib/generators/rswag/api/install/USAGE index 87b8bc5..b5b56cc 100644 --- a/rswag-api/lib/generators/rswag/api/install/USAGE +++ b/rswag-api/lib/generators/rswag/api/install/USAGE @@ -5,4 +5,4 @@ Example: rails generate rswag:api:install This will create: - config/initializers/rswag-api.rb + config/initializers/rswag_api.rb diff --git a/rswag-api/lib/generators/rswag/api/install/install_generator.rb b/rswag-api/lib/generators/rswag/api/install/install_generator.rb index 744f2b1..f76d064 100644 --- a/rswag-api/lib/generators/rswag/api/install/install_generator.rb +++ b/rswag-api/lib/generators/rswag/api/install/install_generator.rb @@ -7,7 +7,7 @@ module Rswag source_root File.expand_path('../templates', __FILE__) def add_initializer - template('rswag-api.rb', 'config/initializers/rswag-api.rb') + template('rswag_api.rb', 'config/initializers/rswag_api.rb') end def add_routes diff --git a/rswag-api/lib/generators/rswag/api/install/templates/rswag-api.rb b/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb similarity index 100% rename from rswag-api/lib/generators/rswag/api/install/templates/rswag-api.rb rename to rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb diff --git a/rswag-api/rswag-api.gemspec b/rswag-api/rswag-api.gemspec index 97e7a6a..e19a24a 100644 --- a/rswag-api/rswag-api.gemspec +++ b/rswag-api/rswag-api.gemspec @@ -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: Gem::Specification.new do |s| - s.name = "rswag-api" + s.name = 'rswag-api' s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] - s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" - 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.license = "MIT" + s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian'] + s.email = ['domaindrivendev@gmail.com'] + s.homepage = 'https://github.com/rswag/rswag' + 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.license = 'MIT' - s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile"] + s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile'] s.add_dependency 'railties', '>= 3.1', '< 7.0' end diff --git a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb index fe2294d..6e983fc 100644 --- a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb +++ b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb @@ -1,6 +1,7 @@ require 'generator_spec' require 'generators/rswag/api/install/install_generator' + module Rswag module Api @@ -17,7 +18,7 @@ module Rswag end it 'installs the Rails initializer' do - assert_file('config/initializers/rswag-api.rb') + assert_file('config/initializers/rswag_api.rb') end # Don't know how to test this @@ -25,3 +26,4 @@ module Rswag end end end + diff --git a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json b/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json index 11b296d..6f89dfa 100644 --- a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json +++ b/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json @@ -1,5 +1,5 @@ { - "swagger": "2.0", + "openapi": "3.0.1", "info": { "title": "API V1", "version": "v1" diff --git a/rswag-api/spec/rswag/api/middleware_spec.rb b/rswag-api/spec/rswag/api/middleware_spec.rb index 049b3fd..6f90724 100644 --- a/rswag-api/spec/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/rswag/api/middleware_spec.rb @@ -97,7 +97,7 @@ module Rswag it 'locates files at the provided swagger_root' do expect(response.length).to eql(3) 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 diff --git a/rswag-specs/Rakefile b/rswag-specs/Rakefile index 2cbae8a..2d528b1 100644 --- a/rswag-specs/Rakefile +++ b/rswag-specs/Rakefile @@ -20,8 +20,4 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end - - - Bundler::GemHelper.install_tasks - diff --git a/rswag-specs/lib/generators/rspec/swagger_generator.rb b/rswag-specs/lib/generators/rspec/swagger_generator.rb index ddb862c..7299176 100644 --- a/rswag-specs/lib/generators/rspec/swagger_generator.rb +++ b/rswag-specs/lib/generators/rspec/swagger_generator.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'rswag/route_parser' require 'rails/generators' module Rspec class SwaggerGenerator < ::Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def setup @routes = Rswag::RouteParser.new(controller_path).routes diff --git a/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb b/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb index 050c57b..92f9dd8 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + require 'rails/generators' module Rswag module Specs - class InstallGenerator < Rails::Generators::Base - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def add_swagger_helper template('swagger_helper.rb', 'spec/swagger_helper.rb') diff --git a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb index 327b2c8..8f71560 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.configure do |config| @@ -19,7 +21,17 @@ RSpec.configure do |config| title: 'API V1', version: 'v1' }, - paths: {} + paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ] } } diff --git a/rswag-specs/lib/rswag/route_parser.rb b/rswag-specs/lib/rswag/route_parser.rb index 523b36b..03470ee 100644 --- a/rswag-specs/lib/rswag/route_parser.rb +++ b/rswag-specs/lib/rswag/route_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rswag class RouteParser attr_reader :controller @@ -9,7 +11,7 @@ module Rswag def routes ::Rails.application.routes.routes.select do |route| route.defaults[:controller] == controller - end.reduce({}) do |tree, route| + end.each_with_object({}) do |tree, route| path = path_from(route) verb = verb_from(route) tree[path] ||= { params: params_from(route), actions: {} } @@ -28,7 +30,7 @@ module Rswag def verb_from(route) verb = route.verb - if verb.kind_of? String + if verb.is_a? String verb.downcase else verb.source.gsub(/[$^]/, '').downcase diff --git a/rswag-specs/lib/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb index a3f0c16..1db62a5 100644 --- a/rswag-specs/lib/rswag/specs.rb +++ b/rswag-specs/lib/rswag/specs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rspec/core' require 'rswag/specs/example_group_helpers' require 'rswag/specs/example_helpers' @@ -6,7 +8,6 @@ require 'rswag/specs/railtie' if defined?(Rails::Railtie) module Rswag module Specs - # Extend RSpec with a swagger-based DSL ::RSpec.configure do |c| c.add_setting :swagger_root diff --git a/rswag-specs/lib/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb index 4c6ee68..b9dca6b 100644 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ b/rswag-specs/lib/rswag/specs/configuration.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true + module Rswag module Specs - class Configuration - def initialize(rspec_config) @rspec_config = rspec_config end @@ -12,6 +12,7 @@ module Rswag if @rspec_config.swagger_root.nil? raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' end + @rspec_config.swagger_root end end @@ -21,6 +22,7 @@ module Rswag if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' end + @rspec_config.swagger_docs end end @@ -35,6 +37,7 @@ module Rswag @swagger_format ||= begin @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty? raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format) + @rspec_config.swagger_format end end @@ -42,8 +45,14 @@ module Rswag def get_swagger_doc(name) return swagger_docs.values.first if name.nil? raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] + swagger_docs[name] end + + def get_swagger_doc_version(name) + doc = get_swagger_doc(name) + doc[:openapi] || doc[:swagger] + end end class ConfigurationError < StandardError; end diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb index 4939abb..591a7e9 100644 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + module Rswag module Specs module ExampleGroupHelpers - - def path(template, metadata={}, &block) + def path(template, metadata = {}, &block) metadata[:path_item] = { template: template } describe(template, metadata, &block) 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| api_metadata = { operation: { verb: verb, summary: summary } } describe(verb, api_metadata, &block) end end - [ :operationId, :deprecated, :security ].each do |attr_name| + [:operationId, :deprecated, :security].each do |attr_name| define_method(attr_name) do |value| metadata[:operation][attr_name] = value end @@ -23,13 +24,14 @@ module Rswag # NOTE: 'description' requires special treatment because ExampleGroup already # defines a method with that name. Provide an override that supports the existing # functionality while also setting the appropriate metadata if applicable - def description(value=nil) + def description(value = nil) return super() if value.nil? + metadata[:operation][:description] = value end # 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| metadata[:operation][attr_name] = value end @@ -40,7 +42,7 @@ module Rswag attributes[:required] = true end - if metadata.has_key?(:operation) + if metadata.key?(:operation) metadata[:operation][:parameters] ||= [] metadata[:operation][:parameters] << attributes else @@ -49,7 +51,7 @@ module Rswag end end - def response(code, description, metadata={}, &block) + def response(code, description, metadata = {}, &block) metadata[:response] = { code: code, description: description } context(description, metadata, &block) end @@ -60,6 +62,7 @@ module Rswag def header(name, attributes) metadata[:response][:headers] ||= {} + metadata[:response][:headers][name] = attributes end @@ -68,6 +71,7 @@ module Rswag # rspec-core ExampleGroup def examples(example = nil) return super() if example.nil? + metadata[:response][:examples] = example end diff --git a/rswag-specs/lib/rswag/specs/example_helpers.rb b/rswag-specs/lib/rswag/specs/example_helpers.rb index d8ff128..c447742 100644 --- a/rswag-specs/lib/rswag/specs/example_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_helpers.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + require 'rswag/specs/request_factory' require 'rswag/specs/response_validator' module Rswag module Specs module ExampleHelpers - def submit_request(metadata) request = RequestFactory.new.build_request(metadata, self) @@ -19,10 +20,8 @@ module Rswag send( request[:verb], request[:path], - { - params: request[:payload], - headers: request[:headers] - } + params: request[:payload], + headers: request[:headers] ) end end diff --git a/rswag-specs/lib/rswag/specs/extended_schema.rb b/rswag-specs/lib/rswag/specs/extended_schema.rb index 29888f8..3af8efc 100644 --- a/rswag-specs/lib/rswag/specs/extended_schema.rb +++ b/rswag-specs/lib/rswag/specs/extended_schema.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'json-schema' module Rswag module Specs class ExtendedSchema < JSON::Schema::Draft4 - def initialize super @attributes['type'] = ExtendedTypeAttribute @@ -13,9 +14,9 @@ module Rswag end 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 end end diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb index 617403e..0644f3c 100644 --- a/rswag-specs/lib/rswag/specs/railtie.rb +++ b/rswag-specs/lib/rswag/specs/railtie.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + module Rswag module Specs class Railtie < ::Rails::Railtie - rake_tasks do - load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__) + load File.expand_path('../../tasks/rswag-specs_tasks.rake', __dir__) end generators do diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 14b1edc..e5dff39 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/conversions' require 'json' +require 'byebug' module Rswag module Specs class RequestFactory - def initialize(config = ::Rswag::Specs.config) @config = config end @@ -38,8 +40,8 @@ module Rswag def derive_security_params(metadata, swagger_doc) requirements = metadata[:operation][:security] || swagger_doc[:security] || [] - scheme_names = requirements.flat_map { |r| r.keys } - schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values + scheme_names = requirements.flat_map(&:keys) + schemes = security_version(scheme_names, swagger_doc) schemes.map do |scheme| param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } @@ -47,13 +49,55 @@ module Rswag 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) - key = ref.sub('#/parameters/', '').to_sym - definitions = swagger_doc[:parameters] + key = key_version(ref, swagger_doc) + definitions = definition_version(swagger_doc) raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] + definitions[key] 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) request[:verb] = metadata[:operation][:verb] end @@ -61,21 +105,21 @@ module Rswag def add_path(request, metadata, swagger_doc, parameters, example) 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| - template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) + path_template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) end parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| - template.concat(i == 0 ? '?' : '&') - template.concat(build_query_string_part(p, example.send(p[:name]))) + path_template.concat(i.zero? ? '?' : '&') + path_template.concat(build_query_string_part(p, example.send(p[:name]))) end end end def build_query_string_part(param, value) 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] when :ssv @@ -94,43 +138,43 @@ module Rswag def add_headers(request, metadata, swagger_doc, parameters, example) tuples = parameters .select { |p| p[:in] == :header } - .map { |p| [ p[:name], example.send(p[:name]).to_s ] } + .map { |p| [p[:name], example.send(p[:name]).to_s] } # Accept header produces = metadata[:operation][:produces] || swagger_doc[:produces] if produces - accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first - tuples << [ 'Accept', accept ] + accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first + tuples << ['Accept', accept] end # Content-Type header consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] if consumes content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first - tuples << [ 'Content-Type', content_type ] + tuples << ['Content-Type', content_type] end # Rails test infrastructure requires rackified headers rackified_tuples = tuples.map do |pair| [ case pair[0] - when 'Accept' then 'HTTP_ACCEPT' - when 'Content-Type' then 'CONTENT_TYPE' - when 'Authorization' then 'HTTP_AUTHORIZATION' - else pair[0] + when 'Accept' then 'HTTP_ACCEPT' + when 'Content-Type' then 'CONTENT_TYPE' + when 'Authorization' then 'HTTP_AUTHORIZATION' + else pair[0] end, pair[1] ] end - request[:headers] = Hash[ rackified_tuples ] + request[:headers] = Hash[rackified_tuples] end def add_payload(request, parameters, example) content_type = request[:headers]['CONTENT_TYPE'] return if content_type.nil? - if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type) + if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type) request[:payload] = build_form_payload(parameters, example) else request[:payload] = build_json_payload(parameters, example) @@ -144,14 +188,18 @@ module Rswag # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test tuples = parameters .select { |p| p[:in] == :formData } - .map { |p| [ p[:name], example.send(p[:name]) ] } - Hash[ tuples ] + .map { |p| [p[:name], example.send(p[:name])] } + Hash[tuples] end def build_json_payload(parameters, example) body_param = parameters.select { |p| p[:in] == :body }.first body_param ? example.send(body_param[:name]).to_json : nil end + + def doc_version(doc) + doc[:openapi] || doc[:swagger] || '3' + end end end end diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb index c3e363f..2c54874 100644 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ b/rswag-specs/lib/rswag/specs/response_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/hash/slice' require 'json-schema' require 'json' @@ -6,7 +8,6 @@ require 'rswag/specs/extended_schema' module Rswag module Specs class ResponseValidator - def initialize(config = ::Rswag::Specs.config) @config = config end @@ -25,8 +26,8 @@ module Rswag expected = metadata[:response][:code].to_s if response.code != expected raise UnexpectedResponse, - "Expected response code '#{response.code}' to match '#{expected}'\n" \ - "Response body: #{response.body}" + "Expected response code '#{response.code}' to match '#{expected}'\n" \ + "Response body: #{response.body}" end end @@ -41,12 +42,30 @@ module Rswag response_schema = metadata[:response][:schema] 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 .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') - .merge(swagger_doc.slice(:definitions)) + .merge(schemas) + errors = JSON::Validator.fully_validate(validation_schema, body) raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? 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 class UnexpectedResponse < StandardError; end diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index 568f7e2..0b15f12 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/hash/deep_merge' require 'rspec/core/formatters/base_text_formatter' require 'swagger_helper' @@ -20,26 +22,58 @@ module Rswag def example_group_finished(notification) # NOTE: rspec 2.x support - if RSPEC_VERSION > 2 - metadata = notification.group.metadata + metadata = if RSPEC_VERSION > 2 + notification.group.metadata else - metadata = notification.metadata + notification.metadata end # !metadata[:document] won't work, since nil means we should generate # docs. return if metadata[:document] == false - return unless metadata.has_key?(:response) + return unless metadata.key?(:response) 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)) end - def stop(notification=nil) + def stop(_notification = nil) @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) 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.write(pretty_generate(doc)) @@ -62,25 +96,108 @@ module Rswag def yaml_prepare(doc) json_doc = JSON.pretty_generate(doc) - clean_doc = JSON.parse(json_doc) + JSON.parse(json_doc) end def metadata_to_swagger(metadata) 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] operation = metadata[:operation] - .reject { |k,v| k == :verb } + .reject { |k, _v| k == :verb } .merge(responses: { response_code => response }) path_template = metadata[:path_item][:template] path_item = metadata[:path_item] - .reject { |k,v| k == :template } + .reject { |k, _v| k == :template } .merge(verb => operation) { paths: { path_template => path_item } } 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 diff --git a/rswag-specs/lib/tasks/rswag-specs_tasks.rake b/rswag-specs/lib/tasks/rswag-specs_tasks.rake index 54412a2..41f264c 100644 --- a/rswag-specs/lib/tasks/rswag-specs_tasks.rake +++ b/rswag-specs/lib/tasks/rswag-specs_tasks.rake @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'rspec/core/rake_task' namespace :rswag do namespace :specs do - desc 'Generate Swagger JSON files from integration specs' RSpec::Core::RakeTask.new('swaggerize') do |t| t.pattern = ENV.fetch( @@ -12,12 +13,12 @@ namespace :rswag do # NOTE: rspec 2.x support 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 - t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ] + t.rspec_opts = ['--format Rswag::Specs::SwaggerFormatter', '--order defined'] end end end end -task :rswag => ['rswag:specs:swaggerize'] +task rswag: ['rswag:specs:swaggerize'] diff --git a/rswag-specs/rswag-specs.gemspec b/rswag-specs/rswag-specs.gemspec index ff46439..0f7dbc6 100644 --- a/rswag-specs/rswag-specs.gemspec +++ b/rswag-specs/rswag-specs.gemspec @@ -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: Gem::Specification.new do |s| - s.name = "rswag-specs" + s.name = 'rswag-specs' s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] - s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" - 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.license = "MIT" + s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian'] + s.email = ['domaindrivendev@gmail.com'] + s.homepage = 'https://github.com/rswag/rswag' + 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.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 'railties', '>= 3.1', '< 7.0' diff --git a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb index f11ea65..1349230 100644 --- a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb +++ b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'generator_spec' require 'generators/rspec/swagger_generator' require 'tmpdir' @@ -9,12 +11,11 @@ module Rspec before(:all) do prepare_destination - fixtures_dir = File.expand_path('../fixtures', __FILE__) + fixtures_dir = File.expand_path('fixtures', __dir__) FileUtils.cp_r("#{fixtures_dir}/spec", destination_root) end after(:all) do - end it 'installs the swagger_helper for rspec' do @@ -31,11 +32,11 @@ module Rspec def fake_routes { - "/posts/{post_id}/comments/{id}" => { - :params => ["post_id", "id"], - :actions => { - "get" => { :summary=>"show comment" }, - "patch" => { :summary=>"update_comments comment" } + '/posts/{post_id}/comments/{id}' => { + params: ['post_id', 'id'], + actions: { + 'get' => { summary: 'show comment' }, + 'patch' => { summary: 'update_comments comment' } } } } diff --git a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb index 809e4f6..ffd7ddc 100644 --- a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb @@ -1,16 +1,17 @@ +# frozen_string_literal: true + require 'generator_spec' require 'generators/rswag/specs/install/install_generator' module Rswag module Specs - RSpec.describe InstallGenerator do include GeneratorSpec::TestCase - destination File.expand_path('../tmp', __FILE__) + destination File.expand_path('tmp', __dir__) before(:all) do prepare_destination - fixtures_dir = File.expand_path('../fixtures', __FILE__) + fixtures_dir = File.expand_path('fixtures', __dir__) FileUtils.cp_r("#{fixtures_dir}/spec", destination_root) run_generator diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index e3aacdc..98345d4 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'rswag/specs/configuration' module Rswag module Specs - RSpec.describe Configuration do subject { described_class.new(rspec_config) } diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb index ed667ca..2922523 100644 --- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'rswag/specs/example_group_helpers' module Rswag module Specs - RSpec.describe ExampleGroupHelpers do subject { double('example_group') } @@ -34,31 +35,6 @@ module Rswag 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 before do subject.tags('Blogs', 'Admin') @@ -74,12 +50,12 @@ module Rswag it "adds to the 'operation' metadata" do expect(api_metadata[:operation]).to match( - tags: [ 'Blogs', 'Admin' ], + tags: ['Blogs', 'Admin'], description: 'Some description', operationId: 'createBlog', - consumes: [ 'application/json', 'application/xml' ], - produces: [ 'application/json', 'application/xml' ], - schemes: [ 'http', 'https' ], + consumes: ['application/json', 'application/xml'], + produces: ['application/json', 'application/xml'], + schemes: ['http', 'https'], deprecated: true, security: { api_key: [] } ) @@ -87,14 +63,13 @@ module Rswag end describe '#parameter(attributes)' do - context "when called at the 'path' level" do before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet it "adds to the 'path_item parameters' metadata" do expect(api_metadata[:path_item][:parameters]).to match( - [ name: :blog, in: :body, schema: { type: 'object' } ] + [name: :blog, in: :body, schema: { type: 'object' }] ) end end @@ -105,7 +80,7 @@ module Rswag it "adds to the 'operation parameters' metadata" do expect(api_metadata[:operation][:parameters]).to match( - [ name: :blog, in: :body, schema: { type: 'object' } ] + [name: :blog, in: :body, schema: { type: 'object' }] ) end end @@ -116,7 +91,7 @@ module Rswag it "automatically sets the 'required' flag" do expect(api_metadata[:operation][:parameters]).to match( - [ name: :id, in: :path, required: true ] + [name: :id, in: :path, required: true] ) end end @@ -126,7 +101,7 @@ module Rswag let(:api_metadata) { { operation: {} } } 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 diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index 283d4fd..0f3e1ba 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'rswag/specs/example_helpers' module Rswag module Specs - RSpec.describe ExampleHelpers do subject { double('example') } @@ -15,6 +16,7 @@ module Rswag let(:config) { double('config') } let(:swagger_doc) do { + swagger: '2.0', securityDefinitions: { api_key: { type: :apiKey, @@ -24,13 +26,14 @@ module Rswag } } end + let(:metadata) do { path_item: { template: '/blogs/{blog_id}/comments/{id}' }, operation: { verb: :put, summary: 'Updates a blog', - consumes: [ 'application/json' ], + consumes: ['application/json'], parameters: [ { name: :blog_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 expect(subject).to have_received(:put).with( '/blogs/1/comments/2?q1=foo&api_key=fookey', - "{\"text\":\"Some comment\"}", + '{"text":"Some comment"}', { 'CONTENT_TYPE' => 'application/json' } ) end diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index 0a70f19..aff5fb4 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'rswag/specs/request_factory' module Rswag module Specs - RSpec.describe RequestFactory do subject { RequestFactory.new(config) } @@ -10,7 +11,7 @@ module Rswag allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) end let(:config) { double('config') } - let(:swagger_doc) { {} } + let(:swagger_doc) { { swagger: '2.0' } } let(:example) { double('example') } let(:metadata) do { @@ -53,7 +54,7 @@ module Rswag allow(example).to receive(:q2).and_return('bar') 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') end end @@ -63,40 +64,40 @@ module Rswag metadata[:operation][:parameters] = [ { 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 context 'collectionFormat = csv' do 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') end end context 'collectionFormat = ssv' do 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') end end context 'collectionFormat = tsv' do 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') end end context 'collectionFormat = pipes' do 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') end end context 'collectionFormat = multi' do 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') end end @@ -104,7 +105,7 @@ module Rswag context "'header' parameters" 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') end @@ -127,9 +128,9 @@ module Rswag end end - context "consumes content" do + context 'consumes content' do before do - metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ] + metadata[:operation][:consumes] = ['application/json', 'application/xml'] end context "no 'Content-Type' provided" do @@ -150,18 +151,18 @@ module Rswag context 'JSON payload' 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') end 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 context 'form payload' do before do - metadata[:operation][:consumes] = [ 'multipart/form-data' ] + metadata[:operation][:consumes] = ['multipart/form-data'] metadata[:operation][:parameters] = [ { name: 'f1', in: :formData, type: :string }, { name: 'f2', in: :formData, type: :string } @@ -181,7 +182,7 @@ module Rswag context 'produces content' do before do - metadata[:operation][:produces] = [ 'application/json', 'application/xml' ] + metadata[:operation][:produces] = ['application/json', 'application/xml'] end context "no 'Accept' value provided" do @@ -192,7 +193,7 @@ module Rswag context "explicit 'Accept' value provided" do before do - allow(example).to receive(:'Accept').and_return('application/xml') + allow(example).to receive(:Accept).and_return('application/xml') end it "sets 'HTTP_ACCEPT' header to example value" do @@ -202,21 +203,53 @@ module Rswag end context 'basic auth' do - before do - swagger_doc[:securityDefinitions] = { basic: { type: :basic } } - metadata[:operation][:security] = [ basic: [] ] - allow(example).to receive(:Authorization).and_return('Basic foobar') + context 'swagger 2.0' do + before do + swagger_doc[:securityDefinitions] = { basic: { type: :basic } } + metadata[:operation][:security] = [basic: []] + allow(example).to receive(:Authorization).and_return('Basic foobar') + end + + it "sets 'HTTP_AUTHORIZATION' header to example value" do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') + end end - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') + 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 before do 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') end @@ -256,8 +289,8 @@ module Rswag context 'oauth2' do before do - swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: [ 'read:blogs' ] } } - metadata[:operation][:security] = [ oauth2: [ 'read:blogs' ] ] + swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: ['read:blogs'] } } + metadata[:operation][:security] = [oauth2: ['read:blogs']] allow(example).to receive(:Authorization).and_return('Bearer foobar') end @@ -272,39 +305,72 @@ module Rswag basic: { type: :basic }, 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(:api_key).and_return('foobar') 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[:path]).to eq('/blogs?api_key=foobar') end end - context "path-level parameters" do + context 'path-level parameters' do before do - metadata[:operation][:parameters] = [ { name: 'q1', in: :query, type: :string } ] - metadata[:path_item][:parameters] = [ { name: 'q2', in: :query, type: :string } ] + metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }] + metadata[:path_item][:parameters] = [{ name: 'q2', in: :query, type: :string }] allow(example).to receive(:q1).and_return('foo') allow(example).to receive(:q2).and_return('bar') end - it "populates operation and path level parameters " do + it 'populates operation and path level parameters' do expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end context 'referenced parameters' do - before do - swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } - metadata[:operation][:parameters] = [ { '$ref' => '#/parameters/q1' } ] - allow(example).to receive(:q1).and_return('foo') + context 'swagger 2.0' do + before do + swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } + metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] + allow(example).to receive(:q1).and_return('foo') + end + + it 'uses the referenced metadata to build the request' do + expect(request[:path]).to eq('/blogs?q1=foo') + end end - it 'uses the referenced metadata to build the request' do - expect(request[:path]).to eq('/blogs?q1=foo') + 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 @@ -316,18 +382,18 @@ module Rswag end end - context "global consumes" do - before { swagger_doc[:consumes] = [ 'application/xml' ] } + context 'global consumes' do + before { swagger_doc[:consumes] = ['application/xml'] } it "defaults 'CONTENT_TYPE' to global value(s)" do expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') end end - context "global security requirements" do + context 'global security requirements' do before do 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') end diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index 9f52b5b..e4c8e9c 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + require 'rswag/specs/response_validator' module Rswag module Specs - RSpec.describe ResponseValidator do subject { ResponseValidator.new(config) } before do allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + allow(config).to receive(:get_swagger_doc_version).and_return('2.0') end let(:config) { double('config') } let(:swagger_doc) { {} } @@ -20,7 +22,7 @@ module Rswag schema: { type: :object, properties: { text: { type: :string } }, - required: [ 'text' ] + required: ['text'] } } } @@ -32,43 +34,89 @@ module Rswag OpenStruct.new( code: '200', headers: { 'X-Rate-Limit-Limit' => '10' }, - body: "{\"text\":\"Some comment\"}" + body: '{"text":"Some comment"}' ) end - context "response matches metadata" do + context 'response matches metadata' do it { expect { call }.to_not raise_error } end - context "response code differs from metadata" do + context 'response code differs from metadata' do before { response.code = '400' } - it { expect { call }.to raise_error /Expected response code/ } + it { expect { call }.to raise_error(/Expected response code/) } end - context "response headers differ from metadata" do + context 'response headers differ from metadata' do before { response.headers = {} } - it { expect { call }.to raise_error /Expected response header/ } + it { expect { call }.to raise_error(/Expected response header/) } end - context "response body differs from metadata" do - before { response.body = "{\"foo\":\"Some comment\"}" } - it { expect { call }.to raise_error /Expected response body/ } + context 'response body differs from metadata' do + before { response.body = '{"foo":"Some comment"}' } + it { expect { call }.to raise_error(/Expected response body/) } end context 'referenced schemas' do - before do - swagger_doc[:definitions] = { - 'blog' => { - type: :object, - properties: { foo: { type: :string } }, - required: [ 'foo' ] + context 'swagger 2.0' do + before do + swagger_doc[:definitions] = { + 'blog' => { + type: :object, + properties: { foo: { type: :string } }, + required: ['foo'] + } } - } - metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } + metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } + end + + it 'uses the referenced schema to validate the response body' do + expect { call }.to raise_error(/Expected response body/) + end end - it 'uses the referenced schema to validate the response body' do - expect { call }.to raise_error /Expected response body/ + 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 diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index b0a53b5..1a9fdf0 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'rswag/specs/swagger_formatter' require 'ostruct' module Rswag module Specs - RSpec.describe SwaggerFormatter do subject { described_class.new(output, config) } @@ -13,44 +14,153 @@ module Rswag end let(:config) { double('config') } 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 before do allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) subject.example_group_finished(notification) end - let(:swagger_doc) { {} } let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } let(:api_metadata) do { - path_item: { template: '/blogs' }, - operation: { verb: :post, summary: 'Creates a blog' }, - response: { code: '201', description: 'blog created' }, + path_item: { template: '/blogs', parameters: [{ type: :string }] }, + operation: { verb: :post, summary: 'Creates a blog', parameters: [{ type: :string }] }, + response: { code: '201', description: 'blog created', headers: { type: :string }, schema: { '$ref' => '#/definitions/blog' } }, document: document } end context 'with the document tag set to false' do + let(:swagger_doc) { { swagger: '2.0' } } let(:document) { false } it 'does not update the swagger doc' do - expect(swagger_doc).to be_empty + expect(swagger_doc).to match({ swagger: '2.0' }) end end 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. let(:document) { nil } it 'converts to swagger and merges into the corresponding swagger doc' do expect(swagger_doc).to match( + swagger: '2.0', paths: { '/blogs' => { + parameters: [{ type: :string }], post: { + parameters: [{ type: :string }], summary: 'Creates a blog', 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 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( - 'v1/swagger.json' => { info: { version: 'v1' } }, - 'v2/swagger.json' => { info: { version: 'v2' } } + 'v1/swagger.json' => doc_1, + 'v2/swagger.json' => doc_2 ) allow(config).to receive(:swagger_format).and_return(swagger_format) subject.stop(notification) end + let(:doc_1) { { info: { version: 'v1' } } } + let(:doc_2) { { info: { version: 'v2' } } } + let(:swagger_format) { :json } + let(:notification) { double('notification') } context 'with default format' do - let(:swagger_format) { :json } - it 'writes the swagger_doc(s) to file' do expect(File).to exist("#{swagger_root}/v1/swagger.json") expect(File).to exist("#{swagger_root}/v2/swagger.json") @@ -93,8 +205,77 @@ module Rswag 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 - FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) + FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) end end end diff --git a/rswag-specs/spec/spec_helper.rb b/rswag-specs/spec/spec_helper.rb index 63504e1..eba5ebe 100644 --- a/rswag-specs/spec/spec_helper.rb +++ b/rswag-specs/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module VERSION MAJOR = 3 diff --git a/rswag-specs/spec/swagger_helper.rb b/rswag-specs/spec/swagger_helper.rb index 330a7a4..434e237 100644 --- a/rswag-specs/spec/swagger_helper.rb +++ b/rswag-specs/spec/swagger_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # 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 diff --git a/rswag-ui/lib/rswag/ui/middleware.rb b/rswag-ui/lib/rswag/ui/middleware.rb index 3bad997..038379e 100644 --- a/rswag-ui/lib/rswag/ui/middleware.rb +++ b/rswag-ui/lib/rswag/ui/middleware.rb @@ -8,7 +8,7 @@ module Rswag end def call(env) - if base_path?(env) + if base_path?(env) redirect_uri = env['SCRIPT_NAME'].chomp('/') + '/index.html' return [ 301, { 'Location' => redirect_uri }, [ ] ] end diff --git a/rswag-ui/rswag-ui.gemspec b/rswag-ui/rswag-ui.gemspec index d1ea0b1..97cf681 100644 --- a/rswag-ui/rswag-ui.gemspec +++ b/rswag-ui/rswag-ui.gemspec @@ -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: Gem::Specification.new do |s| - s.name = "rswag-ui" + s.name = 'rswag-ui' s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] - s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" - 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.license = "MIT" + s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian'] + s.email = ['domaindrivendev@gmail.com'] + s.homepage = 'https://github.com/rswag/rswag' + s.summary = 'A Rails Engine that includes swagger-ui and powers it from configured Swagger endpoints' + s.description = 'Expose beautiful API documentation, powered by Swagger JSON endpoints, including a UI to explore and test operations' + 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 'railties', '>= 3.1', '< 7.0' diff --git a/rswag/rswag.gemspec b/rswag/rswag.gemspec index a2ed250..edf341f 100644 --- a/rswag/rswag.gemspec +++ b/rswag/rswag.gemspec @@ -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: Gem::Specification.new do |s| - s.name = "rswag" + s.name = 'rswag' s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] - s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" - s.summary = "Swagger tooling for Rails API's" - s.description = "Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests" - s.license = "MIT" + s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian'] + s.email = ['domaindrivendev@gmail.com'] + s.homepage = 'https://github.com/rswag/rswag' + 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.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-api', ENV['TRAVIS_TAG'] || '0.0.0' diff --git a/rswag/spec/generators/rswag/specs/install_generator_spec.rb b/rswag/spec/generators/rswag/specs/install_generator_spec.rb index 840ab43..38cf4f7 100644 --- a/rswag/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag/spec/generators/rswag/specs/install_generator_spec.rb @@ -22,7 +22,7 @@ module Rswag end it 'installs initializer for rswag-api' do - assert_file('config/rswag-api.rb') + assert_file('config/rswag_api.rb') end it 'installs initializer for rswag-ui' do diff --git a/test-app/.byebug_history b/test-app/.byebug_history deleted file mode 100644 index ad4ba66..0000000 --- a/test-app/.byebug_history +++ /dev/null @@ -1,4 +0,0 @@ -exit -env['PATH_INFO'] -env['SCRIPT_NAME'] -env diff --git a/test-app/Rakefile b/test-app/Rakefile index 9946cea..4057ab4 100644 --- a/test-app/Rakefile +++ b/test-app/Rakefile @@ -5,3 +5,8 @@ require File.expand_path('../config/application', __FILE__) 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 diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb index 8c83a1c..75c5ca7 100644 --- a/test-app/app/controllers/blogs_controller.rb +++ b/test-app/app/controllers/blogs_controller.rb @@ -8,6 +8,25 @@ class BlogsController < ApplicationController respond_with @blog 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 def upload @blog = Blog.find_by_id(params[:id]) diff --git a/test-app/app/models/blog.rb b/test-app/app/models/blog.rb index 9fb7070..9d248d7 100644 --- a/test-app/app/models/blog.rb +++ b/test-app/app/models/blog.rb @@ -1,11 +1,16 @@ +# frozen_string_literal: true + class Blog < ActiveRecord::Base validates :content, presence: true - def as_json(options) + alias_attribute :headline, :title + alias_attribute :text, :content + + def as_json(_options) { id: id, title: title, - content: nil, + content: content, thumbnail: thumbnail } end diff --git a/test-app/config/application.rb b/test-app/config/application.rb index 09be55a..dfafc75 100644 --- a/test-app/config/application.rb +++ b/test-app/config/application.rb @@ -11,6 +11,7 @@ Bundler.require(*Rails.groups) module TestApp class Application < Rails::Application + config.load_defaults 5.2 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/test-app/config/initializers/secret_token.rb b/test-app/config/initializers/secret_token.rb index 843ff63..44c139c 100644 --- a/test-app/config/initializers/secret_token.rb +++ b/test-app/config/initializers/secret_token.rb @@ -1,10 +1,12 @@ # Be sure to restart your server when you modify this file. -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# 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 -TestApp::Application.config.secret_key_base = 'f6a820cc8aa76094583cd68ef46a735e25e3278648086355f8bd24721f036959c728c06a28dcecfe695f17ae2db44dfa1424f22b81377f2a1496d4e19f6f7faa' +if Rails.version.first.to_i < 5 + # Your secret key for verifying the integrity of signed cookies. + # If you change this key, all old signed cookies will become invalid! + # Make sure the secret is at least 30 characters and all random, + # no regular words or you'll be exposed to dictionary attacks. + TestApp::Application.config.secret_token = '60f36cd33756d73f362053f1d45256ae50d75440b634ae73b070a6e35a2df38692f59e28e5ecbd1f9f2e850255f6d29a468bc59ac4484c2b7f0548ddbfc1b870' +else + # See http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#config-secrets-yml + TestApp::Application.config.secret_key_base = 'f6a820cc8aa76094583cd68ef46a735e25e3278648086355f8bd24721f036959c728c06a28dcecfe695f17ae2db44dfa1424f22b81377f2a1496d4e19f6f7faa' +end diff --git a/test-app/config/routes.rb b/test-app/config/routes.rb index be02215..038f728 100644 --- a/test-app/config/routes.rb +++ b/test-app/config/routes.rb @@ -1,4 +1,7 @@ TestApp::Application.routes.draw do + + post '/blogs/flexible', to: 'blogs#flexible_create' + post '/blogs/alternate', to: 'blogs#alternate_create' resources :blogs put '/blogs/:id/upload', to: 'blogs#upload' @@ -6,6 +9,6 @@ TestApp::Application.routes.draw do post 'auth-tests/api-key', to: 'auth_tests#api_key' post 'auth-tests/basic-and-api-key', to: 'auth_tests#basic_and_api_key' - mount Rswag::Api::Engine => 'api-docs' - mount Rswag::Ui::Engine => 'api-docs' + mount Rswag::Api::Engine => 'api-docs' + mount Rswag::Ui::Engine => 'api-docs' end diff --git a/test-app/db/schema.rb b/test-app/db/schema.rb index e01f8f3..440d919 100644 --- a/test-app/db/schema.rb +++ b/test-app/db/schema.rb @@ -2,11 +2,11 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# This file is the source Rails uses to define your schema when running `rails -# db:schema:load`. When creating a new database, `rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. diff --git a/test-app/spec/integration/auth_tests_spec.rb b/test-app/spec/integration/auth_tests_spec.rb index 573219e..58e4180 100644 --- a/test-app/spec/integration/auth_tests_spec.rb +++ b/test-app/spec/integration/auth_tests_spec.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + require 'swagger_helper' RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do - path '/auth-tests/basic' do post 'Authenticates with basic auth' do tags 'Auth Tests' operationId 'testBasicAuth' - security [ basic_auth: [] ] + security [basic_auth: []] response '204', 'Valid credentials' do 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 tags 'Auth Tests' operationId 'testApiKey' - security [ api_key: [] ] + security [api_key: []] response '204', 'Valid credentials' do 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 tags 'Auth Tests' operationId 'testBasicAndApiKey' - security [ { basic_auth: [], api_key: [] } ] + security [{ basic_auth: [], api_key: [] }] response '204', 'Valid credentials' do let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" } diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 28ee892..ee4a67e 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -15,6 +15,7 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do let(:blog) { { title: 'foo', content: 'bar' } } response '201', 'blog created' do + # schema '$ref' => '#/definitions/blog' run_test! end @@ -48,6 +49,30 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do 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 parameter name: :id, in: :path, type: :string @@ -68,11 +93,11 @@ RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do schema '$ref' => '#/definitions/blog' examples 'application/json' => { - id: 1, - title: 'Hello world!', - content: 'Hello world and hello universe. Thank you all very much!!!', - thumbnail: "thumbnail.png" - } + id: 1, + title: 'Hello world!', + content: 'Hello world and hello universe. Thank you all very much!!!', + thumbnail: 'thumbnail.png' + } let(:id) { blog.id } run_test! diff --git a/test-app/spec/rails_helper.rb b/test-app/spec/rails_helper.rb index 3d9bd5f..01a3ce4 100644 --- a/test-app/spec/rails_helper.rb +++ b/test-app/spec/rails_helper.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' 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 -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 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! diff --git a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb index 27fc195..dab7da2 100644 --- a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb +++ b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake' @@ -5,11 +7,11 @@ RSpec.describe 'rswag:specs:swaggerize' do let(:swagger_root) { Rails.root.to_s + '/swagger' } before do 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 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") end end diff --git a/test-app/spec/spec_helper.rb b/test-app/spec/spec_helper.rb index d20f071..3d22c43 100644 --- a/test-app/spec/spec_helper.rb +++ b/test-app/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # 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`. # The generated `.rspec` file contains `--require spec_helper` which will cause @@ -50,54 +52,4 @@ RSpec.configure do |config| config.after(:suite) do File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png") end - -# The settings below are suggested to provide a good initial experience -# 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 diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index fa1162a..3f2e151 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' 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 # to ensure that it's configured to serve Swagger from the same folder 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 # When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will # 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' config.swagger_docs = { 'v1/swagger.json' => { - swagger: '2.0', + openapi: '3.0.0', info: { title: 'API V1', version: 'v1' }, paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ], definitions: { errors_object: { type: 'object', @@ -40,9 +52,19 @@ RSpec.configure do |config| id: { type: 'integer' }, title: { type: 'string' }, 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: { diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index 8531c7a..957206c 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -1,5 +1,5 @@ { - "swagger": "2.0", + "openapi": "3.0.0", "info": { "title": "API V1", "version": "v1" @@ -21,10 +21,14 @@ ], "responses": { "204": { - "description": "Valid credentials" + "description": "Valid credentials", + "content": { + } }, "401": { - "description": "Invalid credentials" + "description": "Invalid credentials", + "content": { + } } } } @@ -45,10 +49,14 @@ ], "responses": { "204": { - "description": "Valid credentials" + "description": "Valid credentials", + "content": { + } }, "401": { - "description": "Invalid credentials" + "description": "Invalid credentials", + "content": { + } } } } @@ -72,10 +80,14 @@ ], "responses": { "204": { - "description": "Valid credentials" + "description": "Valid credentials", + "content": { + } }, "401": { - "description": "Invalid credentials" + "description": "Invalid credentials", + "content": { + } } } } @@ -88,29 +100,32 @@ ], "description": "Creates a new blog from provided data", "operationId": "createBlog", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], "parameters": [ - { - "name": "blog", - "in": "body", - "schema": { - "$ref": "#/definitions/blog" - } - } + ], "responses": { "201": { - "description": "blog created" + "description": "blog created", + "content": { + } }, "422": { "description": "invalid request", - "schema": { - "$ref": "#/definitions/errors_object" + "content": { + "application/json": { + "schema": { + "$ref": "#/definitions/errors_object" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/definitions/blog" + } } } } @@ -122,19 +137,68 @@ ], "description": "Searches blogs by keywords", "operationId": "searchBlogs", - "produces": [ - "application/json" - ], "parameters": [ { "name": "keywords", "in": "query", - "type": "string" + "schema": { + "type": "string" + } } ], "responses": { "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", "in": "path", - "type": "string", - "required": true + "required": true, + "schema": { + "type": "string" + } } ], "get": { @@ -155,9 +221,6 @@ ], "description": "Retrieves a specific blog by id", "operationId": "getBlog", - "produces": [ - "application/json" - ], "responses": { "200": { "description": "blog found", @@ -172,9 +235,6 @@ "type": "string" } }, - "schema": { - "$ref": "#/definitions/blog" - }, "examples": { "application/json": { "id": 1, @@ -182,10 +242,19 @@ "content": "Hello world and hello universe. Thank you all very much!!!", "thumbnail": "thumbnail.png" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/definitions/blog" + } + } } }, "404": { - "description": "blog not found" + "description": "blog not found", + "content": { + } } } } @@ -195,8 +264,10 @@ { "name": "id", "in": "path", - "type": "string", - "required": true + "required": true, + "schema": { + "type": "string" + } } ], "put": { @@ -206,25 +277,38 @@ ], "description": "Upload a thumbnail for specific blog by id", "operationId": "uploadThumbnailBlog", - "consumes": [ - "multipart/form-data" - ], "parameters": [ - { - "name": "file", - "in": "formData", - "type": "file", - "required": true - } + ], "responses": { "200": { - "description": "blog updated" + "description": "blog updated", + "content": { + } + } + }, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "file" + } + } } } } } }, + "servers": [ + { + "url": "https://{defaultHost}", + "variables": { + "defaultHost": { + "default": "www.example.com" + } + } + } + ], "definitions": { "errors_object": { "type": "object", @@ -257,25 +341,49 @@ "x-nullable": true }, "thumbnail": { - "type": "string" + "type": "string", + "x-nullable": true } }, "required": [ "id", - "title", - "content", - "thumbnail" + "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": { - "basic_auth": { - "type": "basic" - }, - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "query" + "components": { + "securitySchemes": { + "basic_auth": { + "type": "basic" + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "query" + } } } } \ No newline at end of file