Oaskit

hex.pm Version Build Status License

Oaskit is an OpenAPI 3.1 library for Elixir and Phoenix: spec generation, request validation and casting, built on JSON Schema 2020-12.

It provides macros and plugs to automatically validate incoming HTTP requests against the OpenAPI Specification v3.1.

Documentation

The full documentation is available on hexdocs:

Installation

defp deps do
[
{:oaskit, "~> 0.16"},
]
end

You can also import formatter rules in your .formatter.exs file:

[
import_deps: [:oaskit]
]

Example

A condensed tour. The Quickstart Guide walks through each step in more detail.

The spec module

The spec module is the root of your OpenAPI document. Paths are collected from the Phoenix router.

defmodule MyAppWeb.ApiSpec do
alias Oaskit.Spec.Paths
alias Oaskit.Spec.Server
use Oaskit
@impl true
def spec do
%{
openapi: "3.1.1",
info: %{title: "My App API", version: "1.0.0"},
servers: [Server.from_config(:my_app, MyAppWeb.Endpoint)],
paths: Paths.from_router(MyAppWeb.Router, filter: &String.starts_with?(&1.path, "/api/"))
}
end
end

Router and controllers

Declare the spec in a router pipeline, and add the validation plug to your controllers.

# router.ex
pipeline :api do
plug :accepts, ["json"]
plug Oaskit.Plugs.SpecProvider, spec: MyAppWeb.ApiSpec
end
scope "/api", MyAppWeb do
pipe_through :api
get "/users", UserController, :index
post "/users", UserController, :create
patch "/users/:id", UserController, :update
end
# my_app_web.ex
def controller do
quote do
use Phoenix.Controller, formats: [:json]
use Oaskit.Controller
plug Oaskit.Plugs.ValidateRequest
# ...
end
end

Schemas

Oaskit validates with JSV, so a schema can be a module, a plain Elixir map, or a JSON document decoded from a file.

A schema module defined with JSV.defschema/3 is referenced by name, and valid request bodies are cast to its struct. Properties are required unless wrapped with optional/1, and the struct can be encoded to JSON.

defmodule MyAppWeb.Schemas do
use JSV.Schema
defschema User,
name: non_empty_string(),
email: email(),
# OpenAPI 3.1: a type union, no more `nullable: true`
nickname: optional(%{type: [:string, :null]})
end

An inline schema is a plain Elixir map. For request bodies and responses, wrap it in a {schema, options} tuple.

defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyAppWeb.Schemas.User
# Parameters are validated and cast: `limit` is an integer here
operation :index,
parameters: [
limit: [in: :query, schema: %{type: :integer, minimum: 1, maximum: 100}]
],
responses: [ok: {%{type: :array, items: User}, []}]
def index(conn, _params) do
users = MyApp.Users.list(limit: query_param(conn, :limit, 20))
json(conn, users)
end
# Using a schema module
operation :create,
request_body: User,
responses: [created: User]
def create(conn, _params) do
%User{} = user = body_params(conn)
# ...
end
# Using an inline schema
operation :update,
parameters: [id: [in: :path, schema: %{type: :integer}]],
request_body:
{%{
type: :object,
properties: %{email: %{type: :string, format: :email}},
required: [:email],
additionalProperties: false
}, required: true},
responses: [ok: User]
def update(conn, _params) do
id = path_param(conn, :id)
%{"email" => email} = body_params(conn)
# ...
end
end

Invalid requests are rejected with a response describing the errors: 400 for invalid parameters, 422 for an invalid body and 415 for an unsupported content type. Errors are rendered as JSON, or as an HTML page when the Accept header contains html (disable this with the html_errors: false option). They can be rendered your own way with the :error_handler option of Oaskit.Plugs.ValidateRequest.

Testing responses

Oaskit.Test.valid_response/3 checks the status, content type and body of a response against your spec, and returns the decoded body.

test "create user", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(~p"/api/users", %{name: "Alice", email: "alice@example.com"})
assert %{"name" => "Alice"} = Oaskit.Test.valid_response(MyAppWeb.ApiSpec, conn, 201)
end

Generating and serving the spec

Write the spec to a file, for client generators or CI checks:

mix openapi.dump MyAppWeb.ApiSpec --pretty -o priv/openapi.json

Or serve it, with a Redoc UI:

get "/openapi.json", Oaskit.SpecController, spec: MyAppWeb.ApiSpec
get "/docs", Oaskit.SpecController, redoc: "/openapi.json"

Contributing

Pull requests are welcome, provided they include appropriate tests and documentation.

Roadmap