Skip to content

Commit

Permalink
Merge pull request #71 from msz/protect-case
Browse files Browse the repository at this point in the history
add Hammox.ProtectCase
  • Loading branch information
msz authored Jan 23, 2021
2 parents d4ec045 + aab1770 commit 3aad012
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 15 deletions.
18 changes: 3 additions & 15 deletions lib/hammox.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ defmodule Hammox do
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)
module_exist?(behaviour_name)
Utils.check_module_exists(module)
Utils.check_module_exists(behaviour_name)
mfa_exist?(mfa)

code = {module, function_name}
Expand Down Expand Up @@ -467,18 +467,6 @@ defmodule Hammox do
Enum.at(arg_typespecs, arg_index)
end

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)}."
)
end
end

defp mfa_exist?({module, function_name, arity}) do
case function_exported?(module, function_name, arity) do
true ->
Expand All @@ -493,7 +481,7 @@ defmodule Hammox do
end

defp get_funcs!(module) do
module_exist?(module)
Utils.check_module_exists(module)

module
|> fetch_callbacks()
Expand Down
109 changes: 109 additions & 0 deletions lib/hammox/protect.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
defmodule Hammox.Protect do
@moduledoc """
A `use`able module simplifying protecting functions with Hammox.
The explicit way is to use `Hammox.protect/3` and friends to generate
protected versions of functions as anonymous functions. In tests, the most
convenient way is to generate them once in a setup hook and then resolve
them from test context. However, this can get quite verbose.
If you're willing to trade explicitness for some macro magic, doing `use
Hammox.Protect` in your test module will define functions from the module
you want to protect in it. The effect is similar to `import`ing the module
you're testing, but with added benefit of the functions being protected.
`use Hammox.Protect` supports these options:
- `:module` (required) — the module you'd like to protect (usually the one
you're testing in the test module). Equivalent to the first parameter of
`Hammox.protect/3` in batch usage.
- `:behaviour` — the behaviour module you'd like to protect the
implementation module with. Can be skipped if `:module` and `:behaviour`
are the same module. Equivalent to the second parameter of
`Hammox.protect/3` in batch usage.
- `:funs` — An optional explicit list of functions you'd like to protect.
Equivalent to the third parameter of `Hammox.protect/3` in batch usage.
"""
alias Hammox.Utils

defmacro __using__(opts) do
opts_block =
quote do
{module, behaviour, funs} = Hammox.Protect.extract_opts!(unquote(opts))
end

funs_block =
quote unquote: false do
for {name, arity} <- funs do
def unquote(name)(
unquote_splicing(
Enum.map(
case arity do
0 -> []
arity -> 1..arity
end,
&Macro.var(:"arg#{&1}", __MODULE__)
)
)
) do
protected_fun =
Hammox.Protect.protect(
{unquote(module), unquote(name), unquote(arity)},
unquote(behaviour)
)

apply(
protected_fun,
unquote(
Enum.map(
case arity do
0 -> []
arity -> 1..arity
end,
&Macro.var(:"arg#{&1}", __MODULE__)
)
)
)
end
end
end

quote do
unquote(opts_block)
unquote(funs_block)
end
end

@doc false
def extract_opts!(opts) do
module = Keyword.get(opts, :module)
behaviour = Keyword.get(opts, :behaviour)

if is_nil(module) do
raise ArgumentError,
message: """
Please specify :module to protect with Hammox.Protect.
Example:
use Hammox.Protect, module: ModuleToProtect
"""
end

funs = Keyword.get_lazy(opts, :funs, fn -> get_funs!(behaviour || module) end)

{module, behaviour, funs}
end

@doc false
def protect(mfa, nil), do: Hammox.protect(mfa)
def protect(mfa, behaviour), do: Hammox.protect(mfa, behaviour)

defp get_funs!(module) do
Utils.check_module_exists(module)
{:ok, callbacks} = Code.Typespec.fetch_callbacks(module)

Enum.map(callbacks, fn {callback, _typespecs} ->
callback
end)
end
end
12 changes: 12 additions & 0 deletions lib/hammox/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ defmodule Hammox.Utils do
other
end
end

def check_module_exists(module) do
case Code.ensure_compiled(module) do
{:module, _} ->
true

_ ->
raise(ArgumentError,
message: "Could not find module #{module_to_string(module)}."
)
end
end
end
32 changes: 32 additions & 0 deletions test/hammox/protect_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Hammox.ProtectTest do
alias Hammox.Test.Protect, as: ProtectTest

use ExUnit.Case, async: true

use Hammox.Protect, module: ProtectTest.BehaviourImplementation

use Hammox.Protect,
module: ProtectTest.Implementation,
behaviour: ProtectTest.Behaviour,
funs: [behaviour_wrong_typespec: 0]

test "using Protect without module throws an exception" do
module_string = """
defmodule ProtectWithoutModule do
use Hammox.Protect
end
"""

assert_raise ArgumentError, ~r/Please specify :module to protect/, fn ->
Code.compile_string(module_string)
end
end

test "using Protect creates protected versions of functions from given behaviour-implementation module" do
assert_raise Hammox.TypeMatchError, fn -> behaviour_implementation_wrong_typespec() end
end

test "using Protect creates protected versions of functions from given behaviour and implementation" do
assert_raise Hammox.TypeMatchError, fn -> behaviour_wrong_typespec() end
end
end
3 changes: 3 additions & 0 deletions test/support/protect/behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Hammox.Test.Protect.Behaviour do
@callback behaviour_wrong_typespec() :: :foo
end
6 changes: 6 additions & 0 deletions test/support/protect/behaviour_implementation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule Hammox.Test.Protect.BehaviourImplementation do
@callback behaviour_implementation_wrong_typespec() :: :foo
def behaviour_implementation_wrong_typespec() do
:wrong
end
end
3 changes: 3 additions & 0 deletions test/support/protect/implementation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Hammox.Test.Protect.Implementation do
def behaviour_wrong_typespec, do: :wrong
end

0 comments on commit 3aad012

Please sign in to comment.