defmodule Contract do @type validate_params_t :: %{required(atom()) => any()} @type error :: {:error, any()} @type validate_params :: validate_params_t | {:ok, validate_params_t} | error @type validate_result :: {:ok, %{required(atom()) => any()}} | error def plug({:ok, params}, plugs), do: plug(params, plugs) def plug({:error, _} = error, _), do: error def plug(params, plugs) do plugs |> Enum.reduce({:ok, params}, fn {plug_key, plug_fun}, {:ok, params} when is_function(plug_fun, 1) -> params |> Map.get(plug_key, :undefined) |> case do :undefined -> {:ok, params} value -> plug_fun.(value) |> case do {:ok, value} -> {:ok, params |> Map.put(plug_key, value)} other -> other end end {plug_key, plug_fun}, {:ok, params} when is_function(plug_fun, 2) -> params |> Map.get(plug_key, :undefined) |> case do :undefined -> {:ok, params} value -> plug_fun.(value, params) |> case do {:ok, value} -> {:ok, params |> Map.put(plug_key, value)} other -> other end end {_plug_key, _plug_fun}, {:error, _} = error -> error end) end def default({:ok, params}, defaults) do default(params, defaults) end def default({:error, _} = error, _) do error end def default(params, defaults) do defaults |> Enum.reduce(params, fn {key, value}, params -> params |> Map.get(key) |> case do nil -> params |> Map.put(key, value) _ -> params end end) end def cast({:ok, params}, types) do cast(params, types) end def cast({:error, _} = error, _) do error end def cast(params, types) do type_keys = types |> Map.keys() |> Enum.map(&"#{&1}") initial = params |> Enum.map(fn {key, _value} when is_atom(key) -> {key, nil} {key, _value} when is_bitstring(key) -> (key in type_keys) |> case do true -> {key |> String.to_existing_atom(), nil} false -> nil end end) |> Enum.filter(& &1) |> Enum.into(%{}) {initial, types} |> Ecto.Changeset.cast(params, Map.keys(types)) |> resolve_changeset end @spec validate(validate_params, Map.t()) :: validate_result def validate({:ok, params}, validations), do: validate(params, validations) def validate({:error, _} = error, _), do: error def validate(params, validations) do param_keys = for key <- params |> Map.keys(), do: {key, :any}, into: %{} validation_keys = for key <- validations |> Map.keys(), do: {key, :any}, into: %{} keys = Map.merge(param_keys, validation_keys) initial = params |> Enum.map(fn {key, _value} -> {key, nil} end) |> Enum.into(%{}) changeset = {initial, keys} |> Ecto.Changeset.cast(params, keys |> Map.keys()) validations |> Enum.reduce(changeset, &validate_changeset/2) |> resolve_changeset end defp resolve_changeset(changeset) do changeset |> case do %{valid?: true} = changeset -> {:ok, changeset |> Ecto.Changeset.apply_changes()} errors -> {:error, errors |> resolve} end end defp resolve(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", to_string(value)) end) end) end defp validate_changeset({key, key_validations}, changeset) when is_list(key_validations) do key_validations |> Enum.reduce(changeset, fn validation, changeset -> changeset |> do_validate(key, validation) end) end defp validate_changeset({key, key_validation}, changeset) do changeset |> do_validate(key, key_validation) end defp do_validate(changeset, key, :required) do changeset |> Ecto.Changeset.validate_required(key) end defp do_validate(changeset, key, :confirmation) do changeset |> Ecto.Changeset.validate_confirmation(key, required: true) end defp do_validate(changeset, key, fun) when is_function(fun, 1) do changeset |> Ecto.Changeset.validate_change(key, fn _, value -> fun.(value) |> case do true -> [] false -> [{key, "is invalid"}] nil -> [] error -> [{key, error}] end end) end defp do_validate(changeset, key, fun) when is_function(fun, 2) do changeset |> Ecto.Changeset.validate_change(key, fn _, value -> fun.(value, Ecto.Changeset.apply_changes(changeset)) |> case do true -> [] false -> [{key, "is invalid"}] nil -> [] error -> [{key, error}] end end) end defp do_validate(changeset, _, _) do changeset end end