Repository: msz/hammox
Pull request #62 (2020-10-19 23:51:16)

better Hammox.protect docs


Patch:
diff --git a/README.md b/README.md
index 83d4cc7..26f1476 100644
--- a/README.md
+++ b/README.md
@@ -158,17 +158,17 @@ defmodule RealDatabaseTest do
 end
 ```
 
-Hammox also provides a `setup_all` friendly `Hammox.protect/3` function which
-leverages this pattern. It produces a map of decorated functions from the
-module and is especially useful when you're decorating several functions at
-once:
+Hammox also provides a `setup_all` friendly version of `Hammox.protect` which
+leverages this pattern. Simply pass both the implementation module and the
+behaviour module and you will get a map of all callbacks defined by the
+behaviour as decorated implementation functions.
 
 ```elixir
 defmodule RealDatabaseTest do
   use ExUnit.Case, async: true
 
   setup_all do
-    Hammox.protect(RealDatabase, Database, get_users: 0)
+    Hammox.protect(RealDatabase, Database)
   end
 
   test "get_users/0 returns list of users", %{get_users_0: get_users_0} do
diff --git a/lib/hammox.ex b/lib/hammox.ex
index 4b70648..a62477e 100644
--- a/lib/hammox.ex
+++ b/lib/hammox.ex
@@ -15,6 +15,8 @@ defmodule Hammox do
   alias Hammox.TypeMatchError
   alias Hammox.Cache
 
+  @type function_arity_pair :: {atom(), arity() | [arity()]}
+
   defmodule TypespecNotFoundError do
     @moduledoc false
     defexception [:message]
@@ -83,86 +85,47 @@ defmodule Hammox do
 
   @doc since: "0.1.0"
   @doc """
-  Same as `protect/3`, but decorate all public functions inside the module.
-
-  Example:
+  See `protect/3`.
+  """
+  def protect(module)
 
-  ```elixir
-  defmodule Calculator do
-    @callback add(integer(), integer()) :: integer()
-    def add(a, b), do: a + b
-    @callback add(integer(), integer(), integer()) :: integer()
-    def add(a, b, c), do: a + b + c
-    @callback multiply(integer(), integer()) :: integer()
-    def multiply(a, b), do: a * b
+  @spec protect(module :: module()) :: %{atom() => fun()}
+  def protect(module) when is_atom(module) do
+    funs = get_funcs!(module)
+    protect(module, module, funs)
   end
 
-  %{
-    add_2: add_2,
-    add_3: add_3,
-    multiply_2: multiply_2
-  } = Hammox.protect(Calculator)
-  """
-  @spec protect(module_name :: module()) :: map()
-  def protect(module_name) when is_atom(module_name) do
-    funs = get_funcs!(module_name)
-    protect(module_name, module_name, funs)
+  @spec protect(mfa :: mfa()) :: fun()
+  def protect({module, function_name, arity})
+      when is_atom(module) and is_atom(function_name) and is_integer(arity) do
+    protect({module, function_name, arity}, module)
   end
 
   @doc since: "0.1.0"
   @doc """
-  Takes the function provided by a module, function, arity tuple and
-  decorates it with Hammox type checking.
-
-  Returns a new anonymous function.
-
-  Example:
-
-  ```elixir
-  defmodule Calculator do
-    @callback add(integer(), integer()) :: integer()
-  end
-
-  defmodule TestCalculator do
-    def add(a, b), do: a + b
-  end
-
-  add_2 = Hammox.protect({TestCalculator, :add, 2}, Calculator)
-
-  add_2.(1.5, 2.5) # throws Hammox.TypeMatchError
-  ```
-
-  A common tactic is to put behaviour callbacks and the "default"
-  implementations for these callbacks in the same module. For these kinds of
-  modules, you can also use `protect/2` as a shortcut for `protect/3`:
-  ```elixir
-  # calling this
-  Hammox.protect(SomeModule, foo: 1)
-  # works the same as this
-  Hammox.protect(SomeModule, SomeModule, foo: 1)
-  ```
+  See `protect/3`.
   """
   def protect(mfa, behaviour_name)
 
-  @spec protect(module_name :: module(), funs :: [{atom(), arity() | [arity()]}]) :: map()
-  def protect(module_name, funs) when is_atom(module_name) and is_list(funs),
-    do: protect(module_name, module_name, funs)
+  @spec protect(module :: module(), funs :: [function_arity_pair()]) :: %{atom() => fun()}
+  def protect(module, funs) when is_atom(module) and is_list(funs),
+    do: protect(module, module, funs)
 
   @spec protect(mfa :: mfa(), behaviour_name :: module()) :: fun()
-  def protect({module_name, function_name, arity} = mfa, behaviour_name)
-      when is_atom(module_name) and is_atom(function_name) and is_integer(arity) and
+  def protect({module, function_name, arity} = mfa, behaviour_name)
+      when is_atom(module) and is_atom(function_name) and is_integer(arity) and
              is_atom(behaviour_name) do
-    module_exist?(module_name)
+    module_exist?(module)
     module_exist?(behaviour_name)
     mfa_exist?(mfa)
 
-    code = {module_name, function_name}
+    code = {module, function_name}
 
     typespecs = fetch_typespecs!(behaviour_name, function_name, arity)
     protected(code, typespecs, arity)
   end
 
-  @spec protect(implementation_name :: module(), behaviour_name :: module()) :: map()
+  @spec protect(implementation_name :: module(), behaviour_name :: module()) :: %{atom() => fun()}
   def protect(implementation_name, behaviour_name)
       when is_atom(implementation_name) and is_atom(behaviour_name) do
     funs = get_funcs!(behaviour_name)
@@ -171,46 +134,106 @@ defmodule Hammox do
 
   @doc since: "0.1.0"
   @doc """
-  Same as `protect/2`, but allows decorating multiple functions at the same
-  time.
+  Decorates functions with Hammox checks based on given behaviour.
 
-  Provide a list of functions to decorate as third argument.
+  ## Basic usage
 
-  Returns a map where the keys are atoms of the form
-  `:{function_name}_{arity}` and values are the decorated anonymous
-  functions.
+  When passed an MFA tuple representing the function you'd like to protect,
+  and a behaviour containing a callback for the function, it returns a new
+  anonymous function that raises `Hammox.TypeMatchError` when called
+  incorrectly or when it returns an incorrect value.
 
   Example:
+  ```elixir
+  defmodule Calculator do
+    @callback add(integer(), integer()) :: integer()
+  end
+
+  defmodule TestCalculator do
+    def add(a, b), do: a + b
+  end
 
+  add_2 = Hammox.protect({TestCalculator, :add, 2}, Calculator)
+
+  add_2.(1.5, 2.5) # throws Hammox.TypeMatchError
+  ```
+
+  ## Batch usage
+
+  You can decorate all functions defined by a given behaviour by passing an
+  implementation module and a behaviour module. Optionally, you can pass an
+  explicit list of functions as the third argument.
+
+  The returned map is useful as the return value for a test setup callback to
+  set test context for all tests to use.
+
+  Example:
   ```elixir
   defmodule Calculator do
     @callback add(integer(), integer()) :: integer()
     @callback add(integer(), integer(), integer()) :: integer()
+    @callback add(integer(), integer(), integer(), integer()) :: integer()
     @callback multiply(integer(), integer()) :: integer()
   end
 
   defmodule TestCalculator do
     def add(a, b), do: a + b
     def add(a, b, c), do: a + b + c
+    def add(a, b, c, d), do: a + b + c + d
     def multiply(a, b), do: a * b
   end
 
+  %{
+    add_2: add_2,
+    add_3: add_3,
+    add_4: add_4
+    multiply_2: multiply_2
+  } = Hammox.protect(TestCalculator, Calculator)
+
+  # optionally
   %{
     add_2: add_2,
     add_3: add_3,
     multiply_2: multiply_2
   } = Hammox.protect(TestCalculator, Calculator, add: [2, 3], multiply: 2)
+  ```
+
+  ## Behaviour-implementation shortcuts
+
+  Often, there exists one "default" implementation for a behaviour. A common
+  practice is then to define both the callbacks and the implementations in
+  one module. For these behaviour-implementation modules, Hammox provides
+  shortucts that only require one module.
+
+  Example:
+
+  ```elixir
+  defmodule Calculator do
+    @callback add(integer(), integer()) :: integer()
+    def add(a, b), do: a + b
+  end
+
+  Hammox.protect({Calculator, :add, 2})
+  # is equivalent to
+  Hammox.protect({Calculator, :add, 2}, Calculator)
+
+  Hammox.protect(Calculator, add: 2)
+  # is equivalent to
+  Hammox.protect(Calculator, Calculator, add: 2)
 
