Current section

Files

Jump to
squid_mesh lib squid_mesh.ex
Raw

lib/squid_mesh.ex

defmodule SquidMesh do
@moduledoc """
Public entrypoint for the Squid Mesh runtime.
The API exposed here stays focused on declarative workflow operations. Host
applications start, inspect, and later control runs through this surface
without needing to work directly with persistence internals.
"""
alias SquidMesh.Config
alias SquidMesh.Run
alias SquidMesh.RunExplanation
alias SquidMesh.RunStore
alias SquidMesh.Runtime.Dispatcher
alias SquidMesh.Runtime.Reviewer
alias SquidMesh.Runtime.Unblocker
@doc """
Loads Squid Mesh configuration from the application environment with optional
runtime overrides.
"""
@spec config(keyword()) :: {:ok, Config.t()} | {:error, {:missing_config, [atom()]}}
defdelegate config(overrides \\ []), to: Config, as: :load
@doc """
Loads Squid Mesh configuration and raises if required keys are missing.
"""
@spec config!(keyword()) :: Config.t()
defdelegate config!(overrides \\ []), to: Config, as: :load!
@doc """
Starts a new workflow run with the given payload through the workflow's
default trigger.
"""
@spec start_run(module(), map()) ::
{:ok, Run.t()}
| {:error, {:missing_config, [atom()]}}
| {:error, RunStore.create_error()}
| {:error, {:dispatch_failed, term()}}
def start_run(workflow, payload) when is_map(payload) do
start_run(workflow, payload, [])
end
@spec start_run(module(), map(), keyword()) ::
{:ok, Run.t()}
| {:error, {:missing_config, [atom()]}}
| {:error, RunStore.create_error()}
| {:error, {:dispatch_failed, term()}}
def start_run(workflow, payload, overrides) when is_map(payload) and is_list(overrides) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <-
RunStore.create_and_dispatch_run(config.repo, workflow, payload, fn run ->
Dispatcher.dispatch_run(config, run)
end) do
SquidMesh.Observability.emit_run_created(run)
{:ok, run}
else
{:error, reason} when is_tuple(reason) and elem(reason, 0) == :invalid_run ->
{:error, reason}
{:error, reason} = error when reason in [:not_found] ->
error
{:error, %_{} = reason} ->
{:error, {:dispatch_failed, reason}}
{:error, reason} when is_tuple(reason) ->
{:error, reason}
{:error, reason} ->
{:error, {:dispatch_failed, reason}}
end
end
def start_run(_workflow, _payload, overrides) when is_list(overrides) do
{:error, {:invalid_payload, :expected_map}}
end
@doc """
Starts a new workflow run through a named trigger with the given payload.
"""
@spec start_run(module(), atom(), map()) ::
{:ok, Run.t()}
| {:error, {:missing_config, [atom()]}}
| {:error, RunStore.create_error()}
| {:error, {:dispatch_failed, term()}}
def start_run(workflow, trigger_name, payload)
when is_atom(trigger_name) and is_map(payload) do
start_run(workflow, trigger_name, payload, [])
end
@spec start_run(module(), atom(), map(), keyword()) ::
{:ok, Run.t()}
| {:error, {:missing_config, [atom()]}}
| {:error, RunStore.create_error()}
| {:error, {:dispatch_failed, term()}}
def start_run(workflow, trigger_name, payload, overrides)
when is_atom(trigger_name) and is_map(payload) and is_list(overrides) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <-
RunStore.create_and_dispatch_run(
config.repo,
workflow,
trigger_name,
payload,
fn run -> Dispatcher.dispatch_run(config, run) end
) do
SquidMesh.Observability.emit_run_created(run)
{:ok, run}
else
{:error, reason} when is_tuple(reason) and elem(reason, 0) == :invalid_run ->
{:error, reason}
{:error, reason} = error when reason in [:not_found] ->
error
{:error, %_{} = reason} ->
{:error, {:dispatch_failed, reason}}
{:error, reason} when is_tuple(reason) ->
{:error, reason}
{:error, reason} ->
{:error, {:dispatch_failed, reason}}
end
end
@doc """
Fetches one workflow run by id.
"""
@spec inspect_run(Ecto.UUID.t(), keyword()) ::
{:ok, Run.t()} | {:error, :not_found | :invalid_run_id | {:missing_config, [atom()]}}
def inspect_run(run_id, overrides \\ []) do
{inspect_opts, config_overrides} = Keyword.split(overrides, [:include_history])
with {:ok, config} <- Config.load(config_overrides) do
RunStore.get_run(config.repo, run_id, inspect_opts)
end
end
@doc """
Explains the current runtime state of one workflow run.
The result is structured diagnostic data for host apps, CLIs, and dashboards.
Use `inspect_run/2` for the factual run snapshot and `explain_run/2` when an
operator-facing surface needs the reason, evidence, and valid next actions for
the run's current state.
"""
@spec explain_run(Ecto.UUID.t(), keyword()) ::
{:ok, RunExplanation.t()}
| {:error, :not_found | :invalid_run_id | Config.config_error()}
def explain_run(run_id, overrides \\ []) do
with {:ok, config} <- Config.load(overrides) do
RunExplanation.explain(config, run_id)
end
end
@doc """
Lists workflow runs with optional filters.
"""
@spec list_runs(RunStore.list_filters(), keyword()) ::
{:ok, [Run.t()]} | {:error, {:missing_config, [atom()]}}
def list_runs(filters \\ [], overrides \\ []) do
with {:ok, config} <- Config.load(overrides) do
RunStore.list_runs(config.repo, filters)
end
end
@doc """
Requests cancellation for an eligible workflow run.
"""
@spec cancel_run(Ecto.UUID.t(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()}
def cancel_run(run_id, overrides \\ []) do
with {:ok, config} <- Config.load(overrides) do
RunStore.cancel_run(config.repo, run_id)
end
end
@doc """
Resumes a run that is intentionally paused for manual intervention.
"""
@spec unblock_run(Ecto.UUID.t()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def unblock_run(run_id), do: unblock_run(run_id, %{}, [])
@spec unblock_run(Ecto.UUID.t(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def unblock_run(run_id, overrides) when is_list(overrides) do
unblock_run(run_id, %{}, overrides)
end
@spec unblock_run(Ecto.UUID.t(), map()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def unblock_run(run_id, attrs) when is_map(attrs) do
unblock_run(run_id, attrs, [])
end
@spec unblock_run(Ecto.UUID.t(), map(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def unblock_run(run_id, attrs, overrides) when is_map(attrs) and is_list(overrides) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <- RunStore.get_run(config.repo, run_id),
:ok <- Unblocker.unblock(config, run, attrs) do
RunStore.get_run(config.repo, run_id)
end
end
@doc """
Approves a paused approval step and resumes the run through its success path.
"""
@spec approve_run(Ecto.UUID.t(), map(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def approve_run(run_id, attrs, overrides \\ []) when is_map(attrs) and is_list(overrides) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <- RunStore.get_run(config.repo, run_id),
:ok <- Reviewer.review(config, run, :approved, attrs) do
RunStore.get_run(config.repo, run_id)
end
end
@doc """
Rejects a paused approval step and resumes the run through its rejection path.
"""
@spec reject_run(Ecto.UUID.t(), map(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found
| :invalid_run_id
| {:missing_config, [atom()]}
| RunStore.transition_error()
| term()}
def reject_run(run_id, attrs, overrides \\ []) when is_map(attrs) and is_list(overrides) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <- RunStore.get_run(config.repo, run_id),
:ok <- Reviewer.review(config, run, :rejected, attrs) do
RunStore.get_run(config.repo, run_id)
end
end
@doc """
Creates a new run from a prior run and links it to the original run.
"""
@spec replay_run(Ecto.UUID.t(), keyword()) ::
{:ok, Run.t()}
| {:error,
:not_found | :invalid_run_id | {:missing_config, [atom()]} | RunStore.replay_error()}
| {:error, {:dispatch_failed, term()}}
def replay_run(run_id, overrides \\ []) do
with {:ok, config} <- Config.load(overrides),
{:ok, run} <-
RunStore.replay_and_dispatch_run(config.repo, run_id, fn run ->
Dispatcher.dispatch_run(config, run)
end) do
SquidMesh.Observability.emit_run_replayed(run)
{:ok, run}
else
{:error, %_{} = reason} ->
{:error, {:dispatch_failed, reason}}
{:error, reason} ->
{:error, reason}
end
end
end