defmodule Mobus.Stepwise.Capabilities do
@moduledoc """
Configurable capability runner adapter for the stepwise engine.
When adapter is nil (default), capability execution returns
`{:ok, %{context: %{}}}` (no-op). This allows the package to work for
form-only wizards (e.g. Atrapos tenant creation) without requiring a
capability runner.
Configure via `:mobus_stepwise` application config:
config :mobus_stepwise, :capability_runner_adapter, MyApp.CapabilityRunner
### `:strict` mode
Some consumers (e.g. workflow_stem with its own adapter plumbing) prefer
strict semantics where a nil adapter signals a misconfiguration rather
than silently succeeding. Enable via:
config :mobus_stepwise, :capability_runner_strict, true
When strict is `true`, calling `execute/3` with no adapter configured
returns `{:error, :capability_runner_disabled}` instead of the default
no-op success. Default: `false` (backwards compatible).
"""
@doc """
Returns the configured capability runner adapter module, or `nil` if none is set.
Reads from application config key `:mobus_stepwise, :capability_runner_adapter`.
## Examples
Capabilities.adapter()
#=> MyApp.CapabilityRunner
# When unconfigured:
Capabilities.adapter()
#=> nil
"""
@spec adapter() :: module() | nil
def adapter do
Application.get_env(:mobus_stepwise, :capability_runner_adapter)
end
@doc """
Returns `true` if a capability runner adapter is configured and exports `execute/3`.
Checks that the adapter is a non-nil atom module with an `execute/3` function exported.
## Examples
Capabilities.enabled?()
#=> true
"""
@spec enabled?() :: boolean()
def enabled? do
is_atom(adapter()) and not is_nil(adapter()) and function_exported?(adapter(), :execute, 3)
end
@doc """
Executes a capability via the configured adapter, or returns a no-op result.
When no adapter is configured, returns `{:ok, %{context: %{}}}` (no-op).
Otherwise delegates to `adapter.execute(tenant_id, capability_handle, input)`.
## Parameters
* `tenant_id` — tenant identifier string
* `capability_handle` — capability name (string or atom), e.g. `"myapp.validate"`
* `input` — execution input map with event context, payload, and state
## Returns
* `{:ok, result}` — successful execution with context updates and/or artifacts
* `{:error, reason}` — capability execution failed
## Examples
Capabilities.execute("tenant-1", "myapp.validate", %{payload: %{email: "a@b.com"}})
#=> {:ok, %{context: %{valid: true}}}
"""
@spec execute(String.t(), String.t() | atom(), map()) :: {:ok, term()} | {:error, term()}
def execute(tenant_id, capability_handle, input) when is_binary(tenant_id) and is_map(input) do
case adapter() do
nil ->
if strict?() do
{:error, :capability_runner_disabled}
else
{:ok, %{context: %{}}}
end
mod when is_atom(mod) ->
mod.execute(tenant_id, capability_handle, input)
end
end
@doc """
Returns `true` when `:capability_runner_strict` mode is active.
When true, `execute/3` returns `{:error, :capability_runner_disabled}` if
no adapter is configured instead of silently succeeding. Consumers that
always configure an adapter (e.g. workflow_stem) should set this to `true`
so accidental nil-adapter calls are caught as errors.
## Examples
Capabilities.strict?()
#=> false
# After config:
# config :mobus_stepwise, :capability_runner_strict, true
Capabilities.strict?()
#=> true
"""
@spec strict?() :: boolean()
def strict? do
Application.get_env(:mobus_stepwise, :capability_runner_strict, false)
end
end