Packages
ecto_flow_assertions
0.1.0
Common Ecto schema and changeset assertions, written to be easily used in pipelines.
Current section
Files
Jump to
Current section
Files
lib/changeset_a.ex
defmodule FlowAssertions.Ecto.ChangesetA do
use FlowAssertions.Define
alias FlowAssertions.Ecto.Messages
use FlowAssertions
alias Ecto.Changeset
alias FlowAssertions.MapA
@moduledoc """
Assertions for `Ecto.Changeset` structures.
"""
# ------------------------------------------------------------------------
@doc """
A pipeline-ready version of `assert changeset.valid?`
"""
defchain assert_valid(%Changeset{} = changeset) do
elaborate_assert(changeset.valid?, Messages.changeset_invalid,
expr: AssertionError.no_value,
left: changeset)
end
@doc """
A pipeline-ready version of `refute changeset.valid?`
"""
defchain assert_invalid(%Changeset{} = changeset) do
elaborate_assert(not changeset.valid?, Messages.changeset_valid,
expr: AssertionError.no_value,
left: changeset)
end
# ------------------------------------------------------------------------
@doc ~S"""
Applies `FlowAssertions.MapA.assert_fields/2` to the changes in the changeset.
To check that fields have been changed:
changeset |> assert_changes([:name, :tags])
To check specific changed values:
changeset |> assert_changes(name: "Bossie", tags: [])
"""
defchain assert_changes(%Changeset{} = changeset, keyword_list),
do: assert_fields(changeset.changes, keyword_list)
@doc """
Like `assert_changes/2` for cases where you care only about a single field.
This is just a convenience function for the grammatically obsessive.
changeset |> assert_change(:name)
changeset |> assert_change(name: "Bossie")
"""
def assert_change(cs, field_description) when not is_list(field_description),
do: assert_changes(cs, [field_description])
def assert_change(cs, field_description),
do: assert_changes(cs, field_description)
@doc """
The changeset must contain no changes.
"""
defchain assert_no_changes(%Changeset{} = changeset) do
changes = changeset.changes
elaborate_assert(changes == %{}, Messages.some_field_changes(changeset),
left: changeset)
end
@doc """
Require a changeset to have no changes in particular fields. Unmentioned fields may
have changes. When there's only a single field, it needn't be enclosed in a list.
changeset |> assert_no_changes([:name, :tags])
changeset |> assert_no_changes(:name)
"""
defchain assert_no_changes(%Changeset{} = changeset, field) when is_atom(field) do
struct_must_have_key!(changeset.data, field)
elaborate_refute(Map.has_key?(changeset.changes, field),
Messages.bad_field_change(field),
left: changeset)
end
defchain assert_no_changes(%Changeset{} = changeset, field_or_fields)
when is_list(field_or_fields),
do: Enum.map field_or_fields, &(assert_no_changes changeset, &1)
# ------------------------------------------------------------------------
@doc ~S"""
Assert that a changeset contains specific errors. In the simplest case,
it requires that each named field have at least one error, but doesn't require
any specific message:
changeset |> assert_errors([:name, :tags])
A message may also be required:
changeset
|> assert_errors(name: "may not be blank", tags: "is invalid")
The given string must be an exact match for one of the field's error messages.
If you want to check more than one error message for a given field,
enclose them in a list:
changeset
|> assert_errors(name: "may not be blank",
tags: ["is invalid", "has something else wrong"])
The list need not be a complete list of errors.
"""
defchain assert_errors(%Changeset{} = changeset, error_descriptions) do
errors_map = phoenix_errors_on(changeset)
assert_field_has_an_error = fn field ->
elaborate_assert(Map.has_key?(errors_map, field),
Messages.no_error_for_field(field),
left: changeset)
end
has_message_match? = fn expected, field_error_list ->
Enum.any?(field_error_list, fn error_message ->
good_enough?(error_message, expected)
end)
end
assert_message_match = fn field, expected ->
field_error_list = errors_map[field]
elaborate_assert(has_message_match?.(expected, field_error_list),
Messages.not_right_error_message(field),
left: field_error_list,
right: expected)
end
Enum.map(error_descriptions, fn
field when is_atom(field) ->
assert_field_has_an_error.(field)
{field, expecteds} when is_list(expecteds) ->
assert_field_has_an_error.(field)
for expected <- expecteds,
do: assert_message_match.(field, expected)
{field, expected} ->
assert_field_has_an_error.(field)
assert_message_match.(field, expected)
end)
end
@doc """
Like `assert_errors` but reads better when there's only a single error
to be checked:
assert_error(changeset, name: "is invalid")
If the message isn't to be checked, you can use a single atom:
assert_error(changeset, :name)
"""
defchain assert_error(cs, error_description) when is_atom(error_description),
do: assert_errors(cs, [error_description])
defchain assert_error(cs, error_description),
do: assert_errors(cs, error_description)
@doc """
Assert that a field or fields have no associated errors.
changeset |> assert_error_free([:in_service_datestring, :name])
You needn't use a list if there's only one field to check.
changeset |> assert_error_free(:in_service_datestring)
"""
defchain assert_error_free(changeset, field) when is_atom(field),
do: assert_error_free(changeset, [field])
defchain assert_error_free(changeset, fields) do
errors = phoenix_errors_on(changeset)
check = fn(field) ->
struct_must_have_key!(changeset.data, field)
elaborate_refute(Map.has_key?(errors, field),
Messages.unexpected_error(field),
left: changeset)
end
Enum.map(fields, check)
end
# # ------------------------------------------------------------------------
defchain assert_data(changeset, expected) do
assert_fields(changeset.data, expected)
end
@doc """
Assert that a field in the data part of a changeset matches a binding form.
changeset |> assert_data_shape(:field, %User{})
changeset |> assert_data_shape(:field, [_ | _])
"""
defmacro assert_data_shape(changeset, key, shape) do
quote do
eval_once = unquote(changeset)
assert_field_shape(eval_once.data, unquote(key), unquote(shape))
eval_once
end
end
# ----------------------------------------------------------------------------
@doc """
Operate on the single element of a list in a changeset field.
This is typically used with fields that take list values. Often,
you only want to test the empty list and a singleton list.
(When testing functions that produce their values with `Enum.map/2` or `for`,
creating a second list element gains you nothing.)
Using `with_singleton_content`, it's
convenient to apply assertions to the single element:
changeset
|> assert_valid
|> with_singleton_content(:changes, :service_gaps)
|> assert_shape(%VM.ServiceGap{})
|> Ex.Datespan.assert_datestrings(:first)
The second value can be `:data`, `:changes`, or `:newest`. The first use
their respective fields in the changeset. The last uses `Ecto.Changeset.fetch_field!/2`, meaning:
1. If the field is present in `Changeset.changes`, that value is used.
2. Otherwise, the value in `Changeset.data` is used.
If `field` does not exist or isn't an `Enum`, `with_singleton_content` will fail in
the same way `FlowAssertions.EnumA.singleton_content/1` does.
"""
def with_singleton_content(%Changeset{} = changeset, :newest, field) do
which_key =
if Map.has_key?(changeset.changes, field), do: :changes, else: :data
with_singleton_content(changeset, which_key, field)
end
def with_singleton_content(%Changeset{} = changeset, version, field) do
changeset
|> Map.get(version)
|> MapA.with_singleton_content(field)
end
# ----------------------------------------------------------------------------
# Taken from Phoenix's `test/support/data_case.ex`.
defp phoenix_errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end