Packages

Code that uses Ecto needs to be tested. Such tests need the database to be populated. It pays to write a test data builder for that purpose. This package makes writing that builder easier.

Current section

Files

Jump to
Raw

lib/repo.ex

defmodule EctoTestDataBuilder.Repo do
alias EctoTestDataBuilder.Schema, as: Schema
@moduledoc """
Functions for manipulating the repo cache.
One can `fully_load/3` individual cache values, or groups of
them. "Fully loaded" is application-defined, but typically means
most or all of a schema's associations.
One can also produce `shorthand/2` references to values by
installing them as atom keys at the top level of the repo cache,
allowing:
...Repo.get(repo.bossie.id)...
"""
@doc """
Fully load all values in a list of schemas, with "fully" determined by caller.
The result is a new repo, with the values within the schemas having
been loaded from the persistent store. Typically, the values have
had some or all of their associations loaded.
There are these variants:
fully_load(repo, value_loader, schemas: [:animal, :procedure] )
fully_load(repo, value_loader, schema: :animal )
fully_load(repo, value_loader, schema: :animal, names: ["bossie"] )
fully_load(repo, value_loader, schema: :animal, name: "bossie" )
A `value_loader` is given two arguments. The first is a schema name; the
second is a value from which a query key can be extracted. The loader
most likely calls code like this:
query =
from a in Animal,
where: a.id == ^current_id,
preload: [:service_gaps, :species]
Repo.one!(query)
Each value is loaded in a separate query.
In many cases, it would be possible to use a `where a.id in [...]`
query, but that would require the loader to keep track of which
loaded value corresponds to which name/key. Loading one at a time
means you don't have to write that code (which isn't trivial in the
absence of a guarantee about order).
It is safe - a no-op - to refer to a schema that has never been created (and
consequently contains no values). Referring to a nonexistent name raises an
exception.
If `shorthand/2` has been used, the shorthand values are also updated.
"""
def fully_load(repo, loader, opts) do
case Enum.into(opts, %{}) do
%{schema: schema, names: names} ->
load_for_names_within_schema(repo, schema, names, loader)
%{schema: schema, name: name} ->
load_for_names_within_schema(repo, schema, [name], loader)
%{schema: schema} ->
load_for_all_within_schema(repo, schema, loader)
%{schemas: schemas} ->
load_for_all_within_schemas(repo, schemas, loader)
end
end
defp load_for_all_within_schemas(repo, schema_list, loader) do
Enum.reduce(schema_list, repo, fn schema, acc ->
load_for_all_within_schema(acc, schema, loader)
end)
end
defp load_for_all_within_schema(repo, schema, loader) do
names = Schema.names(repo, schema)
load_for_names_within_schema(repo, schema, names, loader)
end
defp load_for_names_within_schema(repo, schema, names, loader) do
values =
for n <- names, do: requiring_existence(repo, schema, n, &(&1))
new_values =
for v <- values, do: loader.(schema, v)
replacements =
Enum.zip(names, new_values)
repo
|> Schema.replace(schema, replacements)
|> replace_shorthand(schema, replacements)
end
@doc """
Make particular names available in a `repo.name` format.
This:
repo =
put(:animal, "bossie", %Animal{id: 5})
shorthand(schema: :animal)
allows this:
repo.bossie.id # 5
There are these variants:
shorthand(repo, schemas: [:animal, :procedure] )
shorthand(repo, schema: :animal )
shorthand(repo, schema: :animal, names: ["bossie"] )
shorthand(repo, schema: :animal, name: "bossie" )
Names are downcased and any spaces are replaced with underscores, so this:
shorthand(repo, schema: :animal, name: "Bossie the Cow")
... is attached to the repo such that `repo.bossie_the_cow` retrieves the value.
It is safe - a no-op - to refer to a schema that has never been created (and
consequently contains no values). Referring to a nonexistent name raises an
exception.
"""
def shorthand(repo, opts) do
case Enum.into(opts, %{}) do
%{schema: schema, names: names} ->
shorthand_for_names_within_schema(repo, schema, names)
%{schema: schema, name: name} ->
shorthand_for_names_within_schema(repo, schema, [name])
%{schema: schema} ->
shorthand_for_all_within_schema(repo, schema)
%{schemas: schemas} ->
shorthand_for_all_within_schemas(repo, schemas)
end
end
defp shorthand_for_all_within_schemas(repo, schema_list) do
Enum.reduce(schema_list, repo, fn schema, acc ->
shorthand_for_all_within_schema(acc, schema)
end)
end
defp shorthand_for_all_within_schema(repo, schema) do
names = Schema.names(repo, schema)
shorthand_for_names_within_schema(repo, schema, names)
end
defp shorthand_for_names_within_schema(repo, schema, names) do
Enum.reduce(names, repo, fn name, acc ->
requiring_existence(repo, schema, name, fn value ->
acc
|> remember_shorthand({schema, name})
|> install_shorthand({schema, name}, value)
end)
end)
end
defp requiring_existence(repo, schema, name, f) do
case Schema.get(repo, schema, name) do
nil ->
raise "There is no `#{inspect name}` in schema `#{inspect schema}`"
value ->
f.(value)
end
end
defp remember_shorthand(repo, {_, name} = key) do
name_atom =
name |> String.downcase |> String.replace(" ", "_") |> String.to_atom
memory = Map.put(shorthands(repo), key, name_atom)
Map.put(repo, :__shorthands__, memory)
end
defp install_shorthand(repo, schema_and_name, value) do
case get_in(repo, [:__shorthands__, schema_and_name]) do
nil ->
repo
name_atom ->
Map.put(repo, name_atom, value)
end
end
defp replace_shorthand(repo, schema, name_value_pairs) do
Enum.reduce(name_value_pairs, repo, fn {name, value}, acc ->
install_shorthand(acc, {schema, name}, value)
end)
end
defp shorthands(repo),
do: Map.get(repo, :__shorthands__, %{})
end