# PhoenixParams
A plug for Phoenix applications for validating and transforming HTTP request params.
Define a request schema, validate and transform the input before the controller is called: this allows you to write clean and assertive controller code.
## Sample app
A simple phoenix application that illustrates basic usage via example can be found [here](sample_app).
## Table of contents
<!-- MarkdownTOC -->
- [Example usage](#example-usage)
- [Macros](#macros)
- [The `param` macro](#the-param-macro)
- [The `global_validator` macro](#the-global_validator-macro)
- [The `typedef` macro](#the-typedef-macro)
- [Builtin types](#builtin-types)
- [Custom types](#custom-types)
- [Custom validators](#custom-validators)
- [Nested types](#nested-types)
- [Builtin validators](#builtin-validators)
- [`numericality`](#numericality)
- [`length`](#length)
- [`size`](#size)
- [`in`](#in)
- [`regex`](#regex)
- [Errors](#errors)
- [Known limitations](#known-limitations)
<!-- /MarkdownTOC -->
<a id="example-usage"></a>
## Example usage
Detailed examples can be found in the [sample app](sample_app).
* Define a [request](sample_app/lib/my_app_web/requests/user/create.ex):
```elixir
use PhoenixParams,
error_view: MyAppWeb.ErrorView
input_key_type: :atom # :atom | :string (default)
param :email,
type: String,
regex: ~r/[a-z_.]+@[a-z_.]+/
param :date_of_birth,
type: Date,
required: true,
validator: &__MODULE__.validate_dob/1
# ...
def validate_dob(date) do
date < Date.utc_today || {:error, "can't be in the future"}
end
# ...
end
```
* Set up the [controller](sample_app/lib/my_app_web/controllers/user_controller.ex#L4):
```elixir
# ...
plug MyAppWeb.Requests.User.Create when action == :create
def create(conn, params) do
params.date_of_birth
# => ~D[1986-03-27]
# ...
end
```
* Set up the [error view](sample_app/lib/my_app_web/views/error_view.ex#L8-L10):
```elixir
# ...
def render("400.json", %{conn: %Plug.Conn{assigns: %{validation_failed: errors}}}) do
errors
# => [
# {
# "param": "email",
# "message": "Validation error: has invalid format",
# "error_code": "INVALID"
# },
# {
# "param": "date_of_birth",
# "message": "Validation error: invalid date",
# "error_code": "INVALID"
# }
# ]
end
# ...
```
<a id="macros"></a>
## Macros
Request can be defined via the macros provided by `PhoenixParams`.
<a id="the-param-macro"></a>
### The `param` macro
Defines an input parameter to be coerced/validated.
Example:
```elixir
param :email,
type: String,
source: :body,
regex: ~r/[a-z_.]+@[a-z_.]+/
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex)
Accepts two arguments: _name_ and _options_
Allowed options:
| option | type | description |
|:--------------|:---------|:------------|
|`type` | atom | mandatory. Example: `type: Integer`. See [Builtin types](#builtin-types) |
|`required` | boolean | optional. Defaults to `false`. When `true`, a validation error is returned whenever the param is missing or its value is `nil`. |
|`nested` | boolean | optional. Defaults to `false`. Denotes the param's type is a [nested request](#nested-types). |
|`default` | any | optional. Value to set if the current value is nil. If a function is given, use its result. Default values are set before validation. |
|`source` | atom | optional. Possible values `:path`, `:query`, `:body`, `:auto` (default) |
|`validator` | function | optional. A [custom validator](#custom-validators) in the format `&Mod.fun/arity`. |
|`regex` | regex | optional. A [builtin validator](#regex) |
|`length` | map | optional. A [builtin validator](#length) |
|`size` | map | optional. A [builtin validator](#size) |
|`in` | list | optional. A [builtin validator](#in) |
|`numericality` | map | optional. A [builtin validator](#numericality) |
<a id="the-global_validator-macro"></a>
### The `global_validator` macro
Defines a global validation to be applied.
Example:
```elixir
global_validator &__MODULE__.my_global_validator/1
def my_global_validator(params) do
# ...
end
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex).
Accepts one argument: a remote function in the format `&Mod.fun/arity`, which will be called with one argument - the coerced `params` (map).
The function will _not_ be called unless all individual coercions and validations on the params have passed.
The return value is ignored, unless it's a `{:error, reason}`, which signals a validation failure.
<a id="the-typedef-macro"></a>
### The `typedef` macro
Defines a custom param type, see [custom types](#custom-types).
<a id="builtin-types"></a>
## Builtin types
* `String`
* `Integer`
* `Float`
* `Boolean`
* `Date` - expects a ISO8601 date and coerces it to a Date struct.
* `DateTime` - expects a ISO8601 date with time and coerces it to a DateTime struct.
Types can be wrapped in `[]`, indicating the value is a list. Example:
* `[String]`
* `[Integer]`
* ...
<a id="custom-types"></a>
## Custom types
Defined via the `typedef` macro.
Useful when the [builtin types](#builtin-types) are not enough to represent the input data.
Example:
```elixir
typedef Locale, &__MODULE__.coerce_locale/1
def coerce_locale(l) do
# ...
end
```
Detailed examples [here](sample_app/lib/my_app_web/requests/shared/address.ex).
Accepts two arguments: a _name_ and a _coercer_.
The function will _always_ be called, even if the param is missing (value would be `nil` in this case).
The return value will replace the original one, unless it's a `{:error, reason}`, which signals a coercion failure.
<a id="custom-validators"></a>
## Custom validators
Functions which will be called with one argument - the param value - when (if) all params are successfully coerced.
The function's return value is ignored, unless it matches `{:error, reason}`, which signals a validation failure.
Example:
```elixir
param :date_of_birth,
type: Date,
required: true,
validator: &__MODULE__.validate_dob/1
def validate_dob(date) do
date < Date.utc_today || {:error, "can't be in the future"}
end
```
If the type is a list, in order to validate each element, manually call the `validate_each/2` function inside your custom validator. This function expects the _list_ and a _function_ (in the format `&Mod.fun/arity`) which will validate separate elements.
Example:
```elixir
param :hobbies,
type: [String],
validation: &__MODULE__.validate_hobbies/1
def validate_hobbies(list), do: validate_each(list, &validate_hobby/1)
def validate_hobby(value), do: String.length(hobby) > 3 || {:error, "too short"}
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L47-L51).
<a id="nested-types"></a>
## Nested types
Consider the following JSON request:
```json
{
"name": "Hans Zimmer",
"age": 31,
"address": {
"country": "Germany",
"city": "Frankfurt AM",
"street_no": 26
}
}
```
The `address` param is a whole new structure which can be expressed via a nested request definition.
Example:
```elixir
defmodule UserRequest do
# ...
param :name, type: String
param :age, type: Integer
param :address, type: AddressRequest, nested: true
end
defmodule AddressRequest do
# ...
param :country, type: String
param :city, type: String
param :street_no, type: Integer
end
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L33-L34) and [here](sample_app/lib/my_app_web/requests/shared/address.ex)
<a id="builtin-validators"></a>
## Builtin validators
Validators for some common use-cases are provided OOTB. Note that, in case the value is a list, those validators are applied to the entire list (not its elements).
<a id="numericality"></a>
### `numericality`
Validates numbers. Accepts the following options:
| key | value type | meaning |
|:------|:-----------|:--------|
|`:gt` | integer |min valid value (non-inclusive)|
|`:gte` | integer |min valid value (inclusive)|
|`:lt` | integer |max valid value (non-inclusive)|
|`:lte` | integer |max valid value (inclusive)|
|`:eq` | integer |exact valid value|
Example:
```elixir
param :age,
type: Integer,
numericality: %{gte: 18}
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L26).
<a id="length"></a>
### `length`
Validates string lengths. Same options as the [`numericality`](#numericality) validator.
Example:
```elixir
param :email,
type: String,
length: %{gt: 5, lt: 100}
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L7).
<a id="size"></a>
### `size`
Validates list size (ie. the number of elements). Same options as the [`numericality`](#numericality) validator.
Example:
```elixir
param :hobbies,
type: [String],
size: %{eq: 5}
```
<a id="in"></a>
### `in`
Validates against a list of valid values. Accepts a list with the allowed values.
Example:
```elixir
param :language,
type: String,
in: ["Elixir", "Ruby", "Python", "Java", "Other"]
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L21).
<a id="regex"></a>
### `regex`
Validates against a regular expression. Accepts a pattern.
Example:
```elixir
param :email,
type: String,
regex: ~r/[a-z_.]+@[a-z_.]+/
```
Detailed examples [here](sample_app/lib/my_app_web/requests/user/create.ex#L11).
<a id="errors"></a>
## Errors
Each error is represented by a map and passed to the error view as a `validation_failed` assign.
The assigned value is either a list (many validation errors) or a map (one error). Example:
```elixir
[
%{
param: "email",
message: "Validation error: invalid format",
error_code: "INVALID"
},
%{
param: "date",
message: "Validation error: required",
error_code: "MISSING"
}
]
```
Each error is a map with the following keys:
* `param` - _optional_. It is omitted if the error is due to a _global validation_ (which usually is used to validate a combination of several params)
* `message` - always present
* `error_code` - always present. Either `"INVALID"` or `"MISSING"`
If the error occurred within a list's element (as reported by `validate_each/2`) the `message` value will be "element at index <i>: <error>". Example: `"element at index 0: invalid format"`
If the error occurred within a [nested param](#nested-types), the `param` value will be "<parent_param>.<nested_param>". Example: `"address.street_number: not an integer"`
If the error occurred within a list's element, which is also a [nested param](#nested-types), the `param` value will be "<parent_param>.[<i>].<nested_param>". Example: `"address.[0].street_number: not an integer"`
If you don't want to perform any transformation to those results, just return them as-is in your error view:
```elixir
def render("400.json", %{conn: %Plug.Conn{assigns: %{validation_failed: errors}}}) do
errors
end
```
Examples [here](sample_app/lib/my_app_web/views/error_view.ex).
<a id="known-limitations"></a>
## Known limitations
They will hopefully be addressed in a future version:
* No more than one validator per param is supported (including builtin validators).<br/>Workaround: call any extra validators inside a custom validator function. Builtin validators are called like so:<br/>`run_builtin_validation(:numericality, opts, value)`
* Builtin validators can't be instructed to to work on individual list elements.<br/>Workaround: call builtin validators inside a custom validator function (see above note).
* There is no `Any` type for param values of an unknown nature.<br/>Workaround: omit those in the request definition and access them in the controller via `conn.body_params` and `conn.query_params`.
* There is no plain `List` type, for lists containing non-homegenic values (of different types).<br/>Workaround: same as above
* Types that are list (eg. `type: [Integer]`) allow `nil` elements.<br/>Workaround: ensure your custom validator (if any) handles those.