Skip to content

Rack middlewares to create OpenAPI based HTTP APIs

License

Notifications You must be signed in to change notification settings

ursm/openapi_first

 
 

Repository files navigation

OpenapiFirst

Join the chat at https://gitter.im/openapi_first/community

OpenapiFirst helps to implement HTTP APIs based on an OpenAPI API description. It supports OpenAPI 3.0 and 3.1.

It provides these Rack middlewares:

  • OpenapiFirst::RequestValidation – Validates the request against the API description and returns 400 if the request is invalid.
  • OpenapiFirst::ResponseValidation Validates the response and raises an exception if the response body is invalid.
  • OpenapiFirst::Router – This internal middleware is added automatically when using request/response validation. It adds the OpenAPI operation for the current request to the Rack env.

Using Request and Response validation together ensures that your implementation follows exactly the API description. This enables you to use the API description as a single source of truth for your API, reason about details and use various tooling.

Request Validation

The OpenapiFirst::RequestValidation middleware returns a 400 status code with a body that describes the error if the request is not valid.

use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'

It adds these fields to the Rack env:

  • env[OpenapiFirst::PARAMS] – The parsed parameters (query, path) for the current request (string keyed)
  • env[OpenapiFirst::REQUEST_BODY] – The parsed request body (string keyed)
  • env[OpenapiFirst::OPERATION] (Added via Router) – The Operation object for the current request. This is an instance of OpenapiFirst::Operation.

Options and defaults

Name Possible values Description Default
spec: The path to the spec file or spec loaded via OpenapiFirst.load
raise_error: false, true If set to true the middleware raises OpenapiFirst::RequestInvalidError instead of returning 4xx. false (don't raise an exception)
error_response: :default, :json_api, Your implementation of ErrorResponse :default

The error responses conform with JSON:API.

Here's an example response body for a missing query parameter "search":

http-status: 400
content-type: "application/json"

{
  "errors": [
    {
      "title": "is missing",
      "source": {
        "parameter": "search"
      }
    }
  ]
}

Parameters

The RequestValidation middleware adds env[OpenapiFirst::PARAMS] (or env['openapi.params'] ) with the converted query and path parameters. This only includes the parameters that are defined in the API description. It supports every style and explode value as described in the OpenAPI 3.0 and 3.1 specs. So you can do things these:

# GET /pets/filter[id]=1,2,3
env[OpenapiFirst::PARAMS] # => { 'filter[id]' => [1,2,3] }

# GET /colors/.blue.black.brown?format=csv
env[OpenapiFirst::PARAMS] # => { 'color_names' => ['blue', 'black', 'brown'], 'format' => 'csv' }

# And a lot more.

Integration for specific webframeworks is ongoing. Don't hesitate to create an issue with you specific needs.

Request body validation

This middleware adds the parsed request body to env[OpenapiFirst::REQUEST_BODY].

The middleware will return a status 415 if the requests content type does not match or 400 if the request body is invalid.

Header, Cookie, Query and Path parameter validation

The RequestValidation middleware validates the request headers, cookies and path parameters as defined in you API description. It returns a 400 status code if the request is invalid. It adds the parsed merged path and query parameters to env['openapi.params']. Separate parsed parameters are made available by location at env['openapi.path_params'], env['openapi.query'], env['openapi.headers'], env['openapi.cookies'] as well if you need to access them separately.

readOnly / writeOnly properties

Request validation fails if request includes a property with readOnly: true.

Response validation fails if response body includes a property with writeOnly: true.

Response validation

The OpenapiFirst::ResponseValidation middleware is especially useful when testing. It always raises an error if the response is not valid.

use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'

Options

Name Possible values Description Default
spec: The path to the spec file or spec loaded via OpenapiFirst.load

OpenapiFirst::Router

This middleware is used automatically, but you can add it to the top of your middleware stack if you want to customize the behavior via options.

use OpenapiFirst::Router, spec: './openapi/openapi.yaml'

This middleware adds env['openapi.operation'] which holds an instance of OpenapiFirst::Operation that responds to #operation_id, #path (and #[] to access raw fields).

Options and defaults

Name Possible values Description Default
spec: The path to the spec file or spec loaded via OpenapiFirst.load
raise_error: false, true If set to true the middleware raises OpenapiFirst::NotFoundError when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. false (don't raise an exception)
not_found: :continue, :halt If set to :continue the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with raise_error: true raise_error gets preference and an exception is raised. :halt (return 4xx response)

Global configuration

You can configure default options gobally via OpenapiFirst::Config:

OpenapiFirst::Config.default_options = {
  error_response: :json_api,
  request_validation_raise_error: true
}

Alternatives

This gem is inspired by committee (Ruby) and connexion (Python).

Here's a comparison between committee and openapi_first.

Try it out

See examples.

Installation

Add this line to your application's Gemfile:

gem 'openapi_first'

OpenapiFirst uses multi_json.

Manual response validation

Instead of using the ResponseValidation middleware you can validate the response in your test manually via rack-test and ResponseValidator.

# In your test (rspec example):
require 'openapi_first'
validator = OpenapiFirst::ResponseValidator.new('petstore.yaml')

# This will raise an exception if it found an error
validator.validate(last_request, last_response)

Handling only certain paths

You can filter the URIs that should be handled by passing only to OpenapiFirst.load:

spec = OpenapiFirst.load('./openapi/openapi.yaml', only: { |path| path.starts_with? '/pets' })
use OpenapiFirst::RequestValidation, spec: spec

Development

Run bin/setup to install dependencies.

See bundle exec rake to run the linter and the tests.

Run bundle exec rspec to run the tests only.

Benchmarks

Results

Run benchmarks

cd benchmarks
bundle
bundle exec ruby benchmarks.rb

Contributing

If you have a question or an idea or found a bug don't hesitate to create an issue on GitHub or reach out via chat.

Pull requests are very welcome as well, of course. Feel free to create a "draft" pull request early on, even if your change is still work in progress. 🤗

About

Rack middlewares to create OpenAPI based HTTP APIs

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 99.5%
  • Other 0.5%