Current section
Files
Jump to
Current section
Files
lib/valid_field.ex
defmodule ValidField do
@moduledoc ~S"""
ValidField allows for unit testing values against a changeset.
"""
@doc """
Raises an ValidField.ValidationError when the values for the field are invalid for
the changset provided. Returns the original changset map from `with_changeset/1`
to allow subsequent calls to be piped
## Examples
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_valid_field(:first_name, ["Test"])
...> |> ValidField.assert_valid_field(:last_name, ["Value"])
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_valid_field(:first_name, [nil, ""])
** (ValidField.ValidationError) Expected the following values to be valid for "first_name": nil, ""
"""
@spec assert_valid_field(map, atom, list) :: map
def assert_valid_field(changeset, field, values) do
invalid_values =
changeset
|> map_value_assertions(field, values)
|> Enum.filter_map(fn {_key, value} -> value end, fn {key, _value} -> key end)
if invalid_values != [] do
raise ValidField.ValidationError, field: field, values: values, validity: "valid"
end
changeset
end
@doc """
Will assert if the given field's current value in the changeset is valid or not.
## Examples
iex> ValidField.with_changeset(%Model{first_name: "Test"})
...> |> ValidField.assert_valid_field(:first_name)
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_valid_field(:first_name)
** (ValidField.ValidationError) Expected the following values to be valid for "first_name": nil
"""
@spec assert_valid_field(map, atom) :: map
def assert_valid_field(changeset, field) do
assert_valid_field(changeset, field, [Map.get(changeset.data, field)])
end
@doc """
Will assert if the given fields current values in the changeset are valid or not.
Only returns an error on the first occurance, doesn't collect.
## Examples
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_valid_fields([:first_name, :last_name])
iex> ValidField.with_changeset(%Model{first_name: "Test", last_name: "Something"})
...> |> ValidField.assert_valid_fields([:first_name, :last_name])
** (ValidField.ValidationError) Expected the following values to be valid for "first_name": nil
"""
@spec assert_valid_fields(map, list) :: map
def assert_valid_fields(changeset, fields) when is_list(fields) do
Enum.each(fields, &(assert_valid_field(changeset, &1)))
end
@doc """
Raises an ValidField.ValidationError when the values for the field are valid for
the changset provided. Returns the original changset map from `with_changeset/1`
to allow subsequent calls to be piped
## Examples
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_invalid_field(:first_name, [nil])
...> |> ValidField.assert_invalid_field(:first_name, [""])
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_invalid_field(:first_name, ["Test"])
** (ValidField.ValidationError) Expected the following values to be invalid for "first_name": "Test"
"""
@spec assert_invalid_field(map, atom, list) :: map
def assert_invalid_field(changeset, field, values) do
valid_values =
changeset
|> map_value_assertions(field, values)
|> Enum.filter_map(fn {_key, value} -> !value end, fn {key, _value} -> key end)
if valid_values != [] do
raise ValidField.ValidationError, field: field, values: valid_values, validity: "invalid"
end
changeset
end
@doc """
Will assert if the given field's current value in the changeset is invalid or not.
## Examples
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_invalid_field(:first_name)
iex> ValidField.with_changeset(%Model{first_name: "Test"})
...> |> ValidField.assert_invalid_field(:first_name)
** (ValidField.ValidationError) Expected the following values to be invalid for "first_name": "Test"
"""
@spec assert_invalid_field(map, atom) :: map
def assert_invalid_field(changeset, field) do
assert_invalid_field(changeset, field, [Map.get(changeset.data, field)])
end
@doc """
Will assert if the given fields current values in the changeset are invalid or not.
Only returns an error on the first occurance, doesn't collect.
## Examples
iex> ValidField.with_changeset(%Model{})
...> |> ValidField.assert_invalid_fields([:first_name])
iex> ValidField.with_changeset(%Model{first_name: "Test"})
...> |> ValidField.assert_invalid_fields([:first_name])
** (ValidField.ValidationError) Expected the following values to be invalid for "first_name": "Test"
"""
@spec assert_invalid_fields(map, list) :: map
def assert_invalid_fields(changeset, fields) when is_list(fields) do
Enum.each fields, fn(field) -> assert_invalid_field(changeset, field) end
end
@doc """
Combines `assert_valid_field/3` and `assert_invalid_field/3` into a single call.
The third argument is the collection of valid values to be tested. The fourth argument
is the collection of invalid values to be tested.
## Examples
ValidField.with_changeset(%Model{})
|> ValidField.assert_field(:first_name, ["George", "Barry"], ["", nil])
"""
@spec assert_field(map, atom, list, list) :: map
def assert_field(changeset, field, valid_values, invalid_values) do
changeset
|> assert_valid_field(field, valid_values)
|> assert_invalid_field(field, invalid_values)
end
@doc """
Returns a changeset map to be used with `assert_valid_field/3` or
`assert_invalid_field/3`. When with_changeset is passed a single arguments, it is
assumed to be an Ecto Model struct and will call the `changeset` function on
the struct's module
## Examples
ValidField.with_changeset(%Model{})
|> ValidField.assert_invalid_field(:first_name, [nil])
|> ValidField.assert_invalid_field(:first_name, [""])
"""
@spec with_changeset(Ecto.Model.t) :: map
def with_changeset(model),
do: with_changeset(model, &model.__struct__.changeset/2)
@doc """
Returns a changeset map to be used with `assert_valid_field/3` or
`assert_invalid_field/3`. The function passed to `with_changeset/2` must accept two
arguments, the first being the model provided to `with_changeset/2`, the second
being the map of properties to be applied in the changeset.
## Examples
ValidField.with_changeset(%Model{}, &Model.changeset/2)
|> ValidField.assert_invalid_field(:first_name, [nil])
|> ValidField.assert_invalid_field(:first_name, [""])
"""
@spec with_changeset(Ecto.Model.t, function) :: map
def with_changeset(model, func) when is_function(func),
do: %{data: model, changeset_func: func}
@doc """
Add values that will be set on the changeset during assertion runs
"""
@spec put_params(map, map) :: map
def put_params(changeset, params) when is_map(changeset) do
Map.put(changeset, :params, params)
end
defp map_value_assertions(changeset, field, values) do
values
|> Enum.map(&({&1, invalid_for?(changeset, field, &1)}))
end
defp invalid_for?(%{data: model, params: params, changeset_func: changeset}, field, value) do
params =
params
|> Map.put(field, value)
|> stringify_keys()
changeset.(model, params).errors
|> Keyword.has_key?(field)
end
defp invalid_for?(%{data: model, changeset_func: changeset}, field, value),
do: invalid_for?(%{params: %{}, data: model, changeset_func: changeset}, field, value)
defp invalid_for?(changeset, field, _value),
do: Keyword.has_key?(changeset.errors, field)
defp stringify_field(field) when is_atom(field),
do: Atom.to_string(field)
defp stringify_field(field) when is_binary(field), do: field
defp stringify_keys(%{__struct__: _struct} = struct), do: struct
defp stringify_keys(map) when is_map(map),
do: Enum.into(map, %{}, fn({key, value}) ->
{stringify_field(key), stringify_keys(value)}
end)
defp stringify_keys(value), do: value
end