Current section

Files

Jump to
ex_machina lib ex_machina strategy.ex
Raw

lib/ex_machina/strategy.ex

defmodule ExMachina.Strategy do
@moduledoc ~S"""
Module for making new strategies for working with factories
## Example
defmodule MyApp.JsonEncodeStrategy do
# The function_name will be used to generate functions in your factory
# This example adds json_encode/1, json_encode/2, json_encode/3,
# json_encode_pair/2 and json_encode_list/3
use ExMachina.Strategy, function_name: :json_encode
# Define a function for handling the records.
# Takes the form of "handle_#{function_name}"
def handle_json_encode(record, %{encoder: encoder}) do
encoder.encode!(record)
end
# Optionally, define a function for handling records and taking in
# options at the function level
def handle_json_encode(record, %{encoder: encoder}, encoding_opts) do
encoder.encode!(record, encoding_opts)
end
end
defmodule MyApp.JsonFactory do
use ExMachina
use MyApp.JsonEncodeStrategy, encoder: Poison
def user_factory do
%User{name: "John"}
end
end
# Will build and then return a JSON encoded version of the user.
MyApp.JsonFactories.json_encode(:user)
The arguments sent to the handling function are
1. The built record
2. The options passed to the strategy
3. The options passed to the function as a third argument
The options sent as the second argument are always converted to a map. The
options are anything you passed when you `use` your strategy in your factory,
merged together with `%{factory_module: FactoryItWasCalledFrom}`.
This allows for customizing the strategy, and for calling other functions on
the factory if needed.
See `ExMachina.EctoStrategy` in the ExMachina repo, and the docs for
`name_from_struct/1` for more examples.
The options sent as the third argument come directly from the options passed
to the function being called. These could be function-level overrides of the
options passed when you `use` the strategy, or they could be other
customizations needed at the level of the function.
See `c:ExMachina.Ecto.insert/3` for an example.
"""
@doc false
defmacro __using__(function_name: function_name) do
quote do
@doc false
def function_name, do: unquote(function_name)
defmacro __using__(opts) do
custom_strategy_module = __MODULE__
function_name = custom_strategy_module.function_name()
handle_response_function_name = :"handle_#{function_name}"
quote do
def unquote(function_name)(already_built_record, function_opts)
when is_map(already_built_record) do
opts =
Map.new(unquote(opts))
|> Map.merge(%{factory_module: __MODULE__})
apply(
unquote(custom_strategy_module),
unquote(handle_response_function_name),
[already_built_record, opts, function_opts]
)
end
def unquote(function_name)(already_built_record) when is_map(already_built_record) do
opts = Map.new(unquote(opts)) |> Map.merge(%{factory_module: __MODULE__})
apply(
unquote(custom_strategy_module),
unquote(handle_response_function_name),
[already_built_record, opts]
)
end
def unquote(function_name)(factory_name, attrs, opts) do
record = ExMachina.build(__MODULE__, factory_name, attrs)
unquote(function_name)(record, opts)
end
def unquote(function_name)(factory_name, attrs) do
record = ExMachina.build(__MODULE__, factory_name, attrs)
unquote(function_name)(record)
end
def unquote(function_name)(factory_name) do
record = ExMachina.build(__MODULE__, factory_name, %{})
unquote(function_name)(record)
end
def unquote(:"#{function_name}_pair")(factory_name, attrs, opts) do
unquote(:"#{function_name}_list")(2, factory_name, attrs, opts)
end
def unquote(:"#{function_name}_pair")(factory_name, attrs \\ %{}) do
unquote(:"#{function_name}_list")(2, factory_name, attrs)
end
def unquote(:"#{function_name}_list")(number_of_records, factory_name, attrs, opts) do
Stream.repeatedly(fn ->
unquote(function_name)(factory_name, attrs, opts)
end)
|> Enum.take(number_of_records)
end
def unquote(:"#{function_name}_list")(number_of_records, factory_name, attrs \\ %{}) do
Stream.repeatedly(fn ->
unquote(function_name)(factory_name, attrs)
end)
|> Enum.take(number_of_records)
end
end
end
end
end
defmacro __using__(opts) do
raise """
expected function_name as an option, instead got #{inspect(opts)}.
Example: use ExMachina.Strategy, function_name: :json_encode
"""
end
@doc ~S"""
Returns the factory name from a struct. Useful for strategies with callbacks.
This function can be useful when you want to call other functions based on the
type of struct passed in. For example, if you wanted to call a function on the
factory module before JSON encoding.
## Examples
ExMachina.Strategy.name_from_struct(%User{}) # Returns :user
ExMachina.Strategy.name_from_struct(%MyUser{}) # Returns :my_user
ExMachina.Strategy.name_from_struct(%MyApp.MyTask{}) # Returns :my_task
## Implementing callback functions with name_from_struct/1
defmodule MyApp.JsonEncodeStrategy do
use ExMachina.Strategy, function_name: :json_encode
def handle_json_encode(record, %{factory_module: factory_module}) do
# If the record was a %User{} this would return :before_encode_user
callback_func_name = :"before_encode_#{ExMachina.Strategy.name_from_struct(record)}"
if callback_defined?(factory_module, callback_func_name) do
# First call the callback function
apply(factory_module, callback_func_name, [record])
# Then encode it
|> Poison.encode!
else
# Otherwise, encode it without calling any callback
Poison.encode!(record)
end
end
defp callback_defined?(module, func_name) do
Code.ensure_loaded?(module) && function_exported?(module, func_name, 1)
end
end
"""
@spec name_from_struct(struct) :: atom
def name_from_struct(%{__struct__: struct_name} = _struct) do
struct_name
|> Module.split()
|> List.last()
|> Macro.underscore()
|> String.downcase()
|> String.to_atom()
end
end