defmodule Skuld.Effects.Command do @moduledoc """ Effect for dispatching commands through a unified pipeline. Commands are structs representing mutations. The handler is a single function `(command -> computation)` that can route commands however it likes (pattern matching, map lookup, etc.). The returned computation can use other effects. ## Example defmodule MyCommandHandler do use Skuld.Syntax # Route via pattern matching def handle(%CreateTodo{title: title}) do comp do {:ok, todo} <- EctoPersist.insert(changeset(title)) _ <- Writer.tell(:events, %TodoCreated{id: todo.id}) |> Comp.then_do(:ok) {:ok, todo} end end def handle(%DeleteTodo{id: id}) do comp do todo <- EctoPersist.get!(Todo, id) _ <- EctoPersist.delete(todo) _ <- Writer.tell(:events, %TodoDeleted{id: id}) |> Comp.then_do(:ok) :ok end end end # Use the Command effect comp do {:ok, todo} <- Command.execute(%CreateTodo{title: "Buy milk"}) todo end |> Command.with_handler(&MyCommandHandler.handle/1) |> EctoPersist.with_handler(Repo) |> Writer.with_handler([], tag: :events, output: &{&1, Enum.reverse(&2)}) |> Comp.run!() ## Handler Function The handler function has signature `(command -> computation)`. The returned computation can use any other effects installed in the pipeline. Routing is entirely up to the handler - use pattern matching, map lookup, or any other approach. """ @behaviour Skuld.Comp.IHandle @behaviour Skuld.Comp.IInstall use Skuld.Comp.DefOp alias Skuld.Comp alias Skuld.Comp.Env alias Skuld.Comp.Types @state_key "Elixir.Skuld.Effects.Command::handler_fn" ############################################################################# ## Operations (generated by def_op) ############################################################################# def_op(execute(command)) @execute_op @__execute_op__ ############################################################################# ## Types ############################################################################# @typedoc "Function that takes a command and returns a computation" @type command_handler :: (struct() -> Types.computation()) ############################################################################# ## Handler Installation ############################################################################# @doc """ Install a command handler for a computation. The handler is a function `(command -> computation)` that receives the command and returns a computation. The computation can use any other effects installed in the pipeline. ## Example my_comp |> Command.with_handler(&MyHandler.handle/1) |> Comp.run!() # Or with an anonymous function for dynamic routing: my_comp |> Command.with_handler(fn cmd -> handler = Map.fetch!(handlers, cmd.__struct__) handler.(cmd) end) |> Comp.run!() """ @spec with_handler(Types.computation(), command_handler()) :: Types.computation() def with_handler(comp, handler_fn) when is_function(handler_fn, 1) do comp |> Comp.scoped(fn env -> previous = Env.get_state(env, @state_key) modified = Env.put_state(env, @state_key, handler_fn) finally_k = fn v, e -> restored_env = case previous do nil -> %{e | state: Map.delete(e.state, @state_key)} val -> Env.put_state(e, @state_key, val) end {v, restored_env} end {modified, finally_k} end) |> Comp.with_handler(@__sig__, &__MODULE__.handle/3) end ############################################################################# ## IHandle Implementation ############################################################################# @impl Skuld.Comp.IHandle def handle({@execute_op, command}, env, k) do handler_fn = get_handler_fn!(env) # Call the handler function to get a computation command_computation = handler_fn.(command) # Run the computation with our continuation Comp.call(command_computation, env, k) end defp get_handler_fn!(env) do Env.get_state!(env, @state_key) end ############################################################################# ## IInstall Implementation ############################################################################# @doc """ Install Command handler via catch clause syntax. Config is the handler function: catch Command -> &MyHandler.handle/1 """ @impl Skuld.Comp.IInstall def __handle__(comp, handler_fn) when is_function(handler_fn, 1), do: with_handler(comp, handler_fn) end