Last active
October 16, 2019 18:25
-
-
Save luisgabrielroldan/c367aca9e167e52fbc08c0a0010169b7 to your computer and use it in GitHub Desktop.
Parameters validation based on Ecto schemaless validations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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