defmodule OpenApiSpex.Plug.CastAndValidate do
@moduledoc """
Module plug that will cast and validate the `Conn.params` and `Conn.body_params` according to the schemas defined for the operation.
The operation_id can be given at compile time as an argument to `init`:
plug OpenApiSpex.Plug.CastAndValidate,
json_render_error_v2: true,
operation_id: "MyApp.ShowUser"
For phoenix applications, the operation_id can be obtained at runtime automatically.
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true
...
end
Casted params and body params are always stored in `conn.private`.
The option `:replace_params` can be set to false to avoid overwriting conn `:body_params` and `:params`
with their casted version.
plug OpenApiSpex.Plug.CastAndValidate,
json_render_error_v2: true,
operation_id: "MyApp.ShowUser",
replace_params: false
If you want customize the error response, you can provide the `:render_error` option to register a plug which creates
a custom response in the case of a validation error.
## Example
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.CastAndValidate, render_error: MyApp.RenderError
...
end
defmodule MyApp.RenderError do
def init(opts), do: opts
def call(conn, reason) do
msg = Jason.encode!(%{error: reason})
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(400, msg)
end
end
"""
@behaviour Plug
alias OpenApiSpex.Plug.PutApiSpec
alias Plug.Conn
@impl Plug
def init(opts) do
opts = Map.new(opts)
error_renderer =
if opts[:json_render_error_v2],
do: OpenApiSpex.Plug.JsonRenderErrorV2,
else: OpenApiSpex.Plug.JsonRenderError
Map.put_new(opts, :render_error, error_renderer)
end
@impl Plug
def call(
conn = %{private: %{open_api_spex: _}},
%{
operation_id: operation_id,
render_error: render_error
} = opts
) do
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
operation = operation_lookup[operation_id]
cast_opts = opts |> Map.take([:replace_params]) |> Map.to_list()
case OpenApiSpex.cast_and_validate(spec, operation, conn, nil, cast_opts) do
{:ok, conn} ->
conn
{:error, errors} ->
errors = render_error.init(errors)
conn
|> render_error.call(errors)
|> Conn.halt()
end
end
def call(
conn = %{
private: %{
phoenix_controller: controller,
phoenix_action: action,
open_api_spex: _
}
},
opts
) do
{_spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
# This caching is to improve performance of extracting Operation specs
# at runtime when they're using the @doc-based syntax.
operation_lookup =
case operation_lookup[{controller, action}] do
nil ->
operation = controller.open_api_operation(action)
if operation do
operation =
PutApiSpec.get_and_cache_controller_action(
conn,
operation.operationId,
{controller, action}
)
{:found_it, operation}
else
# this is the case when operation: false was used
{:skip_it, nil}
end
operation ->
{:found_it, operation}
end
case operation_lookup do
{:skip_it, _} ->
conn
{:found_it, nil} ->
raise "operationId was not found in action API spec"
{:found_it, operation} ->
call(conn, opts |> Map.put(:operation_id, operation.operationId))
end
end
def call(_conn = %{private: %{open_api_spex: _pd}}, _opts) do
raise ":operation_id was neither provided nor inferred from conn. Consider putting plug OpenApiSpex.Plug.CastAndValidate rather into your phoenix controller."
end
def call(_conn, _opts) do
raise ":open_api_spex was not found under :private. Maybe PutApiSpec was not called before?"
end
end