Current section
Files
Jump to
Current section
Files
lib/middleware/validator.ex
defmodule Finch.Middleware.ModelValidator do
@verbs [:create, :show, :index, :destroy, :update]
@doc """
validate_type/3 takes the type, name and value of the field
and returns
{:ok, name, value} when the field passes validation
{:error, name, error_message} when the field fails validation
Validation keeps running through all fields even if the very first
one fails. If there are errors at the end then a :bad_request
exception is thrown and is handled by the resource dispatch method,
which means no more request handling takes place.
"""
def validate_type(:integer, name, value) when is_integer(value), do: {:ok, name, value}
def validate_type(:integer, name, _), do: {:error, name, "This needs to be an integer"}
def validate_type(:string, name, value) when is_bitstring(value) do
if String.length(value) > 0 do
{:ok, name, value}
else
{:error, name, "#{name} cannot be blank"}
end
end
def validate_type(:string, name, _), do: {:error, name, "This needs to be a string"}
def validate_type(:float, name, value) when is_float(value), do: {:ok, name, value}
def validate_type(:float, name, _), do: {:error, name, "This needs to be a float"}
def validate_type(:binary, name, value) when is_binary(value), do: {:ok, name, value}
def validate_type(:binary, name, _), do: {:error, name, "This needs to be a binary"}
def validate_type(:boolean, name, value) when is_boolean(value), do: {:ok, name, value}
def validate_type(:boolean, name, _), do: {:error, name, "This needs to be a boolean"}
def validate_type({:array, _inner_type}, name, value) when is_list(value), do: {:ok, name, value}
def validate_type({:array, _inner_type}, name, _), do: {:error, name, "This needs to be an array"}
##TODO: make the time validation actually work. need to cover...
# :date
# :time
# :virtual
#
@doc """
Validate a datetime field.
By default the format is
"day/month/year hour:min:sec"
JSON serialization is configurable via the Jazz.Encoder
protocol in the encode method.
"""
def validate_type(:datetime, name, value) when is_bitstring(value) do
try do
[date, time] = String.split(value, " ")
[day, month, year] = Enum.map(String.split(date, "/"), &(String.to_integer &1))
[hour, min, sec] = Enum.map(String.split(time, ":"), &(String.to_integer &1))
{:ok, name, %Ecto.DateTime{year: year, month: month, day: day, hour: hour, min: min, sec: sec}}
rescue
_ -> {:error, name, "Invalid date format"}
end
end
def validate_type(:datetime, name, value) do
{:error, name, "This needs to be a string"}
end
def validate_type(_, name, value), do: {:ok, name, value}
@doc """
Adapt the erros that came out of the validation process to a helpful
map that will be returned to the client
"""
def make_error_message(errors) do
%{:errors => Enum.map(errors, fn {:error, name, value} -> {name, value} end) |> Enum.into(%{})}
end
@doc """
Adds the handle/1 method for each REST verb
by default a handle method for each verb is added,
but this is configurable via the options
## options
:only list of verbs that will be validated
:except list of verbs that will not be validated
"""
defmacro __using__(options) do
only = Keyword.get(options, :only, @verbs)
except = Keyword.get(options, :except, [])
only = (only -- except)
quote [unquote: false, bind_quoted: [only: only]] do
import Finch.Middleware.ModelValidator
@doc """
Provides a hook you can override for custom validation on a specific
field. The type of the field may be acceptable, but you may want to
validate another property about the field, such as a number range,
or existence of a model, etc.
"""
def validate_field(_, key, val), do: {key, val}
@doc """
An overridable hook that runs after all the other validation
that allows you to validate all fields together, ensuring that the
combination of all fields is acceptable. If it is unacceptable
then a {:bad_request, message} exception should be thrown.
"""
def validate_together(_, params, bundle), do: {params, bundle}
defp params_to_check(verb, params, field_types) do
included = Enum.filter(field_types, fn {name, _} -> not name in ignore_fields(verb) end)
Enum.map(included, fn {name, _type} -> {name, Dict.get(params, name)} end)
end
def validate({verb, conn, params, module, bundle}) do
field_types = module.model.field_types
check_params = params_to_check(verb, params, field_types)
checked = check_params
|> Enum.map(fn {key, val} -> {Keyword.fetch!(field_types, key), key, val} end)
|> Enum.map(fn {field_type, key, val} -> validate_type(field_type, key, val) end)
errors = Enum.filter(checked, fn {status, _, _} -> status == :error end)
if length(errors) > 0, do: throw {:bad_request, make_error_message(errors)}
checked = checked
|> Enum.map(fn {_, key, val} -> validate_field(verb, key, val) end)
params = Enum.into(checked, params)
{params, bundle} = validate_together(verb, params, bundle)
{verb, conn, params, module, bundle}
end
for verb <- only do
def handle({unquote(verb), conn, params, module, bundle}) do
validate({unquote(verb), conn, params, module, bundle})
end
end
def handle({verb, conn, params, module, bundle}), do: {verb, conn, params, module, bundle}
@doc """
Defines which fields are immune to validation
"""
def ignore_fields(:create), do: [:id] ++ ignore_fields(nil)
def ignore_fields(_), do: [:created, :modified]
defoverridable [
handle: 1,
validate_field: 3,
validate_together: 3,
ignore_fields: 1
]
end
end
end