From a58fd32c2e680c04e768e5a78e3510968036f597 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Thu, 24 Nov 2016 11:50:45 -0500 Subject: [PATCH 1/8] First pass on builder style interactors. Heavily influenced by the DSL and code of Plug.Builder. --- lib/interactor.ex | 33 ++++++-- lib/interactor/builder.ex | 137 +++++++++++++++++++++++++++++++ lib/interactor/interaction.ex | 7 ++ test/interactor/builder_test.exs | 60 ++++++++++++++ 4 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 lib/interactor/builder.ex create mode 100644 lib/interactor/interaction.ex create mode 100644 test/interactor/builder_test.exs diff --git a/lib/interactor.ex b/lib/interactor.ex index 9487392..e546560 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -40,16 +40,23 @@ defmodule Interactor do """ @doc """ + Warning: Deprecated + The primary callback. Typically returns an Ecto.Changeset or an Ecto.Multi. """ @callback handle_call(map) :: any @doc """ + Warning: Deprecated + A callback executed before handle_call. Useful for normalizing inputs. """ @callback before_call(map) :: map @doc """ + Warning: Deprecated + + A callback executed after handle_call and after the Repo executes. Useful for publishing events, tracking metrics, and other non-transaction @@ -57,6 +64,9 @@ defmodule Interactor do """ @callback after_call(any) :: any + @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts} + @callback call(Interactor.Interaction.t, opts) :: Interactor.Interaction.t + @doc """ Executes the `before_call/1`, `handle_call/1`, and `after_call/1` callbacks. @@ -64,13 +74,10 @@ 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 - def call(interactor, context) do - context - |> interactor.before_call - |> interactor.handle_call - |> Interactor.Handler.handle(interactor.__repo) - |> interactor.after_call + #@spec call(module, map, key) :: Interaction.id | any + def call(interactor, assigns, opts \\ []) do + %Interactor.Interaction{assigns: assigns} + |> interactor.call(opts) end @doc """ @@ -119,8 +126,18 @@ defmodule Interactor do quote do def before_call(c), do: c def after_call(r), do: r + def handle_call(r), do: c + + def call(%{assigns: assigns}, _) do + IO.puts("Warning: using deprecated 0.1.0 behaviour, please see README and CHANGELOG for upgrade instructions. This functionality will be removed in 0.3.0") + assigns + |> before_call + |> handle_call + |> Interactor.Handler.handle(__repo) + |> after_call + end - defoverridable [before_call: 1, after_call: 1] + defoverridable [before_call: 1, after_call: 1, handle_call: 1, call: 2] end end diff --git a/lib/interactor/builder.ex b/lib/interactor/builder.ex new file mode 100644 index 0000000..0de746f --- /dev/null +++ b/lib/interactor/builder.ex @@ -0,0 +1,137 @@ +defmodule Interactor.Builder do + alias Interactor.Interaction + + @moduledoc """ + + + The Interactor.Builer module functionality and code is **heavily** influenced + and copied from the Plug.Builder code. + TODO. + + Example: + + def Example.CreatePost do + use Interactor.Interaction + import Ecto.Changeset + + interactor :post_changeset + interactor Interactor.Ecto, from: :post_changeset, to: post + interactor Example.SyncToSocket, async: true + interactor :push_to_rss_service, async: true + + def post_changeset(%{assigns: %{attributes: attrs}}, _) do + cast(%Example.Post, attrs, [:title, :body]) + end + + def push_to_rss_service(interaction, _) do + # ... External service call ... + interaction + end + end + + """ + + @type interactor :: module | atom + + @doc """ + + """ + defmacro interactor(interactor, opts \\ []) do + quote do + @interactors {unquote(interactor), unquote(opts), true} + end + end + + @doc false + defmacro __using__(_opts) do + quote do + @behaviour Interactor + import Interactor.Builder, only: [interactor: 1, interactor: 2] + import Interactor.Interaction # TODO, is this a good idea? assign/3 could conflict + + def call(interaction, opts) do + interactor_builder_call(interaction, opts) + end + + defoverridable [call: 2] + + Module.register_attribute(__MODULE__, :interactors, accumulate: true) + @before_compile Interactor.Builder + end + end + + @doc false + defmacro __before_compile__(env) do + interactors = Module.get_attribute(env.module, :interactors) + {interaction, body} = Interactor.Builder.compile(env, interactors) + + quote do + defp interactor_builder_call(unquote(interaction), _), do: unquote(body) + end + end + + @doc false + #@spec compile(Macro.Env.t, [{interactor, Interactor.opts, Macro.t}]) :: {Macro.t, Macro.t} + def compile(env, pipeline) do + interaction = quote do: interaction + {interaction, Enum.reduce(pipeline, interaction, "e_interactor(&1, &2, env))} + end + + # `acc` is a series of nested interactor calls in the form of + # interactor3(interactor2(interactor1(interaction))). + # `quote_interactor` wraps a new interactor around that series of calls. + defp quote_interactor({interactor, opts, guards}, acc, env) do + call = quote_interactor_call(interactor, opts) + + {fun, meta, [arg, [do: clauses]]} = + quote do + case unquote(compile_guards(call, guards)) do + %Interactor.Interaction{success: false} = interaction -> interaction + %Interactor.Interaction{} = interaction -> unquote(acc) + # In "other" cases interaction is binding from previous interactor + {:ok, other} -> + interaction = Interactor.Interaction.assign(interaction, unquote(interactor), other) + unquote(acc) + {:error, error} -> + %{interaction | success: false, error: error} + other -> + interaction = Interactor.Interaction.assign(interaction, unquote(interactor), other) + unquote(acc) + end + end + + generated? = :erlang.system_info(:otp_release) >= '19' + + clauses = Enum.map(clauses, fn {:->, meta, args} -> + if generated? do + {:->, [generated: true] ++ meta, args} + else + {:->, Keyword.put(meta, :line, -1), args} + end + end) + + {fun, meta, [arg, [do: clauses]]} + end + + defp quote_interactor_call(interactor, opts) do + case Atom.to_char_list(interactor) do + ~c"Elixir." ++ _ -> + quote do: unquote(interactor).call(interaction, unquote(Macro.escape(opts))) + _ -> + quote do: unquote(interactor)(interaction, unquote(Macro.escape(opts))) + end + end + + defp compile_guards(call, true) do + call + end + + defp compile_guards(call, guards) do + quote do + case true do + true when unquote(guards) -> unquote(call) + true -> conn + end + end + end +end diff --git a/lib/interactor/interaction.ex b/lib/interactor/interaction.ex new file mode 100644 index 0000000..baa239a --- /dev/null +++ b/lib/interactor/interaction.ex @@ -0,0 +1,7 @@ +defmodule Interactor.Interaction do + defstruct [assigns: %{}, success: true, error: nil] + + def assign(%__MODULE__{} = interaction, key, val) do + Map.update!(interaction, :assigns, &(Map.put(&1, key, val))) + end +end diff --git a/test/interactor/builder_test.exs b/test/interactor/builder_test.exs new file mode 100644 index 0000000..a314403 --- /dev/null +++ b/test/interactor/builder_test.exs @@ -0,0 +1,60 @@ +defmodule Interactor.BuilderTest do + use ExUnit.Case + + defmodule Two do + use Interactor.Builder + + interactor :two + interactor :three + interactor :four + + def two(i,_), do: assign(i, :two, "two") + def three(_,_), do: "three" + def four(_,_), do: "four" + end + + + defmodule One do + use Interactor.Builder + + interactor :one + interactor Two + interactor :five + + def one(_,_), do: {:ok, "one"} + def five(_,_), do: "five" + end + + defmodule FailOne do + use Interactor.Builder + + interactor :one + interactor :two + interactor :three + + def one(_,_), do: {:ok, "one"} + def two(_,_), do: {:error, "error"} + def three(_,_), do: {:ok, "three"} + end + + test "success assigns" do + interaction = %Interactor.Interaction{} = Interactor.call(One, %{}) + assert interaction.success + assert interaction.assigns == %{ + one: "one", + two: "two", + three: "three", + four: "four", + five: "five", + } + end + + test "failure assigns" do + interaction = %Interactor.Interaction{} = Interactor.call(FailOne, %{}) + refute interaction.success + assert interaction.assigns == %{ + one: "one", + } + assert interaction.error == "error" + end +end From 7ab01e194a8835bf4b38c860a29c4e75dd2db683 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Sat, 26 Nov 2016 10:42:51 -0500 Subject: [PATCH 2/8] Support assign_to. --- lib/interactor/builder.ex | 9 +++++++-- test/interactor/builder_test.exs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/interactor/builder.ex b/lib/interactor/builder.ex index 0de746f..8823792 100644 --- a/lib/interactor/builder.ex +++ b/lib/interactor/builder.ex @@ -82,6 +82,7 @@ defmodule Interactor.Builder do # `quote_interactor` wraps a new interactor around that series of calls. defp quote_interactor({interactor, opts, guards}, acc, env) do call = quote_interactor_call(interactor, opts) + assign_to = determine_assign_to(interactor, opts) {fun, meta, [arg, [do: clauses]]} = quote do @@ -90,12 +91,12 @@ defmodule Interactor.Builder do %Interactor.Interaction{} = interaction -> unquote(acc) # In "other" cases interaction is binding from previous interactor {:ok, other} -> - interaction = Interactor.Interaction.assign(interaction, unquote(interactor), other) + interaction = Interactor.Interaction.assign(interaction, unquote(assign_to), other) unquote(acc) {:error, error} -> %{interaction | success: false, error: error} other -> - interaction = Interactor.Interaction.assign(interaction, unquote(interactor), other) + interaction = Interactor.Interaction.assign(interaction, unquote(assign_to), other) unquote(acc) end end @@ -122,6 +123,10 @@ defmodule Interactor.Builder do end end + defp determine_assign_to(interactor, opts) do + opts[:assign_to] || interactor + end + defp compile_guards(call, true) do call end diff --git a/test/interactor/builder_test.exs b/test/interactor/builder_test.exs index a314403..38e1f9c 100644 --- a/test/interactor/builder_test.exs +++ b/test/interactor/builder_test.exs @@ -6,7 +6,7 @@ defmodule Interactor.BuilderTest do interactor :two interactor :three - interactor :four + interactor :four, assign_to: :for def two(i,_), do: assign(i, :two, "two") def three(_,_), do: "three" @@ -44,7 +44,7 @@ defmodule Interactor.BuilderTest do one: "one", two: "two", three: "three", - four: "four", + for: "four", five: "five", } end From ac3b2a978573b9c38be0e6031be2ff8973d91172 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Sun, 27 Nov 2016 12:18:17 -0500 Subject: [PATCH 3/8] Move existing behaviour to Interactor.Legecy. Enables: * Supporting async/task as strategies (opt `strategy: :async`) * `Interactor.call/2` always returns an `%Interaction{}` --- lib/interactor.ex | 168 +++++++++++--------------------- lib/interactor/builder.ex | 25 ++--- lib/interactor/legacy.ex | 135 +++++++++++++++++++++++++ test/interactor/legacy_test.exs | 82 ++++++++++++++++ test/interactor_test.exs | 85 +++++----------- 5 files changed, 301 insertions(+), 194 deletions(-) create mode 100644 lib/interactor/legacy.ex create mode 100644 test/interactor/legacy_test.exs diff --git a/lib/interactor.ex b/lib/interactor.ex index e546560..49c4f33 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -1,145 +1,89 @@ defmodule Interactor do use Behaviour alias Interactor.TaskSupervisor + alias Interactor.Interaction @moduledoc """ A tool for modeling events that happen in your application. - TODO: More on interactor concept + #TODO: Docs, Examples, WHY - Interactor provided a behaviour and functions to execute the behaviours. - - To use simply `use Interactor` in a module and implement the `handle_call/1` - 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: - - * `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 - - Interactors can be called in three ways: - - * `Interactor.call/2` - Executes callbacks, optionaly insert, and return results. - * `Interactor.call_task/2` - Same as call, but returns a `Task` that can be awated on. - * `Interactor.call_aysnc/2` - Same as call, but does not return results. - - Example: - - defmodule CreateArticle do - use Interactor, repo: Repo - - def handle_call(%{attributes: attrs, author: author}) do - cast(%Article{}, attrs, [:title, :body]) - |> put_change(:author_id, author.id) - end - end - - Interactor.call(CreateArticle, %{attributes: params, author: current_user}) """ - @doc """ - Warning: Deprecated - - The primary callback. Typically returns an Ecto.Changeset or an Ecto.Multi. - """ - @callback handle_call(map) :: any - - @doc """ - Warning: Deprecated - - A callback executed before handle_call. Useful for normalizing inputs. - """ - @callback before_call(map) :: map + @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts} @doc """ - Warning: Deprecated + Primary interactor callback. + #TODO: Docs, Examples, explain return values and assign_to - A callback executed after handle_call and after the Repo executes. - - Useful for publishing events, tracking metrics, and other non-transaction - worthy calls. """ - @callback after_call(any) :: any - - @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts} - @callback call(Interactor.Interaction.t, opts) :: Interactor.Interaction.t + @callback call(Interaction.t, opts) :: Interaction.t | {:ok, any} | {:error, any} | any @doc """ - Executes the `before_call/1`, `handle_call/1`, and `after_call/1` callbacks. - - If an Ecto.Changeset or Ecto.Multi is returned by `handle_call/1` and a - `repo` options was passed to `use Interactor` the changeset or multi will be - executed and the results returned. - """ - #@spec call(module, map, key) :: Interaction.id | any - def call(interactor, assigns, opts \\ []) do - %Interactor.Interaction{assigns: assigns} - |> interactor.call(opts) - end + Call an Interactor. - @doc """ - Wraps `call/2` in a supervised Task. Returns the Task. + #TODO: Docs, Examples - Useful if you want async, but want to await results. """ - @spec call_task(module, map) :: Task.t - def call_task(interactor, map) do - Task.Supervisor.async(TaskSupervisor, Interactor, :call, [interactor, map]) + @spec call(module | {module, atom}, Interaction.t | map, Keyword.t) :: Interaction.t + def call(interactor, interaction, opts \\ []) + def call({interactor, fun}, %Interaction{} = interaction, opts), + do: do_call({interactor, fun}, interaction, opts[:strategy], opts) + def call(interactor, %Interaction{} = i, opts), + do: call({interactor, :call}, i, opts) + def call(interactor, assigns, opts), + do: call(interactor, %Interaction{assigns: assigns}, opts) + + defp do_call({interactor, fun}, interaction, nil, opts) do + assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) + case apply(interactor, fun, [interaction, opts]) do + # When interaction is returned do nothing + %Interaction{} = interaction -> interaction + # Otherwise properly add result to interaction + {:error, error} -> %{interaction | success: false, error: error} + {:ok, other} -> Interaction.assign(interaction, assign_to, other) + other -> Interaction.assign(interaction, assign_to, other) + end end - @doc """ - Executes `call/2` asynchronously via a supervised task. Returns {:ok, pid}. - - Primary use case is task you want completely asynchronos with no care for - return values. + defp do_call({interactor, fun}, interaction, :task, opts) do + assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) + task = Task.Supervisor.async(TaskSupervisor, fn() -> + apply(interactor, fun, [interaction, opts]) + end) - Async can be disabled in tests by setting (will still return {:ok, pid}): - - config :interactor, - force_syncronous_tasks: true + Interaction.assign(interaction, assign_to, task) + end - """ - @spec call_async(module, map) :: {:ok, pid} - def call_async(interactor, map) do - if sync_tasks do - t = Task.Supervisor.async(TaskSupervisor, Interactor, :call, [interactor, map]) - Task.await(t) - {:ok, t.pid} + defp do_call({interactor, fun}, interaction, :async, opts) do + assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) + {:ok, pid} = if sync_tasks do + task = Task.Supervisor.async(TaskSupervisor, fn() -> + apply(interactor, fun, [interaction, opts]) + end) + Task.await(task) + {:ok, task.pid} else - Task.Supervisor.start_child(TaskSupervisor, Interactor, :call, [interactor, map]) + Task.Supervisor.start_child(TaskSupervisor, fn() -> + apply(interactor, fun, [interaction, opts]) + end) end - end - defmacro __using__(opts) do - quote do - @behaviour Interactor - @doc false - def __repo, do: unquote(opts[:repo]) - unquote(define_callback_defaults) - end + Interaction.assign(interaction, assign_to, pid) end - defp define_callback_defaults do - quote do - def before_call(c), do: c - def after_call(r), do: r - def handle_call(r), do: c - - def call(%{assigns: assigns}, _) do - IO.puts("Warning: using deprecated 0.1.0 behaviour, please see README and CHANGELOG for upgrade instructions. This functionality will be removed in 0.3.0") - assigns - |> before_call - |> handle_call - |> Interactor.Handler.handle(__repo) - |> after_call - end - - defoverridable [before_call: 1, after_call: 1, handle_call: 1, call: 2] - end + defp determine_assign_to(module, :call, nil) do + module + |> Atom.to_string + |> String.split(".") + |> Enum.reverse + |> hd + |> Macro.underscore + |> String.to_atom end + defp determine_assign_to(_module, fun, nil), do: fun + defp determine_assign_to(_module, _fun, assign_to), do: assign_to defp sync_tasks do Application.get_env(:interactor, :force_syncronous_tasks, false) diff --git a/lib/interactor/builder.ex b/lib/interactor/builder.ex index 8823792..897522e 100644 --- a/lib/interactor/builder.ex +++ b/lib/interactor/builder.ex @@ -1,11 +1,9 @@ defmodule Interactor.Builder do - alias Interactor.Interaction - @moduledoc """ The Interactor.Builer module functionality and code is **heavily** influenced - and copied from the Plug.Builder code. + and copied from the Plug.Builder code. TODO. Example: @@ -82,22 +80,12 @@ defmodule Interactor.Builder do # `quote_interactor` wraps a new interactor around that series of calls. defp quote_interactor({interactor, opts, guards}, acc, env) do call = quote_interactor_call(interactor, opts) - assign_to = determine_assign_to(interactor, opts) {fun, meta, [arg, [do: clauses]]} = quote do case unquote(compile_guards(call, guards)) do %Interactor.Interaction{success: false} = interaction -> interaction %Interactor.Interaction{} = interaction -> unquote(acc) - # In "other" cases interaction is binding from previous interactor - {:ok, other} -> - interaction = Interactor.Interaction.assign(interaction, unquote(assign_to), other) - unquote(acc) - {:error, error} -> - %{interaction | success: false, error: error} - other -> - interaction = Interactor.Interaction.assign(interaction, unquote(assign_to), other) - unquote(acc) end end @@ -114,19 +102,18 @@ defmodule Interactor.Builder do {fun, meta, [arg, [do: clauses]]} end + # Use Interactor.call to execute the Interactor. + # Always returns an interaction, but handles async strategies, assigning + # values, etc. defp quote_interactor_call(interactor, opts) do case Atom.to_char_list(interactor) do ~c"Elixir." ++ _ -> - quote do: unquote(interactor).call(interaction, unquote(Macro.escape(opts))) + quote do: Interactor.call({unquote(interactor), :call}, interaction, unquote(Macro.escape(opts))) _ -> - quote do: unquote(interactor)(interaction, unquote(Macro.escape(opts))) + quote do: Interactor.call({__MODULE__, unquote(interactor)}, interaction, unquote(Macro.escape(opts))) end end - defp determine_assign_to(interactor, opts) do - opts[:assign_to] || interactor - end - defp compile_guards(call, true) do call end diff --git a/lib/interactor/legacy.ex b/lib/interactor/legacy.ex new file mode 100644 index 0000000..43778e6 --- /dev/null +++ b/lib/interactor/legacy.ex @@ -0,0 +1,135 @@ +defmodule Interactor.Legacy do + use Behaviour + alias Interactor.TaskSupervisor + + @moduledoc """ + Legacy Interactor Behaviour. + + When updating from 0.1.0 to 0.2.0 you can replace `use Interactor` with + `use Interactor.Legacy`. You can also `alias Interactor.Legacy, as: Interactor` + to ensure `Interactor.call/2`, `Interactor.call_async/2`, and + `Interactor.call_task/2` continue working as expected. + + A tool for modeling events that happen in your application. + + Interactor provided a behaviour and functions to execute the behaviours. + + To use simply `use Interactor` in a module and implement the `handle_call/1` + 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: + + * `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 + + Interactors can be called in three ways: + + * `Interactor.call/2` - Executes callbacks, optionaly insert, and return results. + * `Interactor.call_task/2` - Same as call, but returns a `Task` that can be awated on. + * `Interactor.call_aysnc/2` - Same as call, but does not return results. + + Example: + + defmodule CreateArticle do + use Interactor, repo: Repo + + def handle_call(%{attributes: attrs, author: author}) do + cast(%Article{}, attrs, [:title, :body]) + |> put_change(:author_id, author.id) + end + end + + Interactor.call(CreateArticle, %{attributes: params, author: current_user}) + """ + + @doc """ + The primary callback. Typically returns an Ecto.Changeset or an Ecto.Multi. + """ + @callback handle_call(map) :: any + + @doc """ + A callback executed before handle_call. Useful for normalizing inputs. + """ + @callback before_call(map) :: map + + @doc """ + A callback executed after handle_call and after the Repo executes. + + Useful for publishing events, tracking metrics, and other non-transaction + worthy calls. + """ + @callback after_call(any) :: any + + @doc """ + Executes the `before_call/1`, `handle_call/1`, and `after_call/1` callbacks. + + If an Ecto.Changeset or Ecto.Multi is returned by `handle_call/1` and a + `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 + def call(interactor, context) do + context + |> interactor.before_call + |> interactor.handle_call + |> Interactor.Handler.handle(interactor.__repo) + |> interactor.after_call + end + + @doc """ + Wraps `call/2` in a supervised Task. Returns the Task. + + Useful if you want async, but want to await results. + """ + @spec call_task(module, map) :: Task.t + def call_task(interactor, map) do + Task.Supervisor.async(TaskSupervisor, __MODULE__, :call, [interactor, map]) + end + + @doc """ + Executes `call/2` asynchronously via a supervised task. Returns {:ok, pid}. + + Primary use case is task you want completely asynchronos with no care for + return values. + + Async can be disabled in tests by setting (will still return {:ok, pid}): + + config :interactor, + force_syncronous_tasks: true + + """ + @spec call_async(module, map) :: {:ok, pid} + def call_async(interactor, map) do + if sync_tasks do + t = Task.Supervisor.async(TaskSupervisor, __MODULE__, :call, [interactor, map]) + Task.await(t) + {:ok, t.pid} + else + Task.Supervisor.start_child(TaskSupervisor, __MODULE__, :call, [interactor, map]) + end + end + + defmacro __using__(opts) do + quote do + @behaviour Interactor.Legacy + @doc false + def __repo, do: unquote(opts[:repo]) + unquote(define_callback_defaults) + end + end + + defp define_callback_defaults do + quote do + def before_call(c), do: c + def after_call(r), do: r + + defoverridable [before_call: 1, after_call: 1] + end + end + + defp sync_tasks do + Application.get_env(:interactor, :force_syncronous_tasks, false) + end +end diff --git a/test/interactor/legacy_test.exs b/test/interactor/legacy_test.exs new file mode 100644 index 0000000..04f2e8e --- /dev/null +++ b/test/interactor/legacy_test.exs @@ -0,0 +1,82 @@ +defmodule Interactor.LegacyTest do + use ExUnit.Case + doctest Interactor + + defmodule Foo do + use Ecto.Schema + + schema "foos" do + field :foo, :string + end + end + + # We don't need to test ecto, just handle repo calls and return something. + defmodule FakeRepo do + def insert_or_update(changeset) do + %Foo{foo: Ecto.Changeset.get_field(changeset, :foo)} + end + + def transaction(fun) when is_function(fun), do: fun.() + def transaction(%Ecto.Multi{} = multi) do + foos = multi + |> Ecto.Multi.to_list + |> Enum.reduce(%{}, fn({key, {_, cs, _}}, m) -> + Map.put(m, key, insert_or_update(cs)) + end) + {:ok, foos} + end + end + + defmodule SimpleExample do + use Interactor.Legacy + def handle_call(%{foo: bar}), do: {:ok, "foo" <> bar} + end + + defmodule ChangesetExample do + use Interactor.Legacy, repo: FakeRepo + import Ecto.Changeset + def handle_call(params), do: cast(%Foo{}, params, [:foo]) + end + + defmodule MultiExample do + use Interactor.Legacy, repo: FakeRepo + alias Ecto.Multi + def handle_call(%{foo1: foo1, foo2: foo2}) do + Multi.new + |> Multi.insert(:foo1, ChangesetExample.handle_call(%{foo: foo1})) + |> Multi.insert(:foo2, ChangesetExample.handle_call(%{foo: foo2})) + end + end + + test "simple - calling call" do + assert {:ok, "foobar"} = Interactor.Legacy.call(SimpleExample, %{foo: "bar"}) + end + + test "simple - calling call_task" do + task = Interactor.Legacy.call_task(SimpleExample, %{foo: "bar"}) + assert {:ok, "foobar"} = Task.await(task) + end + + test "changeset - calling call" do + foo = Interactor.Legacy.call(ChangesetExample, %{foo: "bar"}) + assert foo.foo == "bar" + end + + test "changeset - calling call_task" do + task = Interactor.Legacy.call_task(ChangesetExample, %{foo: "bar"}) + foo = Task.await(task) + assert foo.foo == "bar" + end + + test "multi - calling call_async" do + results = Interactor.Legacy.call_async(MultiExample, %{foo1: "bar", foo2: "baz"}) + assert {:ok, _} = results + end + + test "multi - calling call_task" do + task = Interactor.Legacy.call_task(MultiExample, %{foo1: "bar", foo2: "baz"}) + assert {:ok, %{foo1: foo1, foo2: foo2}} = Task.await(task) + assert foo1.foo == "bar" + assert foo2.foo == "baz" + end +end diff --git a/test/interactor_test.exs b/test/interactor_test.exs index dc0f099..551ef50 100644 --- a/test/interactor_test.exs +++ b/test/interactor_test.exs @@ -1,82 +1,41 @@ defmodule InteractorTest do use ExUnit.Case doctest Interactor + alias Interactor.Interaction - defmodule Foo do - use Ecto.Schema - - schema "foos" do - field :foo, :string - end + defmodule One do + @behaviour Interactor + def call(%Interaction{} = int, _opts), do: Interaction.assign(int, :one, 1) end - # We don't need to test ecto, just handle repo calls and return something. - defmodule FakeRepo do - def insert_or_update(changeset) do - %Foo{foo: Ecto.Changeset.get_field(changeset, :foo)} - end - - def transaction(fun) when is_function(fun), do: fun.() - def transaction(%Ecto.Multi{} = multi) do - foos = multi - |> Ecto.Multi.to_list - |> Enum.reduce(%{}, fn({key, {_, cs, _}}, m) -> - Map.put(m, key, insert_or_update(cs)) - end) - {:ok, foos} - end + defmodule Two do + @behaviour Interactor + def call(_interaction, _opts), do: {:ok, 2} end - defmodule SimpleExample do - use Interactor - def handle_call(%{foo: bar}), do: {:ok, "foo" <> bar} + defmodule Fail do + @behaviour Interactor + def call(_interaction, _opts), do: {:error, "error"} end - defmodule ChangesetExample do - use Interactor, repo: FakeRepo - import Ecto.Changeset - def handle_call(params), do: cast(%Foo{}, params, [:foo]) + test "call/2 - %Interaction{} returned" do + assert %Interaction{assigns: assigns} = Interactor.call(One, %{zero: 0}) + assert assigns == %{zero: 0, one: 1} end - defmodule MultiExample do - use Interactor, repo: FakeRepo - alias Ecto.Multi - def handle_call(%{foo1: foo1, foo2: foo2}) do - Multi.new - |> Multi.insert(:foo1, ChangesetExample.handle_call(%{foo: foo1})) - |> Multi.insert(:foo2, ChangesetExample.handle_call(%{foo: foo2})) - end + test "call/2 - {:ok, 2} returned" do + assert %Interaction{assigns: assigns} = Interactor.call(Two, %{zero: 0}) + assert assigns == %{zero: 0, two: 2} end - test "simple - calling call" do - assert {:ok, "foobar"} = Interactor.call(SimpleExample, %{foo: "bar"}) + test "call/3 - {:ok, 2} returned - with assign to" do + assert %Interaction{assigns: assigns} = Interactor.call(Two, %{zero: 0}, assign_to: :too) + assert assigns == %{zero: 0, too: 2} end - test "simple - calling call_task" do - task = Interactor.call_task(SimpleExample, %{foo: "bar"}) - assert {:ok, "foobar"} = Task.await(task) + test "call/2 - {:error, 2} returned" do + assert %Interaction{success: false, error: "error", assigns: %{zero: 0}} = + Interactor.call(Fail, %{zero: 0}) end - test "changeset - calling call" do - foo = Interactor.call(ChangesetExample, %{foo: "bar"}) - assert foo.foo == "bar" - end - - test "changeset - calling call_task" do - task = Interactor.call_task(ChangesetExample, %{foo: "bar"}) - foo = Task.await(task) - assert foo.foo == "bar" - end - - test "multi - calling call_async" do - results = Interactor.call_async(MultiExample, %{foo1: "bar", foo2: "baz"}) - assert {:ok, _} = results - end - - test "multi - calling call_task" do - task = Interactor.call_task(MultiExample, %{foo1: "bar", foo2: "baz"}) - assert {:ok, %{foo1: foo1, foo2: foo2}} = Task.await(task) - assert foo1.foo == "bar" - assert foo2.foo == "baz" - end end From fd92226058b9a18e15834a3bb952f7581cffe470 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Mon, 28 Nov 2016 12:27:36 -0500 Subject: [PATCH 4/8] Move async functionality to Strategy modules. This also allows easy extension for other strategies such as Exq, Toniq, worker pools, gen server, etc. --- lib/interactor.ex | 53 +++++++++----------------------- lib/interactor/builder.ex | 1 + lib/interactor/strategy.ex | 46 +++++++++++++++++++++++++++ lib/interactor/strategy/async.ex | 45 +++++++++++++++++++++++++++ lib/interactor/strategy/sync.ex | 14 +++++++++ lib/interactor/strategy/task.ex | 45 +++++++++++++++++++++++++++ test/interactor_test.exs | 12 ++++++++ 7 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 lib/interactor/strategy.ex create mode 100644 lib/interactor/strategy/async.ex create mode 100644 lib/interactor/strategy/sync.ex create mode 100644 lib/interactor/strategy/task.ex diff --git a/lib/interactor.ex b/lib/interactor.ex index 49c4f33..574ca34 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -1,6 +1,5 @@ defmodule Interactor do use Behaviour - alias Interactor.TaskSupervisor alias Interactor.Interaction @moduledoc """ @@ -28,16 +27,24 @@ defmodule Interactor do """ @spec call(module | {module, atom}, Interaction.t | map, Keyword.t) :: Interaction.t def call(interactor, interaction, opts \\ []) - def call({interactor, fun}, %Interaction{} = interaction, opts), - do: do_call({interactor, fun}, interaction, opts[:strategy], opts) - def call(interactor, %Interaction{} = i, opts), - do: call({interactor, :call}, i, opts) + def call({module, fun}, %Interaction{} = interaction, opts), + do: do_call(module, fun, interaction, opts[:strategy], opts) + def call(module, %Interaction{} = i, opts), + do: call({module, :call}, i, opts) def call(interactor, assigns, opts), do: call(interactor, %Interaction{assigns: assigns}, opts) - defp do_call({interactor, fun}, interaction, nil, opts) do - assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) - case apply(interactor, fun, [interaction, opts]) do + defp do_call(module, fun, interaction, :sync, opts), + do: do_call(module, fun, interaction, Interactor.Strategy.Sync, opts) + defp do_call(module, fun, interaction, nil, opts), + do: do_call(module, fun, interaction, Interactor.Strategy.Sync, opts) + defp do_call(module, fun, interaction, :async, opts), + do: do_call(module, fun, interaction, Interactor.Strategy.Async, opts) + defp do_call(module, fun, interaction, :task, opts), + do: do_call(module, fun, interaction, Interactor.Strategy.Task, opts) + defp do_call(module, fun, interaction, strategy, opts) do + assign_to = determine_assign_to(module, fun, opts[:assign_to]) + case strategy.execute(module, fun, interaction, opts) do # When interaction is returned do nothing %Interaction{} = interaction -> interaction # Otherwise properly add result to interaction @@ -47,32 +54,6 @@ defmodule Interactor do end end - defp do_call({interactor, fun}, interaction, :task, opts) do - assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) - task = Task.Supervisor.async(TaskSupervisor, fn() -> - apply(interactor, fun, [interaction, opts]) - end) - - Interaction.assign(interaction, assign_to, task) - end - - defp do_call({interactor, fun}, interaction, :async, opts) do - assign_to = determine_assign_to(interactor, fun, opts[:assign_to]) - {:ok, pid} = if sync_tasks do - task = Task.Supervisor.async(TaskSupervisor, fn() -> - apply(interactor, fun, [interaction, opts]) - end) - Task.await(task) - {:ok, task.pid} - else - Task.Supervisor.start_child(TaskSupervisor, fn() -> - apply(interactor, fun, [interaction, opts]) - end) - end - - Interaction.assign(interaction, assign_to, pid) - end - defp determine_assign_to(module, :call, nil) do module |> Atom.to_string @@ -84,8 +65,4 @@ defmodule Interactor do end defp determine_assign_to(_module, fun, nil), do: fun defp determine_assign_to(_module, _fun, assign_to), do: assign_to - - defp sync_tasks do - Application.get_env(:interactor, :force_syncronous_tasks, false) - end end diff --git a/lib/interactor/builder.ex b/lib/interactor/builder.ex index 897522e..1dcded6 100644 --- a/lib/interactor/builder.ex +++ b/lib/interactor/builder.ex @@ -1,4 +1,5 @@ defmodule Interactor.Builder do + @moduledoc """ diff --git a/lib/interactor/strategy.ex b/lib/interactor/strategy.ex new file mode 100644 index 0000000..50fca79 --- /dev/null +++ b/lib/interactor/strategy.ex @@ -0,0 +1,46 @@ +defmodule Interactor.Strategy do + use Behaviour + + @moduledoc """ + An interactor strategy is how the interactor is executed. + + Built in strategies are: + + * :sync - Interactor.Strategy.Sync - default + * :async - Interactor.Strategy.Async + * :task - Interactor.Strategy.Task + + Strategies are determined with the `strategy` option, eg: + + Interactor.call(SimpleInteractor, %{foo: :bar}, strategy: :task) + + Or with Interactor.Builder: + + interactor :create_user + interactor :send_email, strategy: :async + + The default strategy is `:sync` which simply executes the interactor in the + current process and assigns the results. See docs on each strategy for more. + + Custom strategies can be used by passing a module implementing the + Interactor.Strategy behaviour as the strategy option. For example: + + interactor :create_user + interactor :send_email, strategy: MyApp.Exq + + """ + + @doc """ + Execute the interactor. + + Receives the module and function of the interactor, the interaction and the + opts. The simplest possible implementation is just to apply the function + inline, which is in fact what the :sync strategy does. + + apply(module, fun, [interaction, opts]) + + This callback should return either the %Interaction{}, {:ok, value}, or + {:error, error}. + """ + @callback execute(module, atom, Interaction.t, Keyword.t) :: Interaction.t | {:ok, any} | {:error, any} +end diff --git a/lib/interactor/strategy/async.ex b/lib/interactor/strategy/async.ex new file mode 100644 index 0000000..babcc96 --- /dev/null +++ b/lib/interactor/strategy/async.ex @@ -0,0 +1,45 @@ +defmodule Interactor.Strategy.Async do + @behaviour Interactor.Strategy + alias Interactor.TaskSupervisor + + @moduledoc """ + Execute interaction asynchronously, only return value is PID. + + To use: + + interactor :update_view_count, strategy: :async + + or: + + interactor :update_view_count, strategy: Interactor.Strategy.Async + + + When running tests async interactors can be forced to run synchronously by setting the following config. Return values will still be pids. + + config :interactor, + force_syncronous_tasks: true + """ + + @doc """ + Execute interactor asynchronously in a supervised fashion. + + Returns pid for assignment in interaction. + """ + def execute(module, fun, interaction, opts) do + if sync_tasks do + task = Task.Supervisor.async(TaskSupervisor, fn() -> + apply(module, fun, [interaction, opts]) + end) + Task.await(task) + {:ok, task.pid} + else + Task.Supervisor.start_child(TaskSupervisor, fn() -> + apply(module, fun, [interaction, opts]) + end) + end + end + + defp sync_tasks do + Application.get_env(:interactor, :force_syncronous_tasks, false) + end +end diff --git a/lib/interactor/strategy/sync.ex b/lib/interactor/strategy/sync.ex new file mode 100644 index 0000000..6ad1c6a --- /dev/null +++ b/lib/interactor/strategy/sync.ex @@ -0,0 +1,14 @@ +defmodule Interactor.Strategy.Sync do + @behaviour Interactor.Strategy + + @moduledoc """ + Execute interaction synchronously. Default strategy. + """ + + @doc """ + Execute interactor in current process. + """ + def execute(module, fun, interaction, opts) do + apply(module, fun, [interaction, opts]) + end +end diff --git a/lib/interactor/strategy/task.ex b/lib/interactor/strategy/task.ex new file mode 100644 index 0000000..2759565 --- /dev/null +++ b/lib/interactor/strategy/task.ex @@ -0,0 +1,45 @@ +defmodule Interactor.Strategy.Task do + @behaviour Interactor.Strategy + alias Interactor.TaskSupervisor + import Interactor.Interaction + + @moduledoc """ + Execute interaction in a task, return value is a Task which is assigned to interaction. + + To use: + + interactor :do_work, strategy: :task + + or: + + interactor :do_work, strategy: Interactor.Strategy.Task + + Interaction with %Task{} in assigns can be all waited on with Interactor.Strategy.Task.await/1. + """ + + @doc """ + Execute interactor in a supervised task. + + Task is returned for assignment in interaction. + """ + def execute(module, fun, interaction, opts) do + Task.Supervisor.async TaskSupervisor, fn() -> + apply(module, fun, [interaction, opts]) + end + end + + @doc """ + Await all tasks in assigns, return interaction with fulfilled values replacing tasks. + """ + def await(interaction) do + Enum.reduce interaction.assigns, interaction, fn + {k, %Task{} = t}, interaction -> + val = case Task.await(t) do + {:ok, val} -> val + val -> val + end + assign(interaction, k, val) + _, interaction -> interaction + end + end +end diff --git a/test/interactor_test.exs b/test/interactor_test.exs index 551ef50..59d24f9 100644 --- a/test/interactor_test.exs +++ b/test/interactor_test.exs @@ -38,4 +38,16 @@ defmodule InteractorTest do Interactor.call(Fail, %{zero: 0}) end + test "call/2 - async - Interaction returned" do + assert %Interaction{assigns: assigns} = Interactor.call(One, %{zero: 0}, strategy: :async) + assert %{zero: 0, one: pid} = assigns + assert is_pid(pid) + end + + test "call/2 - task - Interaction returned" do + assert %Interaction{} = int = Interactor.call(Two, %{zero: 0}, strategy: :task) + assert %{zero: 0, two: %Task{}} = int.assigns + int = Interactor.Strategy.Task.await(int) + assert %{zero: 0, two: 2} = int.assigns + end end From 50514e7be94a49fc973ae3c6c46e7b76e209a61b Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Tue, 29 Nov 2016 10:58:00 -0500 Subject: [PATCH 5/8] Add rollback support. --- lib/interactor.ex | 34 +++++++++++++++++++---- lib/interactor/interaction.ex | 41 +++++++++++++++++++++++++++- test/interactor/interaction_test.exs | 30 ++++++++++++++++++++ test/interactor_test.exs | 8 ++++++ 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 test/interactor/interaction_test.exs diff --git a/lib/interactor.ex b/lib/interactor.ex index 574ca34..7e06330 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -19,6 +19,12 @@ defmodule Interactor do """ @callback call(Interaction.t, opts) :: Interaction.t | {:ok, any} | {:error, any} | any + @doc """ + Optional callback to be executed if interactors up the chain return an error. When using Interaction.Builder prefer the `rollback` option. + """ + @callback rollback(Interaction.t) :: Interaction.t + @optional_callbacks rollback: 1 + @doc """ Call an Interactor. @@ -44,13 +50,20 @@ defmodule Interactor do do: do_call(module, fun, interaction, Interactor.Strategy.Task, opts) defp do_call(module, fun, interaction, strategy, opts) do assign_to = determine_assign_to(module, fun, opts[:assign_to]) + rollback = determine_rollback(module, fun, opts[:rollback]) case strategy.execute(module, fun, interaction, opts) do - # When interaction is returned do nothing - %Interaction{} = interaction -> interaction - # Otherwise properly add result to interaction - {:error, error} -> %{interaction | success: false, error: error} - {:ok, other} -> Interaction.assign(interaction, assign_to, other) - other -> Interaction.assign(interaction, assign_to, other) + %Interaction{} = interaction -> + Interaction.add_rollback(interaction, rollback) + {:error, error} -> + Interaction.rollback(%{interaction | success: false, error: error}) + {:ok, other} -> + interaction + |> Interaction.assign(assign_to, other) + |> Interaction.add_rollback(rollback) + other -> + interaction + |> Interaction.assign(assign_to, other) + |> Interaction.add_rollback(rollback) end end @@ -65,4 +78,13 @@ defmodule Interactor do end defp determine_assign_to(_module, fun, nil), do: fun defp determine_assign_to(_module, _fun, assign_to), do: assign_to + + defp determine_rollback(module, :call, nil) do + if {:rollback, 1} in module.__info__(:functions) do + {module, :rollback} + end + end + defp determine_rollback(_module, _fun, nil), do: nil + defp determine_rollback(module, _fun, rollback), do: {module, rollback} + end diff --git a/lib/interactor/interaction.ex b/lib/interactor/interaction.ex index baa239a..40a589a 100644 --- a/lib/interactor/interaction.ex +++ b/lib/interactor/interaction.ex @@ -1,7 +1,46 @@ defmodule Interactor.Interaction do - defstruct [assigns: %{}, success: true, error: nil] + @moduledoc """ + An interaction holds the state to be passed between Interactors. + """ + defstruct [assigns: %{}, success: true, error: nil, rollback: []] + + @type t :: %__MODULE__{ + assigns: Map.t, + success: boolean, + error: nil | any, + rollback: [{module, atom}], + } + + @doc """ + Assign a value to the interaction's assigns map. + """ + @spec assign(Interaction.t, atom, any) :: Interaction.t def assign(%__MODULE__{} = interaction, key, val) do Map.update!(interaction, :assigns, &(Map.put(&1, key, val))) end + + @doc """ + Push a rollback function into the interaction's rollback list. + """ + @spec add_rollback(Interaction.t, nil | {module, atom}) :: Interaction.t + def add_rollback(%__MODULE__{} = interaction, nil), do: interaction + def add_rollback(%__MODULE__{} = interaction, {module, fun}) do + Map.update!(interaction, :rollback, &([{module, fun} | &1])) + end + + @doc """ + Execute all rollback functions in reverse of the order they were added. + + Called when an interactor up the chain returns {:error, anyvalue}. + + NOTE: Rollback for the interactor that fails is not called, only previously + successful interactors have rollback called. + """ + @spec rollback(Interaction.t) :: Interaction.t + def rollback(%__MODULE__{} = interaction) do + Enum.reduce interaction.rollback, interaction, fn({mod, fun}, i) -> + apply(mod, fun, [i]) + end + end end diff --git a/test/interactor/interaction_test.exs b/test/interactor/interaction_test.exs new file mode 100644 index 0000000..e611594 --- /dev/null +++ b/test/interactor/interaction_test.exs @@ -0,0 +1,30 @@ +defmodule Interactor.InteractionTest do + use ExUnit.Case + alias Interactor.Interaction + import Interaction + + test "assigns" do + assert %Interaction{assigns: %{foo: :bar}} == + assign(%Interaction{}, :foo, :bar) + end + + test "add_rollback" do + assert %Interaction{rollback: []} == + add_rollback(%Interaction{}, nil) + + assert %Interaction{rollback: [{Foo, :bar}]} == + add_rollback(%Interaction{}, {Foo, :bar}) + end + + test "rollback" do + interaction = %Interaction{} + |> add_rollback({__MODULE__, :rollback1}) + |> add_rollback({__MODULE__, :rollback2}) + + assert %Interaction{assigns: %{one: 1, two: 2}} = + rollback(interaction) + end + + def rollback1(int), do: assign(int, :one, 1) + def rollback2(int), do: assign(int, :two, 2) +end diff --git a/test/interactor_test.exs b/test/interactor_test.exs index 59d24f9..8d90e06 100644 --- a/test/interactor_test.exs +++ b/test/interactor_test.exs @@ -11,6 +11,7 @@ defmodule InteractorTest do defmodule Two do @behaviour Interactor def call(_interaction, _opts), do: {:ok, 2} + def rollback(interaction), do: Interaction.assign(interaction, :two, 0) end defmodule Fail do @@ -50,4 +51,11 @@ defmodule InteractorTest do int = Interactor.Strategy.Task.await(int) assert %{zero: 0, two: 2} = int.assigns end + + test "rollback/1" do + assert %Interaction{} = int = Interactor.call(Two, %{zero: 0}) + assert %{zero: 0, two: 2} = int.assigns + int = Interaction.rollback(int) + assert %{zero: 0, two: 0} = int.assigns + end end From 5bc6fcb1ce80dc393c014db33c761ad52f5c5528 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Tue, 29 Nov 2016 11:21:56 -0500 Subject: [PATCH 6/8] Add basic Interactor.Ecto --- lib/interactor/ecto.ex | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/interactor/ecto.ex diff --git a/lib/interactor/ecto.ex b/lib/interactor/ecto.ex new file mode 100644 index 0000000..9b26e2c --- /dev/null +++ b/lib/interactor/ecto.ex @@ -0,0 +1,24 @@ +defmodule Interactor.Ecto do + @behaviour Interactor + + @moduledoc """ + An interactor which will insert/update/transact your changesets and multis. + """ + + # TODO: Better name for source option? :from, :changeset, :multi ? + def call(interaction, opts) do + case {opts[:source], opts[:repo]} do + {nil, _} -> raise "Interactor.Ecto requires a :source option to indicate which assign field should be attempted to be inserted" + {_, nil} -> raise "Interactor.Ecto requires a :repo option to use to insert or transact with." + {source, repo} -> execute(interaction.assigns[source], repo) + end + end + + defp execute(nil, _), do: raise "Interactor.Ecto could not find given source" + defp execute(%{__struct__: Ecto.Multi} = multi, repo) do + repo.transaction(multi) + end + defp execute(%{__struct__: Ecto.Changeset} = changeset, repo) do + repo.insert_or_update(changeset) + end +end From dcef88212415b124b8585222796784fcb459b791 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Tue, 29 Nov 2016 14:40:04 -0500 Subject: [PATCH 7/8] Clean up unnessisary import. --- lib/interactor/builder.ex | 4 ++-- test/interactor/builder_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/interactor/builder.ex b/lib/interactor/builder.ex index 1dcded6..032ac1e 100644 --- a/lib/interactor/builder.ex +++ b/lib/interactor/builder.ex @@ -46,7 +46,7 @@ defmodule Interactor.Builder do quote do @behaviour Interactor import Interactor.Builder, only: [interactor: 1, interactor: 2] - import Interactor.Interaction # TODO, is this a good idea? assign/3 could conflict + alias Interactor.Interaction def call(interaction, opts) do interactor_builder_call(interaction, opts) @@ -79,7 +79,7 @@ defmodule Interactor.Builder do # `acc` is a series of nested interactor calls in the form of # interactor3(interactor2(interactor1(interaction))). # `quote_interactor` wraps a new interactor around that series of calls. - defp quote_interactor({interactor, opts, guards}, acc, env) do + defp quote_interactor({interactor, opts, guards}, acc, _env) do call = quote_interactor_call(interactor, opts) {fun, meta, [arg, [do: clauses]]} = diff --git a/test/interactor/builder_test.exs b/test/interactor/builder_test.exs index 38e1f9c..b23b046 100644 --- a/test/interactor/builder_test.exs +++ b/test/interactor/builder_test.exs @@ -8,7 +8,7 @@ defmodule Interactor.BuilderTest do interactor :three interactor :four, assign_to: :for - def two(i,_), do: assign(i, :two, "two") + def two(i,_), do: Interaction.assign(i, :two, "two") def three(_,_), do: "three" def four(_,_), do: "four" end From 91104c47cfc48b064978df76b813c27225816175 Mon Sep 17 00:00:00 2001 From: Alan Peabody Date: Tue, 29 Nov 2016 14:42:34 -0500 Subject: [PATCH 8/8] Rollback when failed interaction returned. --- lib/interactor.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/interactor.ex b/lib/interactor.ex index 7e06330..b7bc8c4 100644 --- a/lib/interactor.ex +++ b/lib/interactor.ex @@ -52,6 +52,8 @@ defmodule Interactor do assign_to = determine_assign_to(module, fun, opts[:assign_to]) rollback = determine_rollback(module, fun, opts[:rollback]) case strategy.execute(module, fun, interaction, opts) do + %Interaction{success: false} = interaction -> + Interaction.rollback(interaction) %Interaction{} = interaction -> Interaction.add_rollback(interaction, rollback) {:error, error} ->