Provides macros to define OpenAPI operations directly from controllers.
Macros requires to use Oaskit.Controller from your controllers. This
can be done wherever use Phoenix.Controller is called. With Phoenix, this is
generally in your MyAppWeb module, in the controller function:
defmodule MyAppWeb do
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: MyAppWeb.Layouts]
use Oaskit.Controller # <-- Add it there once for all
use Gettext, backend: MyAppWeb.Gettext
import Plug.Conn
# This is also where you will plug the validation
plug Oaskit.Plugs.ValidateRequest
unquote(verified_routes())
end
end
endIt can also be useful to define a new api_controller function, to separate
controllers that define an HTTP API from those that serve HTML pages.
You would then use that function in your API controllers:
defmodule MyAppWeb.UserController do
use MyAppWeb, :api_controller
# ...
endThe API Controllers in Web module guide explains how the
MyAppWeb module dispatches use MyAppWeb, :controller and how to add such an
api_controller entry point.
Summary
Functions
Returns the validated body from the given Plug.Conn struct.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Controller Macros
Defines an OpenAPI operation for the given Phoenix action (the function listed
in the router that will handle the conn) that can be validated automatically
with the Oaskit.Plugs.ValidateRequest plug.
Defines a parameter for all operations defined later in the module body with
the operation/2 macro.
Defines tags for all operations defined later in the module body with the
operation/2 macro.
This macro allows Oaskit to validate request bodies, query and path
parameters (and responses in tests) when an OpenAPI specification is not
defined with the operation/2 macro but rather provided directly in an
external spec document.
Functions
Returns the validated body from the given Plug.Conn struct.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Returns the validated parameter from conn.private.oaskit.header_params if
found, or the default value.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Returns the validated parameter from conn.private.oaskit.path_params if
found, or the default value.
Accepts a Plug.Conn struct, a parameter name (as atom) and a default value.
Returns the validated parameter from conn.private.oaskit.query_params if
found, or the default value.
Controller Macros
Defines an OpenAPI operation for the given Phoenix action (the function listed
in the router that will handle the conn) that can be validated automatically
with the Oaskit.Plugs.ValidateRequest plug.
This macro accepts the function name and a list of options that will define an
Oaskit.Spec.Operation.
Options
:operation_id- The ID of the operation that is used throughout the validation features. If missing, an id is automatically generated. Operation IDs must be unique.:tags- A list of tags (strings) to attach to the operation.:description- An optional string to describe the operation in the OpenAPI spec.:summary- A short summary of what the operation does.:parameters- A keyword list with parameter names as keys and parameter definitions as values. Parameters can be path, query or header params. See below for more information.:request_body- A keyword list with the:contentand:requiredoptions, or a shortcut: a schema module, or a{schema, options}tuple, for a single"application/json"content type. See below for request body formats.:responses- A map or keyword list where keys are status codes (integers or atoms) and values are responses definitions. See below for responses formats.
Pass false instead of the options to ignore an action function.
Defining parameters
Parameters are organized by their name and their :in option. Two parameters
with the same key can coexist if their :in option is different. The :path,
:query and :header values for :in are currently supported.
Parameter definitions are keyword lists or maps, supporting the following options:
:in- One of:path,:queryor:header. Required.:schema- A JSON schema or Module name exporting ajson_schema/0function.:required- A boolean, defaults totruefor:pathparams,falseotherwise.:examples- A map of names to example objects, like%{"first_page" => %{value: 1}}.:example- A single example value, a shortcut forexamples: %{"default" => %{value: value}}. Ignored when:examplesis given.:description- An optional string to describe the parameter in the OpenAPI spec. This describes the parameter itself; use the:descriptionof the given:schemato describe the accepted values.:deprecated- A boolean to mark the parameter as deprecated.:allowReserved- A boolean, only meaningful for:queryparameters. Documentation only; Oaskit never reads it when validating because query strings are decoded by Plug before Oaskit sees them.
Parameters are stored into conn.private.oaskit.path_params,
conn.private.oaskit.query_params and conn.private.oaskit.header_params,
and can be read with path_param/3, query_param/3 and header_param/3.
They do not override the params
argument passed to your phoenix action function. Those original params are
still the ones as decoded by phoenix.
Parameters example
# Imaginary GET /api/users/:organization route
operation :list_users,
operation_id: "ListUsers",
parameters: [
organization: [in: :path, required: true, schema: %{type: :string}],
page: [in: :query, required: false, schema: %{type: :integer, minimum: 1}],
per_page: [in: :query, required: false, schema: %{type: :integer, minimum: 1}]
],
# ...
def list_users(conn, _params) do
page = query_param(conn, :page)
per_page = query_param(conn, :per_page)
do_something_with(conn, page, per_page)
endDefining the request body
Request bodies can be defined in two ways: Either with a keyword list giving a
mapping of content-types to media type objects, or with a shortcut by
providing only a schema for a unique "application/json" content-type.
The body can be retrieved with body_params/1, or in
conn.private.oaskit.body_params.
Options supported with the full definition:
:content- A map of content-types to media type objects. Content-types must be strings. Media type objects are maps or keyword lists with a required:schemakey, and optionally:examples, a map of names to example objects like%{"alice" => %{value: %{name: "Alice"}}}. A single:examplevalue can be given instead, it is a shortcut forexamples: %{"default" => %{value: value}}.:required- A boolean. Whenfalse, the body can be missing and will not be validated. In that case,conn.private.oaskit.body_paramswill benil. The default value isfalse.:description- An optional string to describe the request body in the OpenAPI spec.
When using the shortcut, a single atom or 2-tuple is expected.
- Supported atoms are
true(a JSON schema that accepts anything),false(a JSON schema that rejects everything) or a module name. The module must export ajson_schema/0function that returns a JSON schema. - When passing a tuple, the first element is a schema (boolean or module), but
a direct JSON schema map (like
%{type: :object, ...}) is also accepted. The second tuple element is a list of options for the request body object, like:required, but not:content.
A JSON schema map cannot be given alone, it must be wrapped in a tuple, as in
{%{type: :object, ...}, []}.
Important, when using the shortcut, we chose to automatically define the
:required option of the request body object to true.
Request body examples
A short form using a module schema:
operation :create_user,
operation_id: "CreateUser",
request_body: UserSchema,
# ...
def create_user(conn, _params) do
case Users.create_user(body_params(conn)) do
# ...
end
endThe operation definition above is equivalent to this:
operation :create_user,
operation_id: "CreateUser",
request_body: [
content: %{"application/json" => %{schema: UserSchema}},
required: true
],
# ...To make the body non-required in the short form, use the tuple version:
operation :create_user,
operation_id: "CreateUser",
request_body: {UserSchema, required: false},
# ...Multiple content-types can be supported. Content-types with wildcards will be
tried last by Plug.Parsers, as well as oaskit when choosing
the schema for validation.
operation :create_user,
request_body: [
content: %{
"application/x-www-form-urlencoded" => %{schema: UserSchema},
"application/json" => %{schema: UserSchema},
"*/*" => %{schema: %{type: :string}}
}
]Defining responses
Responses are defined by a mapping of HTTP statuses to response objects.
- HTTP statuses can be given as integers (
200,404, etc.) or atoms supported byPlug.Conn.Statuslike:ok,:not_found, etc. :defaultcan be given instead of a status to define the default option supported by the OpenAPI specification. This is often used to define a generic error response.
Response objects accept the following options:
:description- This is mandatory for responses.:headersand:links- This is not used by the validation mechanisms of this library, but is useful to be defined in the OpenAPI specification JSON document.:content- A mapping of content-type to media type objects, exactly as in the request bodies definitions.
Finally, the response for each status can also be defined with a shortcut, by
using a single schema that will be associated to the "application/json"
content-type. The mandatory description can be provided when using the tuple
shortcut. Otherwise it is taken from the description keyword of the schema,
given as a map or as a module, and defaults to "no description" for schemas
without a description.
Response examples
A first example using the atom statuses, and a shortcut for the full response definition:
operation :list_users,
operation_id: "ListUsers",
responses: [ok: UsersListPage]The definition above is equivalent to the following:
operation :list_users,
operation_id: "ListUsers",
responses: %{
200 => [
description: UsersListPage.json_schema().description,
content: %{
"application/json" => %{schema: UsersListPage}
}
]
}The description can be overridden when using the shortcut:
operation :list_users,
operation_id: "ListUsers",
responses: [ok: {UsersListPage, description: "A page of users"}]Multiple status codes are generally expected. The shortcut can be used in only a part of them.
operation :list_users,
operation_id: "ListUsers",
responses: [
ok: UsersListPage,
not_found: {GenericErrorSchema, description: "not found generic response"},
forbidden: {%{type: :array}, description: "missing-role messages"},
internal_server_error: [
description: "Error with stacktrace",
content: %{
"application/json" => [
schema: %{type: :array, items: %{type: :string, description: "trace item"}}
],
"text/plain" => [schema: true]
}
]
]Of course, mixing all styles together is discouraged for readability.
Full example
An operation using the full definitions, with multiple content-types for the request body and the responses:
operation :create_user,
operation_id: "CreateUser",
summary: "Creates a user",
tags: ["users"],
parameters: [
organization: [in: :path, schema: %{type: :string}],
"x-request-id": [in: :header, schema: %{type: :string, format: :uuid}]
],
request_body: [
required: true,
content: %{
"application/json" => [
schema: UserSchema,
examples: %{
"alice" => %{value: %{name: "Alice", email: "alice@example.com"}}
}
],
"application/x-www-form-urlencoded" => [schema: UserSchema]
}
],
responses: [
created: [
description: "The created user",
headers: %{"location" => %{schema: %{type: :string}}},
content: %{
"application/json" => [schema: UserSchema],
"text/plain" => [schema: %{type: :string}]
}
],
unprocessable_content: [
description: "Invalid user",
content: %{
"application/json" => [
schema: Oaskit.ErrorHandler.Default.error_response_schema()
]
}
]
]Ignore operations
Pass false instead of the options to explicitly ignore an action. Requests
for that action are not validated, the action does not appear in the OpenAPI
specification, and Oaskit.Plugs.ValidateRequest does not log a
warning about a missing operation.
operation :legacy_import, false
def legacy_import(conn, params) do
# ...
end
Defines a parameter for all operations defined later in the module body with
the operation/2 macro.
Takes the same options as the :parameters option items from that macro.
If an operation also directly defines a parameter with the same name and
:in option, it will take precedence and the parameter defined with
parameter/2 will be ignored.
Example
In the following example, the second operation defines its own version of the
per_page parameter to limit the number of users returned in a single page.
# This macro can be called multiple times
parameter :slug, in: :path, schema: %{type: :string, pattern: "[0-9a-z-]+"}
parameter :page, in: :query, schema: %{type: :integer, minimum: 1}
parameter :per_page, in: :query, schema: %{type: :integer, minimum: 1, maximum: 100}
operation :list_users, operation_id: "ListUsers", responses: [ok: UsersListPage]
def list_users(conn, params) do
# ...
end
operation :list_users_deep,
operation_id: "ListUsersDeep",
parameters: [
per_page: [in: :query, schema: %{type: :integer, minimum: 1, maximum: 20}]
],
responses: [
ok:
{DeepUsersListPage,
description: "Returns users with all associated organization and blog posts data"}
]
def list_users_deep(conn, params) do
# ...
end
Defines tags for all operations defined later in the module body with the
operation/2 macro.
If an operation also directly defines tags, they will be merged.
Example
# This macro can be called multiple times
tags ["users", "v1"]
tags ["other-tag"]
operation :list_users,
operation_id: "ListUsers",
responses: [ok: UsersListPage]
def list_users(conn, params) do
# ...
end
operation :list_users_deep,
operation_id: "ListUsersDeep",
tags: ["slow"],
responses: [ok: DeepUsersListPage]
def list_users_deep(conn, params) do
# ...
end
This macro allows Oaskit to validate request bodies, query and path
parameters (and responses in tests) when an OpenAPI specification is not
defined with the operation/2 macro but rather provided directly in an
external spec document.
For instance with the following spec module:
defmodule MyAppWeb.ExternalAPISpec do
use Oaskit
@api_spec JSON.decode!(File.read!("priv/api/spec.json"))
@impl true
def spec, do: @api_spec
endGiven the spec.json file decribes an operation whose operationId is
"ListUsers", then the request/response validation can be enabled like this:
use_operation :list_users, "ListUsers"
def list_users(conn, params) do
# ...
endParameter names always create atoms
Query and path parameters defined in OpenAPI specifications always define the corresponding atoms, even if that specification is read from a JSON file, or defined manually in code with string keys.
For that reason it is ill advised to use specs generated dynamically at runtime without validating their content.