defmodule EctoFactory do import Enum, only: [random: 1] import Application, only: [get_env: 2] @doc """ Create a struct of the passed in factory After configuring a factory config :ecto_factory, factories: [ user: User, user_with_default_username: { User, username: "mrmicahcooper" } ] You can build a struct with the attributes from your factory as defaults. EctoFactory.schema(:user_with_default_username) %User{ age: 124309132# random number username: "mrmicahcooper" } And you can pass in your own attributes of course: EctoFactory.schema(:user, age: 99, username: "hashrocket") %User{ age: 99, username: "hashrocket" } """ @spec schema(atom() | Ecto.Schema, keyword() | map()) :: Ecto.Schema def schema(factory_name, attrs \\ []) do {schema, attributes} = build_attrs(factory_name, attrs) struct(schema, attributes) end @doc """ Create a map with randomly generated of the passed in Ecto schema. The keys of this map are `atoms` """ @spec build(atom() | Ecto.Schema, keyword() | map()) :: map() def build(factory_name, attrs \\ []) do build_attrs(factory_name, attrs) |> elem(1) |> Enum.into(%{}) end @doc """ Create a map with randomly generated of the passed in Ecto schema. The keys of this map are `String`s """ @spec attrs(atom() | Ecto.Schema, keyword() | map()) :: map() def attrs(factory_name, attrs \\ []) do build_attrs(factory_name, attrs) |> elem(1) |> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end) end # Generators for the standard ecto types @doc """ Generate a random value. `gen/1` will return a value of the atom or tuple to pass it. """ @spec gen(atom() | tuple()) :: integer() | binary() | float() | boolean() | list() | Time | NaiveDateTime | Date | DateTime def gen(:id), do: random(1..9_999_999) def gen(:binary_id), do: Ecto.UUID.generate() def gen(:integer), do: random(1..9_999_999) def gen(:float), do: (random(99..99999) / random(1..99)) |> Float.round(2) def gen(:decimal), do: gen(:float) def gen(:boolean), do: random([true, false]) def gen(:binary), do: Ecto.UUID.generate() def gen(:date), do: Date.utc_today() def gen(:time), do: Time.utc_now() def gen(:time_usec), do: gen(:time) def gen(:naive_datetime) do NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) end def gen(:naive_datetime_usec), do: gen(:naive_datetime) def gen(:utc_datetime), do: DateTime.utc_now() def gen(:utc_datetime_usec), do: gen(:utc_datetime) def gen(:string), do: random_string(20) def gen(:array), do: gen({:array, :string}) def gen({:array, type}), do: 1..random(2..9) |> Enum.map(fn _ -> gen(type) end) def gen(:map), do: gen({:map, :string}) def gen({:map, type}) do for(key <- gen({:array, :string}), into: %{}, do: {key, gen(type)}) end # Special generators for helpful things def gen(:email), do: "#{gen(:string)}@#{gen(:string)}.#{gen(:string)}" def gen(:url) do host = random_string(3..6) <> "." <> random_string(4..8) tld = ~w[com co.uk ninja ca biz org gov software io us rome.it ai] |> random() "https://#{host}.#{tld}" end # Module names are atoms # When the schema passes in a module, ask it for it's Ecto type # # schema "users" do # field(:balance, EctoURI) # end # # Ecto Types must identify themselves with a type() function # https://hexdocs.pm/ecto/Ecto.Type.html def gen(ecto_type_module) when is_atom(ecto_type_module), do: gen(ecto_type_module.type()) # fallback to nil - this should probably raise def gen(_), do: nil defp build_attrs(factory_name, attributes) do {schema, defaults} = if function_exported?(factory_name, :__changeset__, 0) do {factory_name, []} else factory(factory_name) end attributes = Enum.into(attributes, []) non_generated_keys = defaults |> Kernel.++(attributes) |> Keyword.keys() |> Kernel.++(schema.__schema__(:primary_key)) attrs = schema.__changeset__() |> Map.drop(non_generated_keys) |> Enum.map(&cast/1) |> Keyword.merge(defaults) |> Keyword.merge(attributes) |> Enum.map(&gen_attribute/1) {schema, attrs} end defp gen_attribute({key, value}) when is_atom(value) and value not in [true, false, nil], do: {key, gen(value)} defp gen_attribute({key, {_, _} = value}), do: {key, gen(value)} defp gen_attribute(key_value), do: key_value defp cast({key, {:assoc, %{cardinality: :many}}}), do: {key, nil} defp cast({key, {:assoc, %{cardinality: :one}}}), do: {key, nil} defp cast({key, data_type}), do: {key, gen(data_type)} defp factory(factory_name) do case get_env(:ecto_factory, :factories)[factory_name] do nil -> raise(EctoFactory.MissingFactory, factory_name) {schema, defaults} -> {schema, defaults} {schema} -> {schema, []} end end defp random_string(min..max) do for(_ <- 1..random(min..max), into: "", do: <>) end defp random_string(max_length) do for(_ <- 1..random(8..max_length), into: "", do: <>) end end