Oaskit.Controller (oaskit v0.2.0)

View Source

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 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

body_params(conn)

Returns the validated body from the given Plug.Conn struct.

path_param(conn, name, default \\ nil)

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.

query_param(conn, name, default \\ nil)

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

operation(action, spec)

(macro)

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 a schema/0 function.
  • :required - A boolean, defaults to true 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. When false, the body can be missing and will not be validated. In that case, conn.oaskit.private.body_params will be nil. The default value is false.

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 a schema/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 by Plug.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

parameter(name, opts)

(macro)

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

tags(tags)

(macro)

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

use_operation(action, operation_id, opts \\ [])

(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.

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.