openapi_first
OpenapiFirst helps to implement HTTP APIs based on an OpenAPI API description. It supports OpenAPI 3.0 and 3.1. It offers request and response validation and it ensures that your implementation follows exactly the API description.
Contents
Manual use
Load the API description:
require 'openapi_first'
definition = OpenapiFirst.load('petstore.yaml')
Validate request / response:
# Find the request
rack_request = Rack::Request.new(env) # GET /pets/42
request = definition.request(rack_request)
# Inspect the request and access parsed parameters
request.known? # Is the request defined in the API description?
request.content_type
request.body # alias: parsed_body
request.path_parameters # => { "pet_id" => 42 }
request.query_parameters # alias: query
request.params # Merged path and query parameters
request.headers
request.
request.request_method # => "get"
request.path # => "/pets/42"
request.path_definition # => "/pets/{pet_id}"
# Validate the request
request.validate # Returns OpenapiFirst:::Failure if validation fails
request.validate! # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if validation fails
# Find the response
rack_response = Rack::Response[*app.call(env)]
response = request.response(rack_response) # or definition.response(rack_request, rack_response)
# Inspect the response
response.known? # Is the response defined in the API description?
response.status # => 200
response.content_type
response.body
request.headers # parsed response headers
# Validate response
response.validate # Returns OpenapiFirst::Failure if validation fails
response.validate! # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError if validation fails
OpenapiFirst uses multi_json.
Rack Middlewares
All middlewares add a request object to the current Rack env at env[OpenapiFirst::REQUEST]), which is in an instance of OpenapiFirst::RuntimeRequest that responds to .params, .parsed_body etc.
This gives you access to the converted request parameters and body exaclty as described in your API description instead of relying on Rack alone to parse the request. This only includes query 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.
Request validation
The request validation middleware returns a 4xx if the request is invalid or not defined in the API description.
use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml'
Options
| Name | Possible values | Description |
|---|---|---|
spec: |
The path to the spec file or spec loaded via OpenapiFirst.load |
|
raise_error: |
false (default), true |
If set to true the middleware raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError instead of returning 4xx. |
error_response: |
:default (default), :json_api, Your implementation of ErrorResponse |
Here in an example response body about an invalid request body. See also RFC 9457.
http-status: 400
content-type: "application/problem+json"
{
"title": "Bad Request Body",
"status": 400,
"errors": [
{
"message": "value at `/data/name` is not a string",
"pointer": "/data/name",
"code": "string"
},
{
"message": "number at `/data/numberOfLegs` is less than: 2",
"pointer": "/data/numberOfLegs",
"code": "minimum"
},
{
"message": "object at `/data` is missing required properties: mandatory",
"pointer": "/data",
"code": "required"
}
]
}
openapi_first offers a JSON:API error response as well:
use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml, error_response: :jsonapi'
Here is an example error response:
// http-status: 400
// content-type: "application/vnd.api+json"
{
"errors": [
{
"status": "400",
"source": {
"pointer": "/data/name"
},
"title": "value at `/data/name` is not a string",
"code": "string"
},
{
"status": "400",
"source": {
"pointer": "/data/numberOfLegs"
},
"title": "number at `/data/numberOfLegs` is less than: 2",
"code": "minimum"
},
{
"status": "400",
"source": {
"pointer": "/data"
},
"title": "object at `/data` is missing required properties: mandatory",
"code": "required"
}
]
}
You can build your own custom error response with error_response: MyCustomClass that implements OpenapiFirst::ErrorResponse.
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
This middleware is especially useful when testing. It always raises an error if the response is not valid.
use OpenapiFirst::Middlewares::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
Options
| Name | Possible values | Description |
|---|---|---|
spec: |
The path to the spec file or spec loaded via OpenapiFirst.load |
Configuration
You can configure default options globally:
OpenapiFirst.configure do |config|
# Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
config.request_validation_error_response = :json_api
# Configure if the response validation middleware should raise an exception (defaults to false)
config.request_validation_raise_error = true
end
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
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 or start a discussion.
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. 🤗