Skip to content

Instantly share code, notes, and snippets.

@luisgabrielroldan
Last active October 16, 2019 18:25
Show Gist options
  • Save luisgabrielroldan/c367aca9e167e52fbc08c0a0010169b7 to your computer and use it in GitHub Desktop.
Save luisgabrielroldan/c367aca9e167e52fbc08c0a0010169b7 to your computer and use it in GitHub Desktop.
Parameters validation based on Ecto schemaless validations
defmodule ParamsValidator do
@moduledoc false
alias Ecto.Changeset
@type rule ::
{:required, boolean()}
| {:length, ecto_length_opts :: list()}
| {:number, ecto_number_opts :: list()}
@type settings_list :: list({field :: term(), rules :: list(rule())})
@type field_error ::
%{field: term(), message: String.t()}
@spec validate(params :: map(), settings :: settings_list()) ::
{:ok, map()} | {:error, list(field_error)}
@doc """
Validate attributes on a map based on a keywordlist with rules
## Examples
iex> ParamsValidator.validate(%{
id: "",
name: "too long"
}, [
id: [type: :integer, required: true],
name: [type: :string, required: true, length: [max: 3]]
])
{:error, [
%{field: :name, message: "should be at most 3 character(s)"},
%{field: :id, message: "can't be blank"}
]}
"""
def validate(params, settings)
when is_map(params) and is_list(settings) do
case parse_settings(settings) do
{:ok, types_map, validation_fun} ->
{%{}, types_map}
|> Changeset.cast(params, Map.keys(types_map))
|> validation_fun.()
|> handle_validation_result()
error ->
error
end
end
defp handle_validation_result(%{valid?: true, changes: data}),
do: {:ok, data}
defp handle_validation_result(changeset) do
errors =
changeset
|> Changeset.traverse_errors(fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
|> Enum.reduce([], fn {field, messages}, acc ->
Enum.reduce(messages, acc, fn msg, acc1 ->
[%{field: field, message: msg} | acc1]
end)
end)
{:error, errors}
end
defp parse_settings(settings),
do: parse_settings(settings, {[], settings})
defp parse_settings([], {filters, settings}) do
{types_map, filters1} = extract_types(settings, filters)
case validation_pipeline(filters1) do
{:ok, validation_fun} ->
{:ok, types_map, validation_fun}
error ->
error
end
end
defp parse_settings([{field, opts} | rest], {filters, settings}) do
filters = parse_field_opts(opts, field, filters)
parse_settings(rest, {filters, settings})
end
defp extract_types(settings, filters) do
types = Map.new(settings, fn {field, _} -> {field, :any} end)
Enum.reduce(filters, {types, []}, fn
{:type, {field, type}}, {types, filters1} ->
{Map.put(types, field, type), filters1}
other, {types, filters1} ->
{types, [other | filters1]}
end)
end
defp validation_pipeline(filters, fun \\ & &1)
defp validation_pipeline([], fun),
do: {:ok, fun}
defp validation_pipeline([{:required, {field, required}} | rest], prev) do
if required do
validation_pipeline(rest, fn changeset ->
Changeset.validate_required(prev.(changeset), field)
end)
else
validation_pipeline(rest, prev)
end
end
defp validation_pipeline([{filter, {field, opts}} | rest], prev)
when filter in [:number, :length] do
method = String.to_atom("validate_#{filter}")
validation_pipeline(rest, fn changeset ->
apply(Changeset, method, [prev.(changeset), field, opts])
end)
end
defp validation_pipeline([{filter, {_field, _opts}} | _rest], _prev),
do: {:error, {:invalid_setting, filter}}
defp parse_field_opts([], _field, filters),
do: filters
defp parse_field_opts([{filter, opts} | rest], field, filters),
do: parse_field_opts(rest, field, [{filter, {field, opts}} | filters])
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment