Skip to content
This repository has been archived by the owner on Oct 8, 2020. It is now read-only.

Commit

Permalink
Implement Xgit.Util.GenServerUtils (ported from old jgit port). (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten committed Jul 23, 2019
1 parent b0e5a9e commit e876eaa
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 0 deletions.
66 changes: 66 additions & 0 deletions lib/xgit/util/gen_server_utils.ex
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
157 changes: 157 additions & 0 deletions test/xgit/util/gen_server_utils_test.exs
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

0 comments on commit e876eaa

Please sign in to comment.