Current section

Files

Jump to
filtr lib filtr.ex
Raw

lib/filtr.ex

defmodule Filtr do
@moduledoc false
alias Filtr.Helpers
@type_plugin_map Helpers.type_plugin_map()
@spec run(schema :: map(), params :: map()) :: map()
@spec run(schema :: map(), params :: map(), opts :: keyword()) :: map()
def run(schema, params, run_opts \\ []) do
Map.new(schema, fn
{key, nested_schema} when is_map(nested_schema) ->
values = get_value(params, key)
{key, run(nested_schema, values, run_opts)}
{key, opts} ->
opts = Keyword.merge(run_opts, opts)
{type, opts} = Keyword.pop!(opts, :type)
case type do
{:list, nested_schema} when is_map(nested_schema) ->
values = get_value(params, key)
{key, run(nested_schema, values, run_opts)}
{:list, type} ->
values =
params
|> get_value(key)
|> Enum.map(fn value ->
key
|> process_value(value, type, opts)
|> elem(1)
end)
{key, values}
type ->
value = get_value(params, key)
process_value(key, value, type, opts)
end
end)
end
defp process_value(key, value, type, opts) do
with {:ok, value} <- cast(key, value, type, opts),
{:ok, value} <- validate(key, value, type, opts) do
{key, value}
else
error -> {key, error}
end
end
defp cast(key, value, cast_fn, opts) when is_function(cast_fn, 2) do
case cast_fn.(value, opts) do
{:ok, value} -> {:ok, value}
{:error, errors} when is_list(errors) -> process_errors_with_mode(key, errors, opts)
{:error, error} -> process_error_with_mode(key, error, opts)
value -> {:ok, value}
end
end
defp cast(_key, value, :__none__, _opts), do: {:ok, value}
defp cast(_key, value, nil, _opts), do: {:ok, value}
defp cast(key, nil, _type, opts) do
validators = Keyword.get(opts, :validators, [])
default = Keyword.get(validators, :default, :__none__)
required? = Keyword.get(validators, :required, false)
if required? and default == :__none__ do
process_error_with_mode(key, "required", opts)
else
{:ok, default_value(default)}
end
end
defp cast(key, value, type, opts) do
plugins = @type_plugin_map[type]
if is_nil(plugins) do
process_error_with_mode(key, "unsupported type - #{type}", opts)
else
case plugin_cast(plugins, value, type, opts) do
{:ok, value} -> {:ok, value}
{:error, error} -> process_error_with_mode(key, error, opts)
end
end
end
defp default_value(default) when is_function(default, 0), do: default.()
defp default_value(:__none__), do: nil
defp default_value(default), do: default
defp validate(key, value, type, opts) do
validators = Keyword.get(opts, :validators, [])
errors =
validators
|> Keyword.drop([:default, :required])
|> Enum.map(fn
{:custom, func} when is_function(func, 3) ->
func.(value, type, opts)
{:custom, func} when is_function(func, 2) ->
func.(value, type)
{:custom, func} when is_function(func, 1) ->
func.(value)
validator ->
plugins = @type_plugin_map[type]
plugin_validate(plugins, value, type, validator, opts)
end)
|> process_validator_results()
case errors do
[] -> {:ok, value}
errors -> process_errors_with_mode(key, errors, opts)
end
end
defp process_validator_results(results) do
Enum.reduce(results, [], fn result, acc ->
case result do
true -> acc
:ok -> acc
{:ok, _} -> acc
false -> acc ++ ["invalid value"]
:error -> acc ++ ["invalid value"]
{:error, error} -> acc ++ [error]
end
end)
end
defp process_error_with_mode(key, error, opts) do
error_mode = Keyword.get_lazy(opts, :error_mode, fn -> Helpers.default_error_mode() end)
validators = Keyword.get(opts, :validators, [])
default = Keyword.get(validators, :default, :__none__)
case error_mode do
:fallback -> {:ok, default_value(default)}
:strict -> {:error, [error]}
:raise -> raise "Invalid value for #{key}: #{error}"
end
end
defp process_errors_with_mode(key, errors, opts) do
error_mode = Keyword.get_lazy(opts, :error_mode, fn -> Helpers.default_error_mode() end)
validators = Keyword.get(opts, :validators, [])
default = Keyword.get(validators, :default, :__none__)
case error_mode do
:fallback -> {:ok, default_value(default)}
:strict -> {:error, Enum.uniq(errors)}
:raise -> raise "Invalid value for #{key}: #{Enum.join(Enum.uniq(errors), ", ")}"
end
end
defp get_value(params, key) do
Map.get(params, to_string(key)) || Map.get(params, key)
end
defp plugin_cast(plugins, value, type, opts) do
Enum.reduce_while(plugins, {:error, "missing cast for #{type}"}, fn plugin, result ->
try do
plugin.cast(value, type, opts)
catch
FunctionClauseError -> {:cont, result}
else
result -> {:halt, result}
end
end)
end
defp plugin_validate(plugins, value, type, validator, opts) do
Enum.reduce_while(plugins, {:error, "missing validate for #{type}, #{inspect(validator)}"}, fn plugin, result ->
try do
plugin.validate(value, type, validator, opts)
catch
FunctionClauseError -> {:cont, result}
else
result -> {:halt, result}
end
end)
end
end