Skip to content

Seamlessly adds a Swagger to Rails-based API's

Notifications You must be signed in to change notification settings

jdanielian/open-api-rswag

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

open-api-rswag

Build Status

OpenApi 3.0 compatible version of the fantastic rswag ruby gem. Most of the content originated from the original rswag gem. This fork was created to provide Open API 3.0 syntax for the swagger documentation. If you want swagger output < 3.0, use the original rswag gem. This fork was not created with backwards compatibility in mind - it will only output 3.0 syntax.

Currently, this is still a work in progress. It will output Open API 3.0 compatible syntax, but not every case is supported yet.

OpenApi Rswag creates Swagger tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests.

Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome swagger-ui that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which you're probably doing in some form already, to living documentation for your API consumers.

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 for more details.

Compatibility

Rswag Version Swagger (OpenAPI) Spec. swagger-ui
master 3.0 3.17.3

Getting Started

  1. Add this line to your applications Gemfile:

    gem 'open_api-rswag'

    or if you like to avoid loading rspec in other bundler groups.

    # Gemfile
    gem 'open_api-rswag-api'
    gem 'open_api-rswag-ui'
    
    group :test do
      gem 'rspec-rails'
      gem 'open_api-rswag-specs'
    end
  2. Run the install generator

    rails g rswag:install

    Or run the install generators for each package separately if you installed Rswag as separate gems, as indicated above:

    rails g rswag:api:install 
    rails g rswag:ui:install
    RAILS_ENV=test rails g rswag:specs:install
  3. Create an integration spec to describe and test your API.

    # spec/integration/blogs_spec.rb
    require 'swagger_helper'
    
    describe 'Blogs API' do
    
      path '/blogs' do
    
        post 'Creates a blog' do
          tags 'Blogs'
          consumes 'application/json'
          request_body_json schema: {
            type: :object,
            properties: {
              title: { type: :string },
              content: { type: :string }
            },
            required: [ 'title', 'content' ]
          }
    
          response '201', 'blog created' do
            let(:blog) { { title: 'foo', content: 'bar' } }
            run_test!
          end
    
          response '422', 'invalid request' do
            let(:blog) { { title: 'foo' } }
            run_test!
          end
        end
      end
    
      path '/blogs/{id}' do
    
        get 'Retrieves a blog' do
          tags 'Blogs'
          produces 'application/json', 'application/xml'
          parameter name: :id, :in => :path, :type => :string
    
          response '200', 'blog found' do
            schema type: :object,
              properties: {
                id: { type: :integer },
                title: { type: :string },
                content: { type: :string }
              },
              required: [ 'id', 'title', 'content' ]
    
            let(:id) { Blog.create(title: 'foo', content: 'bar').id }
            run_test!
          end
    
          response '404', 'blog not found' do
            let(:id) { 'invalid' }
            run_test!
          end
    
          response '406', 'unsupported accept header' do
            let(:'Accept') { 'application/foo' }
            run_test!
          end
        end
      end
    end
  4. Generate the Swagger JSON file(s)

    rake rswag:specs:swaggerize
  5. Spin up your app and check out the awesome, auto-generated docs at /api-docs!

The rspec DSL

Paths, Operations and Responses

If you've used Swagger before, then the syntax should be very familiar. To describe your API operations, start by specifying a path and then list the supported operations (i.e. HTTP verbs) for that path. Path parameters must be surrounded by curly braces ({}). Within an operation block (see "post" or "get" in the example above), most of the fields supported by the Swagger "Operation" object are available as methods on the example group. To list (and test) the various responses for an operation, create one or more response blocks. Again, you can reference the Swagger "Response" object for available fields.

Take special note of the run_test! method that's called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. For example, the "post" description in the example above specifies a "body" parameter called "blog". It also lists 2 different responses. For the success case (i.e. the 201 response), notice how "let" is used to set the blog parameter to a value that matches the provided schema. For the failure case (i.e. the 422 response), notice how it's set to a value that does not match the provided schema. When the test is executed, rswag also validates the actual response code and, where applicable, the response body against the provided JSON Schema.

If you want to do additional validation on the response, pass a block to the run_test! method:

response '201', 'blog created' do
  run_test! do |response|
    data = JSON.parse(response.body)
    expect(data['title']).to eq('foo')
  end
end

