Skip to content

Repository files navigation

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.

  • Request bodies, path, query and header parameters validation with JSON Schema 2020-12, powered by JSV, a validator that passes the official JSON Schema test suite.
  • Full OpenAPI 3.1 support: type: ["string", "null"] instead of nullable: true, oneOf/anyOf/allOf, $ref and $defs behave as the JSON Schema specification says.
  • Schemas are modules, plain Elixir maps, or JSON files: anything JSV accepts.
  • Response validation helpers for your tests.
  • Mix task for JSON file specification generation, and a controller to serve the spec and a Redoc UI.
  • Heavily inspired by OpenApiSpex, which targets OpenAPI 3.0. Oaskit is the choice when you need OpenAPI 3.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 plain Elixir data, given directly or with the {schema, options} form.

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 JSON response describing the errors: 400 for invalid parameters, 422 for an invalid body and 415 for an unsupported content type. Errors 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

  • Serve Swagger UI.
  • Allow custom formatters for the openapi.dump Mix task, to support other output formats such as YAML.

About

A fully featured OpenAPI Request validator for Elixir Plug applications

Resources

Security policy

Stars

66 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages