diff --git a/lib/interactor.ex b/lib/interactor.ex index 9487392..1f4635b 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -13,11 +13,12 @@ defmodule Interactor do callback. When `use`-ing you can optionaly include a Repo option which will be used to execute any Ecto.Changesets or Ecto.Multi structs you return. - Interactors supports three callbacks: + Interactors supports four callbacks: * `before_call/1` - Useful for manipulating input etc. * `handle_call/1` - The meat, usually returns an Ecto.Changeset or Ecto.Multi. * `after_call/1` - Useful for metrics, publishing events, etc + * `cleanup/1` - Used to undo the effects of this interactor. Interactors can be called in three ways: @@ -57,6 +58,14 @@ defmodule Interactor do """ @callback after_call(any) :: any + @doc """ + Executed to undo this interactor. + + This is primarily for organized interactors. cleanup is called on + finished interactors if something fails further down the chain. + """ + @callback cleanup(any) :: any + @doc """ Executes the `before_call/1`, `handle_call/1`, and `after_call/1` callbacks. @@ -64,7 +73,7 @@ defmodule Interactor do `repo` options was passed to `use Interactor` the changeset or multi will be executed and the results returned. """ - @spec call_task(module, map) :: Task.t + @spec call(module, map) :: any def call(interactor, context) do context |> interactor.before_call @@ -119,8 +128,9 @@ defmodule Interactor do quote do def before_call(c), do: c def after_call(r), do: r + def cleanup(x), do: {:ok, x} - defoverridable [before_call: 1, after_call: 1] + defoverridable [before_call: 1, after_call: 1, cleanup: 1] end end diff --git a/lib/interactor/organizer.ex b/lib/interactor/organizer.ex new file mode 100644 index 0000000..0e0a6b0 --- /dev/null +++ b/lib/interactor/organizer.ex @@ -0,0 +1,49 @@ +defmodule Interactor.Organizer do + defmacro __using__(opts) do + quote do + use Interactor, unquote(opts) + import Interactor.Organizer, only: [organize: 1] + + Module.register_attribute(__MODULE__, :interactors, accumulate: true) + end + end + + defmacro organize(interactors) do + quote do + import Interactor.Organizer + unquote(interactors) + |> Enum.reverse + |> Enum.each(&(Module.put_attribute(__MODULE__, :interactors, &1))) + unquote(define_callbacks) + end + end + + defp define_callbacks do + quote do + def handle_call(attributes) do + execute_interactors(attributes, @interactors) + end + end + end + + def execute_interactors(context, []), do: {:ok, context} + def execute_interactors(context, [interactor | interactors]) do + with {:ok, new_context} <- Interactor.call(interactor, context) do + try do + case execute_interactors(new_context, interactors) do + {:error, error, error_context} -> + handle_cleanup(interactor, error, error_context) + {:error, error} -> + handle_cleanup(interactor, error, new_context) + other -> other + end + rescue error -> handle_cleanup(interactor, error, new_context) + end + end + end + + def handle_cleanup(interactor, error, context) do + {:ok, cleanup_context} = interactor.cleanup(context) + {:error, error, cleanup_context} + end +end diff --git a/test/organizer_test.exs b/test/organizer_test.exs new file mode 100644 index 0000000..882117d --- /dev/null +++ b/test/organizer_test.exs @@ -0,0 +1,65 @@ +defmodule OrganizerTest do + use ExUnit.Case + # doctest Organizer + + defmodule SimpleExample do + use Interactor + def handle_call(%{foo: :bar}), do: {:ok, %{bar: :foo}} + def cleanup(%{bar: :foo}), do: {:ok, %{foo: :bar}} + end + + defmodule SuccessExample do + use Interactor + def handle_call(%{bar: :foo}), do: {:ok, true} + end + + defmodule FailureExample do + use Interactor + def handle_call(%{bar: :foo}) do + {:error, "oh no"} + end + end + + defmodule SimpleExample2 do + use Interactor + def handle_call(_args), do: {:ok, %{bar: :foo, baz: :quux}} + def cleanup(%{bar: :foo, baz: :quux}), do: {:ok, %{bar: :foo}} + end + + defmodule ExceptionExample do + use Interactor + def handle_call(%{bar: :foo}) do + raise "A HUGE EXCEPTION" + end + end + + defmodule MyOrganizer do + use Interactor.Organizer + + organize [SimpleExample, SuccessExample] + end + + defmodule CleanupOrganizer do + use Interactor.Organizer + + organize [SimpleExample, FailureExample] + end + + defmodule ExceptionOrganizer do + use Interactor.Organizer + + organize [SimpleExample, SimpleExample2, ExceptionExample] + end + + test "it works just fine" do + assert {:ok, true} == Interactor.call(MyOrganizer, %{foo: :bar}) + end + + test "clean up after" do + assert {:error, "oh no", %{foo: :bar}} == Interactor.call(CleanupOrganizer, %{foo: :bar}) + end + + test "it cleans up even if it catches an exception" do + assert {:error, %RuntimeError{message: "A HUGE EXCEPTION"}, %{foo: :bar}} == Interactor.call(ExceptionOrganizer, %{foo: :bar}) + end +end