Skip to content

Commit

Permalink
Move existing behaviour to Interactor.Legecy.
Browse files Browse the repository at this point in the history
Enables:
* Supporting async/task as strategies (opt `strategy: :async`)
* `Interactor.call/2` always returns an `%Interaction{}`
  • Loading branch information
alanpeabody committed Nov 27, 2016
1 parent 7ab01e1 commit 6b862c1
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 191 deletions.
168 changes: 56 additions & 112 deletions lib/interactor.ex
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
22 changes: 6 additions & 16 deletions lib/interactor/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Interactor.Builder do
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:
Expand Down Expand Up @@ -89,15 +89,6 @@ defmodule Interactor.Builder 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

Expand All @@ -114,19 +105,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
Expand Down
135 changes: 135 additions & 0 deletions lib/interactor/legacy.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 6b862c1

Please sign in to comment.