Current section
Files
Jump to
Current section
Files
lib/ex_zample.ex
defmodule ExZample do
@moduledoc """
ExZample is a factory library based on Elixir behaviours.
"""
@doc """
Invoked every time you build your data using `ExZample` module.
You need to return a struct with example values.
This callback is optional when the module given is a struct. It will use
the struct default values if no callback is given.
"""
@callback example() :: struct
@type factory :: module
@optional_callbacks example: 0
@doc """
Creates `aliases` for your factories to simplify the call.
A `aliases` should be a map with atom keys and values as `factory` compatible
modules. If you call with repeated keys this function will fail. This function
is ideal to be called once, for example in your `test_helper.ex` file.
## Examples
iex> ExZample.add_aliases(%{user: Factories.User})
...> ExZample.build(:user)
%User{age: 21, email: "test@test.test", first_name: "First Name", id: 1, last_name: "Last Name"}
"""
@spec add_aliases(%{required(atom) => factory}) :: :ok
def add_aliases(aliases) when is_map(aliases), do: add_aliases(:global, aliases)
@doc """
Same as `add_aliases/1`, but you can define a different scope.
This function is specially useful for umbrella apps where each app can define
their factories without leaking any aliases to other apps. You can enforce the
current scope with `ex_zample/1`.
## Examples
iex> ExZample.add_aliases(:my_app, %{user: Factories.User})
...> ExZample.ex_zample(%{ex_zample_scope: :my_app})
...> ExZample.build(:user)
%User{age: 21, email: "test@test.test", first_name: "First Name", id: 1, last_name: "Last Name"}
"""
@spec add_aliases(atom, %{required(atom) => factory}) :: :ok
def add_aliases(scope, aliases) when is_map(aliases) do
current_config = get_config(scope)
new_config =
Map.merge(current_config, aliases, fn factory_alias, current_factory, new_factory ->
if current_factory == new_factory do
current_factory
else
raise ArgumentError, """
The alias #{inspect(factory_alias)} already exists!
It is registered with the factory #{inspect(current_factory)} and
can't be replaced with the new #{inspect(new_factory)} in #{inspect(scope)} scope.
Rename the alias or add it in a different scope.
"""
end
end)
put_config(scope, new_config)
:ok
end
defp get_config(scope), do: Application.get_env(:ex_zample, scope) || %{}
defp put_config(scope, config), do: Application.put_env(:ex_zample, scope, config)
@doc """
Builds a struct with given `factory_or_alias` module.
If the given factory exports the `example/0` function it will use to return
the struct and its values. Otherwise, if the module is a struct it will use
its default values.
If will override the generated data with the given `attrs`.
## Examples
iex> ExZample.build(User)
%ExZample.User{}
iex> ExZample.build(Factories.User)
%ExZample.User{age: 21, email: "test@test.test", first_name: "First Name", id: 1, last_name: "Last Name"}
iex> ExZample.build(:book)
%ExZample.Book{code: "1321", title: "The Book's Title"}
iex> ExZample.build(User, age: 45)
%ExZample.User{age: 45}
iex> ExZample.build(Factories.User, age: 45)
%ExZample.User{age: 45, email: "test@test.test", first_name: "First Name", id: 1, last_name: "Last Name"}
iex> ExZample.build(:book, code: "007")
%ExZample.Book{code: "007", title: "The Book's Title"}
"""
@spec build(factory, Enum.t() | nil) :: struct
def build(factory_or_alias, attrs \\ nil) when is_atom(factory_or_alias) do
data = try_factory(factory_or_alias) || try_alias(factory_or_alias)
if attrs, do: struct!(data, attrs), else: data
end
defp try_factory(factory) do
cond do
function_exported?(factory, :example, 0) ->
factory.example()
function_exported?(factory, :__struct__, 1) ->
struct!(factory)
true ->
nil
end
end
defp try_alias(factory_alias) do
scope = lookup_scope()
if factory = get_config(scope)[factory_alias] do
inspected_argument = inspect(factory)
try_factory(factory) ||
raise ArgumentError,
message: """
#{inspected_argument} is not a factory
If #{inspected_argument} is a module, you need to create a `example/0` function
If #{inspected_argument} is a alias, you need to register it with `ExZample.add_aliases/1`
"""
else
raise ArgumentError,
message:
"There's no alias registered for #{inspect(factory_alias)} in #{inspect(scope)} scope"
end
end
defp lookup_scope do
if scope = Process.get(:ex_zample_scope) do
scope
else
scope = lookup_in_processes(:"$callers") || lookup_in_processes(:"$ancestors") || :global
# NOTE: Faster future lookups
Process.put(:ex_zample_scope, scope)
scope
end
end
defp lookup_in_processes(key),
do: key |> Process.get() |> List.wrap() |> Enum.find_value(&get_process_scope/1)
defp get_process_scope(name_or_pid) do
pid = if is_atom(name_or_pid), do: Process.whereis(name_or_pid), else: name_or_pid
{:dictionary, dictionary} = Process.info(pid, :dictionary)
dictionary[:ex_zample_scope]
end
@doc """
Same as `build/2`, but returns a list with where the size is the given
`count`.
## Examples
iex> ExZample.build_list(3, User)
[%ExZample.User{}, %ExZample.User{}, %ExZample.User{}]
iex> ExZample.build_list(3, :book)
[%ExZample.Book{},%ExZample.Book{}, %ExZample.Book{}]
iex> ExZample.build_list(3, User, age: 45)
[%ExZample.User{age: 45}, %ExZample.User{age: 45}, %ExZample.User{age: 45}]
iex> ExZample.build_list(3, :book, code: "007")
[%ExZample.Book{code: "007"},%ExZample.Book{code: "007"}, %ExZample.Book{code: "007"}]
"""
@spec build_list(count :: pos_integer, factory, attrs :: Enum.t() | nil) :: [struct]
def build_list(count, factory, attrs \\ nil)
def build_list(0, _factory, _attrs), do: []
def build_list(count, factory, attrs) when is_integer(count) and count > 0,
do: Enum.map(1..count, fn _ -> build(factory, attrs) end)
@doc """
Same as `build/2`, but returns a tuple with a pair of structs.
## Examples
iex> ExZample.build_pair(User)
{%ExZample.User{}, %ExZample.User{}}
iex> ExZample.build_pair(:book)
{%ExZample.Book{},%ExZample.Book{}}
iex> ExZample.build_pair(User, age: 45)
{%ExZample.User{age: 45}, %ExZample.User{age: 45}}
iex> ExZample.build_pair(:book, code: "007")
{%ExZample.Book{code: "007"},%ExZample.Book{code: "007"}}
"""
@spec build_pair(factory, attrs :: Enum.t() | nil) :: {struct, struct}
def build_pair(factory, attrs \\ nil), do: {build(factory, attrs), build(factory, attrs)}
@doc """
Utiliy function that you can define the scope that `ExZample` will look
for the aliases. If no scope is defined, `:global` is the default
scope.
This function works well with `setup/1` callback of `ExUnit` and `@tags`.
For example:
defmodule MyTest do
use ExUnit.Case
import ExZample
@moduletag %{ex_zample_scope: :my_app}
setup :ex_zample
test "returns a user" do
assert %User{} == build(:user)
end
end
In the example above, `ExZample` will look for a factory registered in alias
`:user` in the `:my_app` scope.
"""
@spec ex_zample(map) :: :ok
def ex_zample(scope) when is_map(scope) do
if ex_zample_scope = scope[:ex_zample_scope] do
Process.put(:ex_zample_scope, ex_zample_scope)
else
Process.put(:ex_zample_scope, :global)
end
:ok
end
end