If you'd like your specs to be a little more explicit about what's going on here, you can replace the call to run_test! with equivalent "before" and "it" blocks:

response '201', 'blog created' do
  let(:blog) { { title: 'foo', content: 'bar' } }

  before do |example|
    submit_request(example.metadata)
  end

  it 'returns a valid 201 response' do |example|
    assert_response_matches_metadata(example.metadata)
  end
end

Null Values

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.

describe 'Blogs API' do
  path '/blogs' do
    post 'Creates a blog' do
      ...

      response '200', 'blog found' do
        schema type: :object,
          properties: {
            id: { type: :integer },
            title: { type: :string, nullable: true }, # preferred syntax
            content: { type: :string, 'x-nullable': true } # legacy syntax, but still works
          }
        ....
      end
    end
  end
end

Support for anyOf or AllOf schemas

Open API 3.0 now supports more flexible schema validation with the anyOf and allOf directives. open-api-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:

  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'

      request_body_json schema: {
                                  :oneOf => [{'$ref' => '#/components/schemas/blog'},
                                             {'$ref' => '#/components/schemas/flexible_blog'}]
                                },
                        examples: :flexible_blog

      let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } }

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

# spec/swagger_helper.rb
RSpec.configure do |config|
  config.swagger_root = Rails.root.to_s + '/swagger'

  config.swagger_docs = {
    'v1/swagger.json' => {
      openapi: '3.0.0',
      info: {
        title: 'API V1',
        version: 'v1',
        description: 'This is the first version of my API'
      },
    servers: [
      {
        url: 'https://{defaultHost}',
        variables: {
          defaultHost: {
              default: 'www.example.com'
          }
        }
      }
    ]
    }
  }
end

In order for rspec to still run properly when using a namespaced API for versioning (e.g. 'api/v1', 'api/v2'), the above swagger_docs will need the basePath metadata defined for each namespace.

'v1/swagger.json' => {
  openapi: '3.0.0',
  info: {
    title: 'API V1',
    version: 'v1',
    description: 'This is the first version of my API'
  },
  basePath: '/api/v1'
  servers: [
    {
      url: 'http://localhost:3001/api/{version}',
      description: 'The local development server.',
      variables: {
        version: {
          enum: [
            'v1'
          ],
          default: 'v1'
        }
      }
    }
  ]
}

Supporting multiple versions of API

By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in swagger_helper.rb. If your API has multiple versions, you should be using separate documents to describe each of them. In order to assign a file with a given version of API, you'll need to add the swagger_doc tag to each spec specifying its target document name:

# spec/integration/v2/blogs_spec.rb
describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do

  path '/blogs' do
  ...

  path '/blogs/{id}' do
  ...
end

Formatting the description literals:

Swagger supports the Markdown syntax to format strings. This can be especially handy if you were to provide a long description of a given API version or endpoint. Use this guide for reference.

NOTE: There is one difference between the official Markdown syntax and Swagger interpretation, namely tables. To create a table like this:

Column1 Collumn2
cell1 cell2

you should use the folowing syntax, making sure there are no whitespaces at the start of any of the lines:

&#13;
| Column1 | Collumn2 |&#13;
| ------- | -------- |&#13;
| cell1   | cell2    |&#13;
&#13;

Specifying/Testing API Security

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 for more info, as this underwent major changes between Swagger 2.0 and Open API 3.0

# spec/swagger_helper.rb
RSpec.configure do |config|
  config.swagger_root = Rails.root.to_s + '/swagger'

  config.swagger_docs = {
    'v1/swagger.json' => {
      ...  # 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
          }  
        }
      }
    }
  }
end

# spec/integration/blogs_spec.rb
describe 'Blogs API' do

  path '/blogs' do

    post 'Creates a blog' do
      tags 'Blogs'
      security [ basic_auth: [] ]
      ...

      response '201', 'blog created' do
        let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
        run_test!
      end

      response '401', 'authentication failed' do
        let(:Authorization) { "Basic #{::Base64.strict_encode64('bogus:bogus')}" }
        run_test!
      end
    end
  end
end

# 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

Configuration & Customization

The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install.

