Current section
Files
Jump to
Current section
Files
lib/params_validation.ex
require Logger
import Plug.Conn
require Logger
defmodule ParamsValidation do
@moduledoc false
defmacro __using__(_opts) do
quote do
require ParamsValidation
import ParamsValidation, only: [expect: 1, body_params: 0, query_params: 0]
end
end
@spec body_params :: any
defmacro body_params do
quote do
var!(conn).private.params_validator_result.body_params
end
end
@spec query_params :: any
defmacro query_params do
quote do
var!(conn).private.params_validator_result.query_params
end
end
@doc """
# opts
:log_errors? - boolean value indicating whether to log errors (defaults to false)
"""
def init(opts) do
[log_errors?: opts[:log_errors?] || false]
end
@doc """
## opts
`:body_params` map mapping from an atom (param name) into an ecto type (:string, :integer, ...)
`:path_params` map mapping from an atom (param name) into an ecto type (:string, :integer, ...)
`:optional_body_params` a list of atoms (param names)
"""
def expect(opts) do
body_param_types = opts[:body_params] || %{}
path_param_types = opts[:path_params] || %{}
query_param_types = opts[:query_params] || %{}
optional_body_params = opts[:optional_body_params] || []
validate(body_param_types, path_param_types, query_param_types, optional_body_params)
end
defp validate(
body_param_types,
path_param_types,
query_param_types,
optional_body_params
) do
body_keys = Map.keys(body_param_types)
path_keys = Map.keys(path_param_types)
query_keys = Map.keys(query_param_types)
required_body_params = body_keys |> Enum.filter(fn key -> key not in optional_body_params end)
default_body_params = optional_body_params |> Map.new(fn key -> {key, nil} end)
default_query_params = query_keys |> Map.new(fn key -> {key, nil} end)
[
private: %{
params_validator:
{body_param_types, body_keys, path_param_types, path_keys, required_body_params,
default_body_params, query_param_types, query_keys, default_query_params}
}
]
end
def call(
%{
method: "GET",
private: %{
params_validator:
{_body_param_types, _body_keys, path_param_types, path_keys, _required_body_params,
_default_body_params, query_param_types, query_keys, default_query_params}
}
} = conn,
log_errors?: log_errors?
) do
path_params = conn.path_params
query_params = conn.query_params
with {:ok, _applied_params} <-
use_validator(path_params, path_param_types, path_keys, path_keys, %{}),
{:ok, applied_query_params} <-
use_validator(query_params, query_param_types, query_keys, [], default_query_params) do
params_validator_result = %{query_params: applied_query_params}
conn |> Plug.Conn.put_private(:params_validator_result, params_validator_result)
else
{:error, errors} ->
if log_errors?, do: log_error(errors)
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{errors: errors}))
|> halt
end
end
def call(
%{
private: %{
params_validator:
{body_param_types, body_keys, path_param_types, path_keys, required_body_params,
default_body_params, query_param_types, query_keys, default_query_params}
}
} = conn,
log_errors?: log_errors?
) do
body_params = conn.body_params
path_params = conn.path_params
query_params = conn.query_params
with {:ok, applied_body_params} <-
use_validator(
body_params,
body_param_types,
body_keys,
required_body_params,
default_body_params
),
{:ok, _applied_path_params} <-
use_validator(path_params, path_param_types, path_keys, path_keys, %{}),
{:ok, applied_query_params} <-
use_validator(query_params, query_param_types, query_keys, [], default_query_params) do
params_validator_result = %{
body_params: applied_body_params,
query_params: applied_query_params
}
conn |> Plug.Conn.put_private(:params_validator_result, params_validator_result)
else
{:error, errors} ->
if log_errors?, do: log_error(errors)
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{errors: errors}))
|> halt
end
end
def call(
conn,
_opts
) do
conn
end
@spec use_validator(map, map, list, list, map) :: {:ok, map} | {:error, any}
defp use_validator(%Plug.Conn.Unfetched{}, _, _, _, _) do
{:ok, %{}}
end
defp use_validator(params, types, keys, required, default) do
changeset = default_validator(params, types, keys, required)
if changeset.valid? do
{:ok, default |> Map.merge(changeset.changes)}
else
{:error, error_messages(changeset)}
end
end
defp default_validator(params, types, keys, required) do
{params, types}
|> Ecto.Changeset.cast(params, keys)
|> Ecto.Changeset.validate_required(required)
end
defp error_messages(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
defp log_error(error) do
Logger.error("params_validation error: #{inspect(error)}")
end
end