Oaskit.Controller (oaskit v0.2.0)
View SourceProvides 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 alwo where you will plug the validation
plug Oaskit.Plugs.ValidateRequest
unquote(verified_routes())
end
end
end
It can also be useful to define a new api_controller
function, to separate
controllers that define an HTTP API.
You would then use that function in your API controllers:
defmodule MyAppWeb.UserController do
use MyAppWeb, :api_controller
# ...
end
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.
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 automatically.
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.oaskit.private.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.oaskit.private.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 automatically.
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 are query params but also path params. See below for more information.:request_body
- A map of possible content types and responses definitions. A schema module can be given directly to define a single: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 :query
and :path
values for :in
are currently supported.
Parameters support the following options:
:in
- Either:path
or:query
. Required.:schema
- A JSON schema or Module name exporting aschema/0
function.:required
- A boolean, defaults totrue
for:path
params,false
otherwise.:examples
- A list of examples.
Parameters are stored into conn.private.oaskit.path_params
and
conn.private.oaskit.query_params
. 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)
end
Defining the request body
Request bodies can be defined in two ways: Either by providing a mapping of
content-type to media type objects, or with a shortcut by providing only a
schema for an unique "application/json"
content-type.
The body can be retrieved in conn.oaskit.private.body_params
.
Options supported with a generic definition, for each content type:
:content
- A map of content-type to bodies definitions. Content-types should be strings.:required
- A boolean. Whenfalse
, the body can be missing and will not be validated. In that case,conn.oaskit.private.body_params
will benil
. The default value isfalse
.
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 aschema/0
function 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 response body object.
Important, when using the shortcut, we chose to automatically define the
:required
option of the media type 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(conn.private.oaskit.body_params) do
# ...
end
end
The operation definition above is equivalent to this:
operation :create_user,
operation_id: "CreateUser",
request_body: [
content: %{"application/json" => %{schema: CreateUserPayload}},
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: CreateUserPayload},
"application/json" => %{schema: CreateUserPayload},
"*/*" => %{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.Status
like:ok
,:not_found
, etc. :default
can be given instead of a status to define the default option supported by the OpenAPI speficication. This is often used to define a generic error response.
Response objects accept the following options:
:description
- This is mandatory for responses.:headers
and: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, or will otherwise being pulled from the schema description
keyword.
Reponse 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.schema().description,
content: %{
"application/json" => %{schema: UsersListPage}
}
]
}
The description can be overriden 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.
Ignore operations
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
end
Given 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
# ...
end
Parameter 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.