Current section

Files

Jump to
para lib para.ex
Raw

lib/para.ex

defmodule Para do
@moduledoc """
Build structured parameter validation service for web endpoints,
typically in Phoenix/Plug based applications.
Let's imagine that you have a controller named Web.FooController and
wanted to validate the parameters for its :create and :update actions
1. Define your parameters schema
defmodule Web.FooPara do
use Para
validator :create do
required :name, :string
required :age, :integer
required :email, :string
optional :phone, :string
end
validator :update do
optional :name, :string
optional :age, :integer
optional :email, :string
optional :phone, :string
end
end
2. You can now use this module as validator in your controller
defmodule Web.FooController do
use Web, :schema
alias Web.FooPara
def create(conn, params) do
with {:ok, params} <- FooPara.validate(:create, params) do
# ...
end
end
end
Sometimes, you might require to use custom validators or perform data
manipulations. For this, you can use the `callback` macro. Example:
defmodule Web.FooPara do
use Para
validator :create do
required :name, :string
required :age, :integer
required :email, :string
optional :phone, :string
callback :create_validators
end
def create_validators(changeset, params) do
changeset
|> format_email(params)
|> format_phone(params)
|> validate_age()
end
def format_email(changeset, params) do
# ...
end
def format_phone(changeset, params) do
# ...
end
def validate_age(changeset) do
# ...
end
end
"""
use Ecto.Schema
@type t :: {:ok, map()} | {:error, Ecto.Changeset.t()}
defmacro validator(name, do: block) do
{:__block__, [], blocks} = block
quote do
def validate(unquote(name), params) do
Para.validate(__MODULE__, unquote(blocks), params)
end
end
end
def validate(module, blocks, params) do
spec =
blocks
|> discard_droppable_fields(params)
|> Enum.reduce(%{}, fn
{:required, name, type, opts}, acc ->
acc
|> put_in([Access.key(:data, %{}), name], opts[:default])
|> put_in([Access.key(:types, %{}), name], type)
|> put_in([Access.key(:required, [])], Map.get(acc, :required, []) ++ [name])
|> assign_inline_validators(name, opts)
{:optional, name, type, opts}, acc ->
acc
|> put_in([Access.key(:data, %{}), name], opts[:default])
|> put_in([Access.key(:types, %{}), name], type)
|> assign_inline_validators(name, opts)
_, acc ->
acc
end)
callback =
Enum.find(blocks, fn
{:callback, name} -> name
_ -> nil
end)
permitted = Map.keys(spec.data)
changeset =
{spec.data, spec.types}
|> Ecto.Changeset.cast(params, permitted)
|> Ecto.Changeset.validate_required(spec.required)
|> apply_inline_validators(module, spec.validators)
|> apply_callback(module, callback, params)
case changeset do
%{valid?: true} -> {:ok, Ecto.Changeset.apply_changes(changeset)}
_ -> {:error, changeset}
end
end
defmacro callback(name) do
quote do
{:callback, unquote(name)}
end
end
@doc """
Define a required field
## Options
* `:validator` - Define either one of the built-in Ecto.Changeset's validators
or use your own custom inline validator. Please refer docs on [Custom inline validator](#required/3-custom-inline-validator)
* `:droppable` - Drop the field when the value is nil. This
is especially true for actions like update where only specific fields are
submitted instead of all the available fields.
## Custom inline validator
You can define your own validator as such:
def validate_country(changeset, field) do
# always return changeset
end
Then use it as an inline validator for your field
validator :create do
required :country, :string, [validator: :validate_country]
end
"""
defmacro required(name, type \\ :string, opts \\ []) do
quote do
{:required, unquote(name), unquote(type), unquote(opts)}
end
end
@doc """
Define an optional field
"""
defmacro optional(name, type \\ :string, opts \\ []) do
quote do
{:optional, unquote(name), unquote(type), unquote(opts)}
end
end
@doc false
def discard_droppable_fields(blocks, params) do
Enum.filter(blocks, fn {_, name, _, opts} ->
with true <- opts[:droppable],
false <- Map.has_key?(params, Atom.to_string(name)) do
false
else
_ -> true
end
end)
end
@doc false
def assign_inline_validators(spec, name, opts) do
validators = spec[:validators] || %{}
if validator = opts[:validator] do
put_in(spec, [Access.key(:validators, %{}), name], validator)
else
put_in(spec, [Access.key(:validators, %{})], validators)
end
end
def apply_inline_validators(changeset, module, validators) do
Enum.reduce(validators, changeset, fn {key, validator}, acc ->
apply_inline_validator(acc, module, key, validator)
end)
end
def apply_inline_validator(changeset, module, key, validator) do
case validator do
{function, data_or_opts} ->
do_apply_inline_validator(module, function, [changeset, key] ++ [data_or_opts])
{function, data, opts} ->
do_apply_inline_validator(module, function, [changeset, key] ++ [data, opts])
_ ->
changeset
end
end
def do_apply_inline_validator(module, function, params) do
arity = length(params)
if function_exported?(Ecto.Changeset, function, arity) do
apply(Ecto.Changeset, function, params)
else
apply(module, function, params)
end
end
def apply_callback(changeset, _, nil, _), do: changeset
def apply_callback(changeset, module, callback, params) do
apply(module, callback, [changeset, params])
end
defmacro __using__(_) do
quote do
import Para,
only: [
validator: 2,
required: 1,
required: 2,
required: 3,
optional: 1,
optional: 2,
optional: 3
]
end
end
end