Gem Description Added/Updated
open_api-rswag-specs Swagger-based DSL for rspec & accompanying rake task for generating Swagger files spec/swagger_helper.rb
open_api-rswag-api Rails Engine that exposes your Swagger files as JSON endpoints config/initializers/rswag_api.rb, config/routes.rb
open_api-rswag-ui Rails Engine that includes swagger-ui and powers it from your Swagger endpoints config/initializers/rswag-ui.rb, config/routes.rb

Output Location for Generated Swagger Files

You can adjust this in the swagger_helper.rb that's installed with rswag-specs:

# spec/swagger_helper.rb
RSpec.configure do |config|
  config.swagger_root = Rails.root.to_s + '/your-custom-folder-name'
  ...
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.

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. 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:

# spec/swagger_helper.rb
config.swagger_docs = {
  'v1/swagger.json' => {
    openapi: '3.0.0',
    info: {
      title: 'API V1'
    },
    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]
        }
      }
    }
  }
}

# spec/integration/blogs_spec.rb
describe 'Blogs API' do

  path '/blogs' do

    post 'Creates a blog' do

      response 422, 'invalid request' do
        schema '$ref' => '#/components/schemas/errors_object'
  ...
end

# spec/integration/comments_spec.rb
describe 'Blogs API' do

  path '/blogs/{blog_id}/comments' do

    post 'Creates a comment' do

      response 422, 'invalid request' do
        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:

# spec/integration/comments_spec.rb
describe 'Blogs API' do

  path '/blogs/{blog_id}/comments' do

    post 'Creates a comment' do

      response 422, 'invalid request' do
        header 'X-Rate-Limit-Limit', type: :integer, description: 'The number of allowed requests in the current period'
        header 'X-Rate-Limit-Remaining', type: :integer, description: 'The number of remaining requests in the current period'
  ...
end

Response examples

You can provide custom response examples to the generated swagger file by calling the method examples inside the response block: However, auto generated example responses are now enabled by default in open-api-rswag. See below.

# spec/integration/blogs_spec.rb
describe 'Blogs API' do

  path '/blogs/{blog_id}' do

    get 'Retrieves a blog' do

      response 200, 'blog found' do
        examples 'application/json' => {
            id: 1,
            title: 'Hello world!',
            content: '...'
          }
  ...
end

Enable auto generation examples from responses

This is now enabled by default in open-api-rswag. You need to set the config.swagger_dry_run = false value in the spec/spec_helper.rb file. This is one of the more powerful features of rswag. When rswag runs your integration test suite via bundle exec rspec, it will capture the request and response bodies and output those values in the examples section. These integration tests are usually written with let variables for post body parameters, and since its an integration test the service is returning actual values. We might as well re-use these values and embed them into the generated swagger to provide a more real world example for request/response examples.

Add to application.rb:

RSpec.configure do |config|
  config.swagger_dry_run = false
end
open-api-rswag helper methods

There are some helper methods to help with documenting request bodies.

describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
  let(:api_key) { 'fake_key' }

  path '/blogs' do
    post 'Creates a blog' do
      tags 'Blogs'
      description 'Creates a new blog from provided data'
      operationId 'createBlog'
      consumes 'application/json'
      produces 'application/json'

      request_body_json schema: { '$ref' => '#/components/schemas/blog' },
                        examples: :blog

      request_body_text_plain
      request_body_xml schema: { '$ref' => '#/components/schemas/blog' }

      let(:blog) { { blog: { title: 'foo', content: 'bar' } } }

      response '201', 'blog created' do
        schema '$ref' => '#/components/schemas/blog'
        run_test!
      end

      response '422', 'invalid request' do
        schema '$ref' => '#/components/schemas/errors_object'
        let(:blog) { { blog: { title: 'foo' } } }

        run_test! do |response|
          expect(response.body).to include("can't be blank")
        end
      end
    end
  end
end    

In the above example, we see methods request_body_json request_body_plain request_body_xml. These methods can be used to describe json, plain text and xml body. They are just wrapper methods to setup posting JSON, plain text or xml into your endpoint. The simplest most common usage is for json formatted body to use the schema: to specify the location of the schema for the request body and the examples: :blog which will create a named example "blog" under the "requestBody / content / application/json / examples" section. Again, documenting request response examples changed in Open API 3.0. The example above would generate a swagger.json snippet that looks like this:

        ... 
        {"requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "examples": {
                "blog": {  // takes the name from  examples: :blog above
                  "value": {  //this is open api 3.0 structure -> https://swagger.io/docs/specification/adding-examples/
                    "blog": { // here is the actual JSON payload that is submitted to the service, and shows up in swagger UI as an example
                      "title": "foo",
                      "content": "bar"
                    }
                  }
                }
              },
              "schema": {
                "$ref": "#/components/schemas/blog"
              }
            },
            "test/plain": {
              "schema": {
                "type": "string"
              }
            },
            "application/xml": {
              "schema": {
                "$ref": "#/components/schemas/blog"
              }
            }
          }
        },
        }

