Packages

Interactor provides an opinionated interface for performing complex user interactions.

Current section

Files

Jump to
interactor lib interactor.ex
Raw

lib/interactor.ex

defmodule Interactor do
use Behaviour
alias Interactor.TaskSupervisor
@moduledoc """
A tool for modeling events that happen in your application.
TODO: More on interactor concept
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, Interactor, :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.
"""
@spec call_async(module, map) :: {:ok, pid}
def call_async(interactor, map) do
Task.Supervisor.start_child(TaskSupervisor, Interactor, :call, [interactor, map])
end
defmacro __using__(opts) do
quote do
@behaviour Interactor
@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
end