Noted. Definitely it’s a little cryptic.
Oh yeah, the code looks much better.
I’m not convinced of this point. I’ve taken most of your ideas, but I kinda like normalize. To me parse means the data is going to change type.
Anyways, here is the revisited version with my take on @stefanchrobot and @tomekowal suggestions
defmodule Timetask.Helpers.Normalizer do
import Ecto.Changeset
@doc """
Normalizes and validates that `params` is formed according to `schema`.
## Examples
iex> Timetask.Helpers.Normalizer.normalize(%{
...> name: {:string, required: true},
...> description: :string,
...> count: {:integer, default: 10}
...> }, %{name: "only required field"})
{:ok, %{count: 10, name: "only required field"}}
iex> Timetask.Helpers.Normalizer.normalize(%{
...> name: {:string, required: true},
...> description: :string,
...> count: {:integer, default: 10}
...> }, %{"name" => "Also accepts strings as key"})
{:ok, %{count: 10, name: "Also accepts strings as key"}}
iex> normalized_params = Timetask.Helpers.Normalizer.normalize(%{
...> name: {:string, required: true},
...> description: :string,
...> count: {:integer, default: 10}
...> }, %{description: "has no name"})
...> {:error, %{errors: errors}} = normalized_params
...> assert Keyword.has_key?(errors, :name)
iex> Timetask.Helpers.Normalizer.normalize(%{
...> name: "I'm a string"
...> }, %{name: "Use atom or tuple"})
** (ArgumentError) Bad formed schema
"""
@spec normalize(map, map) :: {:error, Ecto.Changeset.t()} | {:ok, map}
def normalize(%{} = schema, %{} = params) do
normalized_schema =
for {field_name, type_spec} <- schema,
do: {field_name, apply_default_opts(type_spec)}
defaults =
for {field_name, {_type, opts}} <- normalized_schema,
default = Keyword.get(opts, :default),
into: %{},
do: {field_name, default}
types =
for {field_name, {type, _opts}} <- normalized_schema, into: %{}, do: {field_name, type}
normalized_params =
for {param_name, param_value} <- params, into: %{}, do: {to_atom(param_name), param_value}
fields = for {field_name, _} <- normalized_schema, do: field_name
required_fields =
for {field_name, {_type, opts}} <- normalized_schema,
Keyword.get(opts, :required),
do: field_name
{defaults, types}
|> cast(normalized_params, fields)
|> validate_required(required_fields)
|> apply_action(:normalize)
end
defp to_atom(name) when is_atom(name), do: name
defp to_atom(name) when is_bitstring(name), do: String.to_atom(name)
defp to_atom(_), do: raise(ArgumentError, "Bad formed schema")
@default_opts [required: false]
defp apply_default_opts(type) when is_atom(type), do: {type, @default_opts}
defp apply_default_opts({type, opts}), do: {type, Keyword.merge(@default_opts, opts)}
defp apply_default_opts(_), do: raise(ArgumentError, "Bad formed schema")
end