+  Hammox.protect(Calculator)
+  # is equivalent to
+  Hammox.protect(Calculator, Calculator)
   ```
   """
   @spec protect(
-          module_name :: module(),
+          module :: module(),
           behaviour_name :: module(),
-          funs :: [{atom(), arity() | [arity()]}]
+          funs :: [function_arity_pair()]
         ) ::
-          map()
-  def protect(module_name, behaviour_name, funs)
-      when is_atom(module_name) and is_atom(behaviour_name) and is_list(funs) do
+          %{atom() => fun()}
+  def protect(module, behaviour_name, funs)
+      when is_atom(module) and is_atom(behaviour_name) and is_list(funs) do
     funs
     |> Enum.flat_map(fn {function_name, arity_or_arities} ->
       arity_or_arities
@@ -222,7 +245,7 @@ defmodule Hammox do
           |> Kernel.<>("_#{arity}")
           |> String.to_atom()
 
-        value = protect({module_name, function_name, arity}, behaviour_name)
+        value = protect({module, function_name, arity}, behaviour_name)
         {key, value}
       end)
     end)
@@ -308,7 +331,7 @@ defmodule Hammox do
   defp protected_code(code, typespecs, args) do
     return_value =
       case code do
-        {module_name, function_name} -> apply(module_name, function_name, args)
+        {module, function_name} -> apply(module, function_name, args)
         anonymous when is_function(anonymous) -> apply(anonymous, args)
       end
 
@@ -405,8 +428,8 @@ defmodule Hammox do
     end
   end
 
-  defp do_fetch_typespecs(behaviour_module_name, function_name, arity) do
-    callbacks = fetch_callbacks(behaviour_module_name)
+  defp do_fetch_typespecs(behaviour_module, function_name, arity) do
+    callbacks = fetch_callbacks(behaviour_module)
 
     callbacks
     |> Enum.find_value([], fn
@@ -414,15 +437,15 @@ defmodule Hammox do
       _ -> false
     end)
     |> Enum.map(fn typespec ->
-      Utils.replace_user_types(typespec, behaviour_module_name)
+      Utils.replace_user_types(typespec, behaviour_module)
     end)
   end
 
-  defp fetch_callbacks(behaviour_module_name) do
-    case Cache.get({:callbacks, behaviour_module_name}) do
+  defp fetch_callbacks(behaviour_module) do
+    case Cache.get({:callbacks, behaviour_module}) do
       nil ->
-        {:ok, callbacks} = Code.Typespec.fetch_callbacks(behaviour_module_name)
-        Cache.put({:callbacks, behaviour_module_name}, callbacks)
+        {:ok, callbacks} = Code.Typespec.fetch_callbacks(behaviour_module)
+        Cache.put({:callbacks, behaviour_module}, callbacks)
         callbacks
 
       callbacks ->
@@ -444,37 +467,35 @@ defmodule Hammox do
     Enum.at(arg_typespecs, arg_index)
   end
 
-  defp module_exist?(module_name) do
-    case Code.ensure_compiled(module_name) do
+  defp module_exist?(module) do
+    case Code.ensure_compiled(module) do
       {:module, _} ->
         true
 
       _ ->
         raise(ArgumentError,
-          message: "Could not find module #{Utils.module_to_string(module_name)}."
+          message: "Could not find module #{Utils.module_to_string(module)}."
         )
     end
   end
 
-  defp mfa_exist?({module_name, function_name, arity}) do
-    case function_exported?(module_name, function_name, arity) do
+  defp mfa_exist?({module, function_name, arity}) do
+    case function_exported?(module, function_name, arity) do
       true ->
         true
 
       _ ->
         raise(ArgumentError,
           message:
-            "Could not find function #{Utils.module_to_string(module_name)}.#{function_name}/#{
-              arity
-            }."
+            "Could not find function #{Utils.module_to_string(module)}.#{function_name}/#{arity}."
         )
     end
   end
 
-  defp get_funcs!(module_name) do
-    module_exist?(module_name)
+  defp get_funcs!(module) do
+    module_exist?(module)
 
-    module_name
+    module
     |> fetch_callbacks()
     |> Enum.map(fn {callback, _typespecs} ->
       callback
diff --git a/lib/hammox/type_match_error.ex b/lib/hammox/type_match_error.ex
index 39143ab..0cce562 100644
--- a/lib/hammox/type_match_error.ex
+++ b/lib/hammox/type_match_error.ex
@@ -1,5 +1,8 @@
 defmodule Hammox.TypeMatchError do
-  @moduledoc false
+  @moduledoc """
+  An error thrown when Hammox detects that values in a function call don't
+  match types defined in typespecs.
+  """
   defexception [:message]
 
   alias Hammox.Utils


Test patch:
diff --git a/test/hammox_test.exs b/test/hammox_test.exs
index e3e23b3..07f2159 100644
--- a/test/hammox_test.exs
+++ b/test/hammox_test.exs
@@ -10,6 +10,11 @@ defmodule HammoxTest do
       assert %{other_foo_0: _, other_foo_1: _, foo_0: _} =
                Hammox.protect(Hammox.Test.BehaviourImplementation)
     end
+
+    test "decorates the function designated by the MFA tuple" do
+      fun = Hammox.protect({Hammox.Test.BehaviourImplementation, :foo, 0})
+      assert_raise(Hammox.TypeMatchError, fn -> fun.() end)
+    end
   end
 
   describe "protect/2" do