NOTE: for this example request body to work in the tests properly, you need to let a variable named blog. The variable with the matching name (blog in this case) is eval-ed and captured to be placed in the examples section. This let value is used in the integration test to run the test AND captured and injected into the requestBody section.

open-api-rswag response examples

In the same way that requestBody examples can be captured and injected into the swagger output, response examples can also be captured. Using the above example, when the integration test is run - the swagger would include the following snippet providing more useful real world examples capturing the response from the execution of the integration test. Again 3.0 swagger changed the structure of how these are documented.

       ...  "responses": {
          "201": {
            "description": "blog created",
            "content": {
              "application/json": {
                "example": {
                  "id": 1,
                  "title": "foo",
                  "content": "bar",
                  "thumbnail": null
                },
                "schema": {
                  "$ref": "#/components/schemas/blog"
                }
              }
            }
          },
          "422": {
            "description": "invalid request",
            "content": {
              "application/json": {
                "example": {
                  "errors": {
                    "content": [
                      "can't be blank"
                    ]
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/errors_object"
                }
              }
            }
          }
        }

Route Prefix for Swagger JSON Endpoints

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:

TestApp::Application.routes.draw do
  ...

  mount OpenApi::Rswag::Api::Engine => 'your-custom-prefix'
end

Assuming a Swagger file exists at <swagger_root>/v1/swagger.json, this configuration would expose the file as the following JSON endpoint:

GET http://<hostname>/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:

OpenApi::Rswag::Api.configure do |c|
  c.swagger_root = Rails.root.to_s + '/your-custom-folder-name'
  ...
end

NOTE: If you're using rswag-specs to generate Swagger files, you'll want to ensure they both use the same <swagger_root>. The reason for separate settings is to maintain independence between the two gems. For example, you could install rswag-api independently and create your Swagger files manually.

Dynamic Values for Swagger JSON

There may be cases where you need to add dynamic values to the Swagger JSON that's returned by rswag-api. For example, you may want to provide an explicit host name. Rather than hardcoding it, you can configure a filter that's executed prior to serializing every Swagger document:

OpenApi::Rswag::Api.configure do |c|
  ...

  c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end

Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authoriation header and remove operations based on user permissions.

Enable Swagger Endpoints for swagger-ui

You can update the rswag-ui.rb initializer, installed with rswag-ui, to specify which Swagger endpoints should be available to power the documentation UI. If you're using rswag-api, these should correspond to the Swagger endpoints it exposes. When the UI is rendered, you'll see these listed in a drop-down to the top right of the page:

OpenApi::Rswag::Ui.configure do |c|
  c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs'
  c.swagger_endpoint '/api-docs/v2/swagger.json', 'API V2 Docs'
end

Route Prefix for the swagger-ui

Similar to rswag-api, you can customize the swagger-ui path by changing it's mount prefix in routes.rb:

TestApp::Application.routes.draw do
  ...

  mount OpenApi::Rswag::Api::Engine => 'api-docs'
  mount OpenApi::Rswag::Ui::Engine => 'your-custom-prefix'
end

Customizing the swagger-ui

The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui/tree/2.x#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator.

rails g rswag:ui:custom

This will add a local version that you can modify at app/views/rswag/ui/home/index.html.erb

Serve UI Assets Directly from your Web Server

Rswag ships with an embedded version of the swagger-ui, which is a static collection of JavaScript and CSS files. These assets are served by the rswag-ui middleware. However, for optimal performance you may want to serve them directly from your web server (e.g. Apache or NGINX). To do this, you'll need to copy them to the web server root. This is the "public" folder in a typical Rails application.

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.

About

Seamlessly adds a Swagger to Rails-based API's

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 94.6%
  • HTML 4.7%
  • Shell 0.7%