diff --git a/lib/ex_machina.ex b/lib/ex_machina.ex index a0b4fd0..4af6fd2 100644 --- a/lib/ex_machina.ex +++ b/lib/ex_machina.ex @@ -7,7 +7,10 @@ defmodule ExMachina do use Application alias ExMachina.UndefinedFactoryError + alias ExMachina.UndefinedGeneratorError + @callback generate(generator_name :: atom) :: any + @callback generate(generator_name :: atom, attrs :: keyword | map) :: any @callback build(factory_name :: atom) :: any @callback build(factory_name :: atom, attrs :: keyword | map) :: any @callback build_list(number_of_records :: integer, factory_name :: atom) :: list @@ -38,6 +41,12 @@ defmodule ExMachina do alias ExMachina.UndefinedFactoryError + if Code.ensure_loaded?(StreamData) do + def generate(generator_name, attrs \\ %{}) do + ExMachina.generate(__MODULE__, generator_name, attrs) + end + end + def build(factory_name, attrs \\ %{}) do ExMachina.build(__MODULE__, factory_name, attrs) end @@ -217,7 +226,8 @@ defmodule ExMachina do def build(module, factory_name, attrs \\ %{}) do attrs = Enum.into(attrs, %{}) - function_name = build_function_name(factory_name) + function_name = build_factory_name(factory_name) + generator_name = build_generator_name(factory_name) cond do factory_accepting_attributes_defined?(module, function_name) -> @@ -229,19 +239,49 @@ defmodule ExMachina do |> merge_attributes(attrs) |> evaluate_lazy_attributes() + Code.ensure_loaded?(StreamData) and + (generator_accepting_attributes_defined?(module, generator_name) or + generator_without_attributes_defined?(module, generator_name)) -> + module |> generate(factory_name, attrs) |> Enum.take(1) |> hd + true -> raise UndefinedFactoryError, factory_name end end - defp build_function_name(factory_name) do - factory_name + if Code.ensure_loaded?(StreamData) do + def generate(module, generator_name, attrs \\ %{}) do + attrs = Enum.into(attrs, %{}) + + function_name = build_generator_name(generator_name) + + cond do + generator_accepting_attributes_defined?(module, function_name) -> + apply(module, function_name, [attrs]) + + generator_without_attributes_defined?(module, function_name) -> + module + |> apply(function_name, []) + |> StreamData.map(&merge_attributes(&1, attrs)) + + true -> + raise UndefinedGeneratorError, generator_name + end + end + end + + defp build_function_name(name, postfix) do + name |> Atom.to_string() - |> Kernel.<>("_factory") + |> then(&"#{&1}_#{postfix}") # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom |> String.to_atom() end + defp build_factory_name(factory_name) do + build_function_name(factory_name, "factory") + end + defp factory_accepting_attributes_defined?(module, function_name) do Code.ensure_loaded?(module) && function_exported?(module, function_name, 1) end @@ -250,6 +290,20 @@ defmodule ExMachina do Code.ensure_loaded?(module) && function_exported?(module, function_name, 0) end + if Code.ensure_loaded?(StreamData) do + defp build_generator_name(factory_name) do + build_function_name(factory_name, "generator") + end + + defp generator_accepting_attributes_defined?(module, function_name) do + Code.ensure_loaded?(module) && function_exported?(module, function_name, 1) + end + + defp generator_without_attributes_defined?(module, function_name) do + Code.ensure_loaded?(module) && function_exported?(module, function_name, 0) + end + end + @doc """ Helper function to merge attributes into a factory that could be either a map or a struct. @@ -347,12 +401,23 @@ defmodule ExMachina do build_list(3, :user) """ def build_list(module, number_of_records, factory_name, attrs \\ %{}) do - stream = - Stream.repeatedly(fn -> - ExMachina.build(module, factory_name, attrs) - end) - - Enum.take(stream, number_of_records) + function_name = build_factory_name(factory_name) + generator_name = build_generator_name(factory_name) + + if not factory_accepting_attributes_defined?(module, function_name) and + not factory_without_attributes_defined?(module, function_name) and + Code.ensure_loaded?(StreamData) and + (generator_accepting_attributes_defined?(module, generator_name) or + generator_without_attributes_defined?(module, generator_name)) do + module |> generate(factory_name, attrs) |> Enum.take(number_of_records) + else + stream = + Stream.repeatedly(fn -> + ExMachina.build(module, factory_name, attrs) + end) + + Enum.take(stream, number_of_records) + end end defmacro __before_compile__(_env) do diff --git a/lib/ex_machina/undefined_generator_error.ex b/lib/ex_machina/undefined_generator_error.ex new file mode 100644 index 0000000..f0cc153 --- /dev/null +++ b/lib/ex_machina/undefined_generator_error.ex @@ -0,0 +1,23 @@ +if Code.ensure_loaded?(StreamData) do + defmodule ExMachina.UndefinedGeneratorError do + @moduledoc """ + Error raised when trying to build or create a generator that is undefined. + """ + + defexception [:message] + + def exception(generator_name) do + message = """ + No generator defined for #{inspect(generator_name)}. + + Please check for typos or define your generator: + + def #{generator_name}_generator do + ... + end + """ + + %__MODULE__{message: message} + end + end +end diff --git a/mix.exs b/mix.exs index 705ac23..1931373 100644 --- a/mix.exs +++ b/mix.exs @@ -34,6 +34,7 @@ defmodule ExMachina.Mixfile do [ {:ecto, "~> 2.2 or ~> 3.0", optional: true}, {:ecto_sql, "~> 3.0", optional: true}, + {:stream_data, "~> 1.2", optional: true}, # Dev and Test dependencies {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index c192c60..3f7262b 100644 --- a/mix.lock +++ b/mix.lock @@ -18,5 +18,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, }