From b10f1b631323aa3747ad8b53d5d5d8e1de3ef154 Mon Sep 17 00:00:00 2001 From: Devon Estes Date: Mon, 17 Dec 2018 13:18:22 +0100 Subject: [PATCH] Add additional callbacks for functions with default params In pretty much every public function in `ex_machina` you allow users to either give or not give parameters for the resource that's being built, but in the documentation that's not shown because you can't show optional arguments in callback definitions. This commit adds callbacks for these `/1` or `/2` functions that are commonly used. This benefits documentation, but also enforces the behaviour contract to make sure that all documented functionality is implemented correctly. --- lib/ex_machina.ex | 42 ++++++++++++++--------- lib/ex_machina/ecto.ex | 78 +++++++++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 41 deletions(-) diff --git a/lib/ex_machina.ex b/lib/ex_machina.ex index c79ae80..0fc7354 100644 --- a/lib/ex_machina.ex +++ b/lib/ex_machina.ex @@ -13,16 +13,16 @@ defmodule ExMachina do defexception [:message] def exception(factory_name) do - message = - """ - No factory defined for #{inspect factory_name}. + message = """ + No factory defined for #{inspect(factory_name)}. - Please check for typos or define your factory: + Please check for typos or define your factory: + + def #{factory_name}_factory do + ... + end + """ - def #{factory_name}_factory do - ... - end - """ %UndefinedFactoryError{message: message} end end @@ -30,7 +30,7 @@ defmodule ExMachina do use Application @doc false - def start(_type, _args), do: ExMachina.Sequence.start_link + def start(_type, _args), do: ExMachina.Sequence.start_link() defmacro __using__(_opts) do quote do @@ -70,7 +70,7 @@ defmodule ExMachina do raise_function_replaced_error("create_list/3", "insert_list/3") end - @spec raise_function_replaced_error(String.t, String.t) :: no_return + @spec raise_function_replaced_error(String.t(), String.t()) :: no_return defp raise_function_replaced_error(old_function, new_function) do raise """ #{old_function} has been removed. @@ -82,12 +82,12 @@ defmodule ExMachina do """ end - defoverridable [create: 1, create: 2, create_pair: 2, create_list: 3] + defoverridable create: 1, create: 2, create_pair: 2, create_list: 3 end end @doc """ - Shortcut for creating unique string values. + Shortcut for creating unique string values. This is automatically imported into a model factory when you `use ExMachina`. @@ -114,7 +114,7 @@ defmodule ExMachina do } end """ - @spec sequence(String.t) :: String.t + @spec sequence(String.t()) :: String.t() def sequence(name), do: ExMachina.Sequence.next(name) @@ -157,15 +157,20 @@ defmodule ExMachina do %{name: "John Doe", admin: false} end + # Returns %{name: "John Doe", admin: false} + build(:user) + # Returns %{name: "John Doe", admin: true} build(:user, admin: true) """ + @callback build(factory_name :: atom) :: any @callback build(factory_name :: atom, attrs :: keyword | map) :: any @doc false def build(module, factory_name, attrs \\ %{}) do attrs = Enum.into(attrs, %{}) function_name = build_function_name(factory_name) + if Code.ensure_loaded?(module) && function_exported?(module, function_name, 0) do apply(module, function_name, []) |> do_merge(attrs) else @@ -175,9 +180,9 @@ defmodule ExMachina do defp build_function_name(factory_name) do factory_name - |> Atom.to_string + |> Atom.to_string() |> Kernel.<>("_factory") - |> String.to_atom + |> String.to_atom() end defp do_merge(%{__struct__: _} = record, attrs), do: struct!(record, attrs) @@ -193,6 +198,7 @@ defmodule ExMachina do # Returns a list of 2 users build_pair(:user) """ + @callback build_pair(factory_name :: atom) :: list @callback build_pair(factory_name :: atom, attrs :: keyword | map) :: list @doc false @@ -208,7 +214,9 @@ defmodule ExMachina do # Returns a list of 3 users build_list(3, :user) """ - @callback build_list(number_of_records :: integer, factory_name :: atom, attrs :: keyword | map) :: list + @callback build_list(number_of_records :: integer, factory_name :: atom) :: list + @callback build_list(number_of_records :: integer, factory_name :: atom, attrs :: keyword | map) :: + list @doc false def build_list(module, number_of_records, factory_name, attrs \\ %{}) do @@ -222,7 +230,7 @@ defmodule ExMachina do quote do @doc "Raises a helpful error if no factory is defined." @spec factory(any) :: no_return - def factory(factory_name), do: raise UndefinedFactoryError, factory_name + def factory(factory_name), do: raise(UndefinedFactoryError, factory_name) end end end diff --git a/lib/ex_machina/ecto.ex b/lib/ex_machina/ecto.ex index add2683..3893348 100644 --- a/lib/ex_machina/ecto.ex +++ b/lib/ex_machina/ecto.ex @@ -14,6 +14,7 @@ defmodule ExMachina.Ecto do """ defmacro __using__(opts) do verify_ecto_dep() + if repo = Keyword.get(opts, :repo) do quote do use ExMachina @@ -37,18 +38,18 @@ defmodule ExMachina.Ecto do end else raise ArgumentError, - """ - expected :repo to be given as an option. Example: + """ + expected :repo to be given as an option. Example: - use ExMachina.Ecto, repo: MyApp.Repo - """ + use ExMachina.Ecto, repo: MyApp.Repo + """ end end defp verify_ecto_dep do unless Code.ensure_loaded?(Ecto) do raise "You tried to use ExMachina.Ecto, but the Ecto module is not loaded. " <> - "Please add ecto to your dependencies." + "Please add ecto to your dependencies." end end @@ -57,6 +58,7 @@ defmodule ExMachina.Ecto do The arguments are the same as `c:ExMachina.build/2`. """ + @callback insert(factory_name :: atom) :: any @callback insert(factory_name :: atom, attrs :: keyword | map) :: any @doc """ @@ -64,6 +66,7 @@ defmodule ExMachina.Ecto do The arguments are the same as `c:ExMachina.build_pair/2`. """ + @callback insert_pair(factory_name :: atom) :: list @callback insert_pair(factory_name :: atom, attrs :: keyword | map) :: list @doc """ @@ -71,7 +74,12 @@ defmodule ExMachina.Ecto do The arguments are the same as `c:ExMachina.build_list/3`. """ - @callback insert_list(number_of_records :: integer, factory_name :: atom, attrs :: keyword | map) :: list + @callback insert_list(number_of_records :: integer, factory_name :: atom) :: list + @callback insert_list( + number_of_records :: integer, + factory_name :: atom, + attrs :: keyword | map + ) :: list @doc """ Builds a factory and returns only its fields. @@ -96,7 +104,11 @@ defmodule ExMachina.Ecto do # Returns %{name: "John Doe", admin: true} params_for(:user, admin: true) + + # Returns %{name: "John Doe", admin: false} + params_for(:user) """ + @callback params_for(factory_name :: atom) :: %{optional(atom) => any} @callback params_for(factory_name :: atom, attrs :: keyword | map) :: %{optional(atom) => any} @doc false @@ -121,7 +133,10 @@ defmodule ExMachina.Ecto do # Returns %{"name" => "John Doe", "admin" => true} string_params_for(:user, admin: true) """ - @callback string_params_for(factory_name :: atom, attrs :: keyword | map) :: %{optional(String.t) => any} + @callback string_params_for(factory_name :: atom) :: %{optional(String.t()) => any} + @callback string_params_for(factory_name :: atom, attrs :: keyword | map) :: %{ + optional(String.t()) => any + } @doc false def string_params_for(module, factory_name, attrs \\ %{}) do @@ -145,7 +160,10 @@ defmodule ExMachina.Ecto do # Inserts an author and returns %{title: "An Awesome Article", author_id: 12} params_with_assocs(:article) """ - @callback params_with_assocs(factory_name :: atom, attrs :: keyword | map) :: %{optional(atom) => any} + @callback params_with_assocs(factory_name :: atom) :: %{optional(atom) => any} + @callback params_with_assocs(factory_name :: atom, attrs :: keyword | map) :: %{ + optional(atom) => any + } @doc false def params_with_assocs(module, factory_name, attrs \\ %{}) do @@ -171,7 +189,10 @@ defmodule ExMachina.Ecto do # Inserts an author and returns %{"title" => "An Awesome Article", "author_id" => 12} string_params_with_assocs(:article) """ - @callback string_params_with_assocs(factory_name :: atom, attrs :: keyword | map) :: %{optional(String.t) => any} + @callback string_params_with_assocs(factory_name :: atom) :: %{optional(String.t()) => any} + @callback string_params_with_assocs(factory_name :: atom, attrs :: keyword | map) :: %{ + optional(String.t()) => any + } @doc false def string_params_with_assocs(module, factory_name, attrs \\ %{}) do @@ -180,7 +201,6 @@ defmodule ExMachina.Ecto do |> convert_atom_keys_to_strings end - defp recursively_strip(record = %{__struct__: _}) do record |> set_persisted_belongs_to_ids @@ -193,7 +213,7 @@ defmodule ExMachina.Ecto do defp recursively_strip(record), do: record defp handle_assocs(record = %{__struct__: struct}) do - Enum.reduce struct.__schema__(:associations), record, fn(association_name, record) -> + Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> case struct.__schema__(:association, association_name) do %{__struct__: Ecto.Association.BelongsTo} -> Map.delete(record, association_name) @@ -203,7 +223,7 @@ defmodule ExMachina.Ecto do |> Map.get(association_name) |> handle_assoc(record, association_name) end - end + end) end defp handle_assoc(original_assoc, record, association_name) do @@ -225,7 +245,7 @@ defmodule ExMachina.Ecto do end defp handle_embeds(record = %{__struct__: struct}) do - Enum.reduce(struct.__schema__(:embeds), record, fn(embed_name, record) -> + Enum.reduce(struct.__schema__(:embeds), record, fn embed_name, record -> record |> Map.get(embed_name) |> handle_embed(record, embed_name) @@ -237,16 +257,18 @@ defmodule ExMachina.Ecto do %{} -> embed = recursively_strip(original_embed) Map.put(record, embed_name, embed) + list when is_list(list) -> embeds_many = Enum.map(original_embed, &recursively_strip/1) Map.put(record, embed_name, embeds_many) + nil -> Map.delete(record, embed_name) end end defp set_persisted_belongs_to_ids(record = %{__struct__: struct}) do - Enum.reduce struct.__schema__(:associations), record, fn(association_name, record) -> + Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> association = struct.__schema__(:association, association_name) case association do @@ -254,11 +276,15 @@ defmodule ExMachina.Ecto do case Map.get(record, association_name) do belongs_to = %{__meta__: %{__struct__: Ecto.Schema.Metadata, state: :loaded}} -> set_belongs_to_primary_key(record, belongs_to, association) - _ -> record + + _ -> + record end - _ -> record + + _ -> + record end - end + end) end defp set_belongs_to_primary_key(record, belongs_to, association) do @@ -267,14 +293,15 @@ defmodule ExMachina.Ecto do end defp insert_belongs_to_assocs(record = %{__struct__: struct}, module) do - Enum.reduce struct.__schema__(:associations), record, fn(association_name, record) -> + Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> case struct.__schema__(:association, association_name) do association = %{__struct__: Ecto.Association.BelongsTo} -> insert_built_belongs_to_assoc(module, association, record) - _ -> record + _ -> + record end - end + end) end defp insert_built_belongs_to_assoc(module, association, record) do @@ -291,7 +318,7 @@ defmodule ExMachina.Ecto do @doc false def drop_ecto_fields(record = %{__struct__: struct}) do record - |> Map.from_struct + |> Map.from_struct() |> Map.delete(:__meta__) |> drop_autogenerated_ids(struct) end @@ -308,20 +335,23 @@ defmodule ExMachina.Ecto do defp drop_fields_with_nil_values(map) do map - |> Enum.reject(fn({_, value}) -> value == nil end) + |> Enum.reject(fn {_, value} -> value == nil end) |> Enum.into(%{}) end defp convert_atom_keys_to_strings(values) when is_list(values) do Enum.map(values, &convert_atom_keys_to_strings/1) end + defp convert_atom_keys_to_strings(%{__struct__: _} = record) when is_map(record) do Map.from_struct(record) |> convert_atom_keys_to_strings() end + defp convert_atom_keys_to_strings(record) when is_map(record) do - Enum.reduce record, Map.new, fn({key, value}, acc) -> + Enum.reduce(record, Map.new(), fn {key, value}, acc -> Map.put(acc, to_string(key), convert_atom_keys_to_strings(value)) - end + end) end + defp convert_atom_keys_to_strings(value), do: value end