This repository has been archived by the owner on Oct 8, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Xgit.Util.GenServerUtils (ported from old jgit port). (#46)
- Loading branch information
Showing
2 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
defmodule Xgit.Util.GenServerUtils do | ||
@moduledoc ~S""" | ||
Some utilities to make error handling in `GenServer` calls easier. | ||
Xgit is somewhat more exception-friendly than typical Elixir code. | ||
""" | ||
|
||
@doc ~S""" | ||
Makes a synchronous call to the server and waits for its reply. | ||
## Return Value | ||
If the response is `:ok`, return `server` (for function chaining). | ||
If the response is `{:ok, (value)}`, return `value`. | ||
If the response is `{:error, (reason)}`, raise `reason` as an error. | ||
""" | ||
@spec call!(server :: GenServer.server(), request :: term, timeout :: non_neg_integer) :: term | ||
def call!(server, request, timeout \\ 5000) do | ||
case GenServer.call(server, request, timeout) do | ||
:ok -> server | ||
{:ok, value} -> value | ||
{:error, reason} -> raise reason | ||
end | ||
end | ||
|
||
@doc ~S""" | ||
Wrap a `handle_call/3` call to a `handle_(something)` call on a module. | ||
Wraps common `:ok` and error responses and exceptions and returns them to caller. | ||
Should be used for standalone modules (i.e. modules that are not open to extension). | ||
""" | ||
@spec wrap_call(mod :: module, function :: atom, args :: term, prev_state :: term) :: | ||
{:reply, term, term} | ||
def wrap_call(mod, function, args, prev_state) do | ||
case apply(mod, function, args) do | ||
{:ok, state} -> {:reply, :ok, state} | ||
{:ok, response, state} -> {:reply, {:ok, response}, state} | ||
{:error, reason, state} -> {:reply, {:error, reason}, state} | ||
end | ||
rescue | ||
e -> {:reply, {:error, e}, prev_state} | ||
end | ||
|
||
@doc ~S""" | ||
Delegate a `handle_call/3` call to a `handle_(something)` call on a module. | ||
Wraps common `:ok` and error responses and exceptions and returns them to caller. | ||
Unlike `wrap_call/4`, assumes that the `GenServer` state is a tuple of | ||
`{module, mod_state}` and re-wraps module state accordingly. | ||
""" | ||
@spec delegate_call_to(mod :: module, function :: atom, args :: term, mod_state :: term) :: | ||
{:reply, term, {module, term}} | ||
def delegate_call_to(mod, function, args, mod_state) do | ||
case apply(mod, function, args) do | ||
{:ok, mod_state} -> {:reply, :ok, {mod, mod_state}} | ||
{:ok, response, mod_state} -> {:reply, {:ok, response}, {mod, mod_state}} | ||
{:error, reason, mod_state} -> {:reply, {:error, reason}, {mod, mod_state}} | ||
end | ||
rescue | ||
e -> {:reply, {:error, e}, {mod, mod_state}} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
defmodule Xgit.Util.GenServerUtilsTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias Xgit.Util.GenServerUtils | ||
|
||
describe "call!/3" do | ||
test "returns server PID when response is simply :ok" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert ^pid = GenServerUtils.call!(pid, :respond_ok) | ||
end | ||
|
||
test "returns value when response is {:ok, value}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert 42 = GenServerUtils.call!(pid, :respond_ok_value) | ||
end | ||
|
||
test "raises error when response is {:error, reason}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
|
||
assert_raise RuntimeError, "foo", fn -> | ||
GenServerUtils.call!(pid, :respond_error_foo) | ||
end | ||
end | ||
end | ||
|
||
describe "wrap_call_to/4" do | ||
test "generates appropriate :reply when response is :ok" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert pid = GenServerUtils.call!(pid, :wrap_ok) | ||
end | ||
|
||
test "generates appropriate :reply when response is {:ok, value}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert "foo" = GenServerUtils.call!(pid, :wrap_ok_foo) | ||
end | ||
|
||
test "raises when response is {:error, reason}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
|
||
assert_raise RuntimeError, "bogus", fn -> | ||
GenServerUtils.call!(pid, :wrap_error_bogus) | ||
end | ||
end | ||
|
||
test "relays error when raised in wrap" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
|
||
assert_raise CaseClauseError, "no case clause matching: 45", fn -> | ||
GenServerUtils.call!(pid, :wrap_raise_error) | ||
end | ||
end | ||
end | ||
|
||
describe "delegate_call_to/4" do | ||
test "generates appropriate :reply when response is :ok" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert pid = GenServerUtils.call!(pid, :delegate_ok) | ||
end | ||
|
||
test "generates appropriate :reply when response is {:ok, value}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
assert "foo" = GenServerUtils.call!(pid, :delegate_ok_foo) | ||
end | ||
|
||
test "raises when response is {:error, reason}" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
|
||
assert_raise RuntimeError, "bogus", fn -> | ||
GenServerUtils.call!(pid, :delegate_error_bogus) | ||
end | ||
end | ||
|
||
test "relays error when raised in delegate" do | ||
{:ok, pid} = GenServer.start_link(__MODULE__.TestServer, []) | ||
|
||
assert_raise CaseClauseError, "no case clause matching: 45", fn -> | ||
GenServerUtils.call!(pid, :delegate_raise_error) | ||
end | ||
end | ||
end | ||
|
||
defmodule TestDelegate do | ||
@spec delegate_ok(term) :: term | ||
def delegate_ok(state), do: {:ok, state} | ||
|
||
@spec delegate_ok_foo(term) :: term | ||
def delegate_ok_foo(state), do: {:ok, "foo", state} | ||
|
||
@spec delegate_error_bogus(term) :: term | ||
def delegate_error_bogus(state), do: {:error, "bogus", state} | ||
|
||
@spec delegate_raise_error(term) :: term | ||
def delegate_raise_error(state) do | ||
case state do | ||
1 -> :first | ||
44 -> :ok | ||
end | ||
end | ||
end | ||
|
||
defmodule TestServer do | ||
use GenServer | ||
|
||
alias Xgit.Util.GenServerUtilsTest.TestDelegate | ||
|
||
import Xgit.Util.GenServerUtils | ||
|
||
@impl true | ||
def init(_), do: {:ok, nil} | ||
|
||
@spec wrap_ok(term) :: {:ok, term} | ||
def wrap_ok(state), do: {:ok, state} | ||
|
||
@spec wrap_ok_foo(term) :: {:ok, term} | ||
def wrap_ok_foo(state), do: {:ok, "foo", state} | ||
|
||
@spec wrap_error_bogus(term) :: {:error, String.t(), term} | ||
def wrap_error_bogus(state), do: {:error, "bogus", state} | ||
|
||
@spec wrap_raise_error(term) :: :ok | :first | ||
def wrap_raise_error(state) do | ||
case state do | ||
1 -> :first | ||
44 -> :ok | ||
end | ||
end | ||
|
||
@impl true | ||
def handle_call(:respond_ok, _from, _state), do: {:reply, :ok, nil} | ||
def handle_call(:respond_ok_value, _from, _state), do: {:reply, {:ok, 42}, nil} | ||
def handle_call(:respond_error_foo, _from, _state), do: {:reply, {:error, "foo"}, nil} | ||
|
||
def handle_call(:wrap_ok, _from, _state), | ||
do: wrap_call(__MODULE__, :wrap_ok, [42], 42) | ||
|
||
def handle_call(:wrap_ok_foo, _from, _state), | ||
do: wrap_call(__MODULE__, :wrap_ok_foo, [44], 44) | ||
|
||
def handle_call(:wrap_error_bogus, _from, _state), | ||
do: wrap_call(__MODULE__, :wrap_error_bogus, [44], 44) | ||
|
||
def handle_call(:wrap_raise_error, _from, _state), | ||
do: wrap_call(__MODULE__, :wrap_raise_error, [45], 45) | ||
|
||
def handle_call(:delegate_ok, _from, _state), | ||
do: delegate_call_to(TestDelegate, :delegate_ok, [42], 42) | ||
|
||
def handle_call(:delegate_ok_foo, _from, _state), | ||
do: delegate_call_to(TestDelegate, :delegate_ok_foo, [44], 44) | ||
|
||
def handle_call(:delegate_error_bogus, _from, _state), | ||
do: delegate_call_to(TestDelegate, :delegate_error_bogus, [44], 44) | ||
|
||
def handle_call(:delegate_raise_error, _from, _state), | ||
do: delegate_call_to(TestDelegate, :delegate_raise_error, [45], 45) | ||
end | ||
end |