PhoenixParams v1.2.2 PhoenixParams
A plug for Phoenix applications for validating HTTP request params.
Example usage:
defmodule ApiWeb.UserController do
use ApiWeb, :controller
plug Api.Plugs.Requests.User.Index when action in [:index]
def index(conn, params) do
# params is now a map with transformed values
# when params names are declared as atoms in the request definition
# params will be a map with atom keys
user = params.user
# ...
end
end
defmodule Api.Plugs.Requests.User.Index do
use Api.Plugs.Request, error_view: ApiWeb.ErrorView
param :format,
type: String,
default: "json",
in: ~w[json csv]
param :date,
type: Date,
required: true,
source: :body,
validator: &__MODULE__.validate_date/1
param :merchant_id,
type: Integer,
numericality: %{greater_than: 0}
param :email,
type: [String],
validator: &__MODULE__.validate_email/1
global_validator &__MODULE__.ensure_mid_or_email/1
#
# Date validators
#
def validate_date(date) do
# return {:error, message} if invalid
# otherwise the validation passes
end
#
# Email validators
#
def validate_email({:error, _}), do: :noop
# Invoke on separate elements
def validate_email(list) when is_list(list) do
validate_each(list, &validate_email/1)
end
def validate_email(email) do
email =~ ~r/..../ || {:error, "is not a valid email address"}
end
#
# Global validators
#
def ensure_mid_or_email({:error, _}) do
params[:merchant_id] || params[:email] ||
{:error, "merchant id or email required"}
end
end
Synopsis:
param
- name - either an atom or binary
- options - a keyword list: type - mandatory. See below for possible values. required - optional. Either true or false (default). nested - optional. Either true or false (default). More info on nested types below validator - optional. A custom validator function in the form &Module.function/arity source - optional. Either :path, :body, :query or :auto (default) default - optional. Default param value.
Supported types of the param are:
Types can be wrapped in [], indicating the value is an array. Example:
[String]
[Integer]
- …
Custom types are also supported. Example:
defmodule Requests.Index do
use Api.Plugs.Request
typedef Phone, &Coercers.phone/1
typedef Device, &Coercers.device/1
param :landline, type: Phone, required: true
param :device, type: Device
end
defmodule Coercers do
def phone(value) do
# transform your value here to anything
end
# ...
end
Nested types are also supported. Example:
defmodule Requests.Shared.Address do
param :country,
type: String,
required: true
# ...
end
defmodule Requests.Index do
param :address,
type: Requests.Shared.Address,
nested: true
end
Several OOTB validations exist:
- numericality - validates numbers. Accepts a keyword list with :gt, :gte, :lt, :lte and/or :eq
- in - validates the presence of anything in a list
- length - validates length of a String. Accepts a keyword list with :gt, :gte, :lt, :lte and/or :eq
- size - validates the number of elements in a list
- regex - validates the string against a regex pattern
The package is designed to be a “plug” and:
- it changes the input map’s string keys to atoms whenever the param names are defined as atoms
- it discards undefined params
- it changes (coerces) the values to whatever type they correspond to
This means that a definition like
param :age, type: Integer
will transform an input%{"name": "baba", "age": "79"}
to%{age: 79}
The original, unchanged params, are still accessible through Plug’s conn.body_params and conn.query_params. requires the below function to be defined in an Phoenix error view:
def render(“400.json”, %{conn: %{assigns: %{validation_failed: errors}}}) do
errors
end
When the type is specified as an array, (eg. [Integer]
), the
validator will receive the entire array. This is done on purpose, but you
can take advantage of the exposed validate_each/2
function to invoke it
on each element, returning properly formatted error message:
param :merchant_id,
type: [Integer],
required: true,
validator: &__MODULE__.checkmid/1
# Invoke validation on each separate element
def checkmid(list) when is_list(list) do
validate_each(list, params, &checkmid/2)
end
# Validate element
def checkmid(mid) do
mid > 0 || {:error, "must be positive"}
end
Errors reported by validate_each
include which element failed validation:
"element at index 0: must be positive"
Finally, there is the global_validator
macro, which allows you to define
a callback to be invoked if all individual parameter validations passed
successfully. This is useful in cases where the context validity is not
dictated by the sole value of a single parameter, but rather a combination.
E.g. mutually-exclusive params, at-least-one-of params, etc. are all example
cases in which the request entity itself is either valid or not.
The callback should accept exactly 1 argument — the request params,
after coercion. Anything return value, different from {:error, reason} will
be considered a pass.
The single argument expected by the __using__
macro is the error view
module (usually YourAppNameWeb.ErrorView
)