The problem
------------
People want a different email per account (especially if the email
factory has a sequence), but they do this:
```elixir
build_pair(:account, email: build(:email))
```
The problem is that `build/2` is just function calling. So the above is
equivalent to this:
```elixir
email = build(:email)
build_pair(:account, email: email)
```
In other words, we get on email factory for all of the accounts.
The problem is made worse when using it with Ecto. We can imagine the
following scenario:
```elixir
insert_pair(:account, user: build(:user))
```
If the user factory has a uniqueness constraint, `insert_pair/2` will
raise an error because we'll try to insert a user with the same value
(even if using a sequence).
Solution
--------
The solution is to delay evaluation of the factory. We do this by
introducing a private struct `%ExMachina.InstanceTemplate{}` to hold the
data necessary to build the instance of that factory.
Note that this struct is private and liable to change, so users of the
library shouldn't depend on it.
The trick then lies in the `build/2` function. We make it a terminal
function in that it will evaluate any lazy factories recursively. To do
that, we update the `build/2` function to evaluate
`ExMachina.InstanceTemplate` structs after building the `build/2`
factory.
In an early implementation of this feature, I evaluated the lazy
factories when we first got the attributes in `build/2` (i.e. `attrs |>
evaluate_lazy_factories() |> Enum.into(%{})`). But then I opted for
evaluating the lazy factories after the factory is created because that
allows us to use `build_lazy/2` from within factory definitions, and not
just when combined with `build/2` or `build_*` functions.
How it interacts with "full-control" factories
---------------------------------------------
We evaluate lazy factories before passing the attrs to the factories
that have full control. That means that we can still use `build_lazy`
within `build/2` and `build_*` functions that have a factory with full
control, but we cannot use `build_lazy/2` as part of the definition of a
factory that has full control.
In other words, we can do this:
```elixir
def account_factory(attrs) do
%{
user: build(:user),
...
}
end
build(:account, build_lazy(:user))
```
But we cannot do this:
```elixir
def account_factory(attrs) do
%{
user: build_lazy(:user),
...
}
end
```
This seems like the best compromise we can make so that people can
continue using factories with full control, but without the ability to
define lazy factories in the definition -- since ExMachina does nothing
after the factory is defined.
Notes on the name `InstanceTemplate`
-----------------------------------
I call it `InstanceTemplate` because it's not exactly an instance of a
factory. I only has the recipe to build a factory in that it has the
factory `name` and the extra attributes. But, evaluating the instance
template twice when the underlying factory has a sequence will not
result in identical factories.
In other words, given the factory:
```elixir
def user_factory do
%{email: sequence("email")}
end
```
Then evaluating the same build lazy template will not give use the same
sequence:
```elixir
template = build_lazy(:user)
InstanceTemplate.evaluate(template) != InstanceTemplate.evaluate(template)
```
Potential future work
---------------------
This work opens the possibility of passing the built factory as an
argument to `build_lazy` in a factory definition, but we'd have to
modify how it works. I leave that for future work, if indeed that's
something people want.
Here's an example of how it could work:
```elixir
def account_factory do
%{
private: true,
profile: build_lazy(fn account ->
build(:profile, private: account.private
end)
}
end
```