Skip to main content

lib/apple_intents/jido/default.ex

defmodule AppleIntents.Jido.Default do
  @moduledoc """
  Default Jido orchestrator — runs `Jido.Exec` when `jido_action` is present
  in the host application.
  """

  @behaviour AppleIntents.Orchestrator

  alias AppleIntents.Context
  alias AppleIntents.Jido.Context, as: JidoContext

  @impl AppleIntents.Orchestrator
  def run(task, params, %Context{} = context, opts) do
    with {:ok, action} <- resolve_task(task),
         :ok <- ensure_executable(action),
         jido_context <- JidoContext.to_jido(context, opts),
         {:ok, result} <- execute(action, params, jido_context, opts) do
      {:ok, normalize_result(result)}
    end
  end

  @doc "Resolve a task reference to a Jido action module."
  @spec resolve_task(module() | String.t()) :: {:ok, module()} | {:error, term()}
  def resolve_task(module) when is_atom(module), do: {:ok, module}

  def resolve_task(task_name) when is_binary(task_name) do
    tasks = Application.get_env(:apple_intents_jido, :tasks, %{})

    case Map.get(tasks, task_name) do
      nil -> {:error, {:unknown_task, task_name}}
      action -> {:ok, action}
    end
  end

  defp ensure_executable(action) do
    if jido_available?() do
      :ok
    else
      {:error, {:jido_not_available, action}}
    end
  end

  defp execute(action, params, jido_context, opts) do
    exec_opts = Keyword.take(opts, [:timeout, :jido, :max_retries, :backoff, :log_level])
    # Jido is an optional dependency; apply/3 avoids a compile-time reference.
    # credo:disable-for-next-line Credo.Check.Refactor.Apply
    apply(Jido.Exec, :run, [action, params, jido_context, exec_opts])
  end

  defp jido_available? do
    Code.ensure_loaded?(Jido.Exec) and function_exported?(Jido.Exec, :run, 4)
  end

  defp normalize_result(result) when is_map(result) do
    Map.new(result, fn
      {key, value} when is_atom(key) -> {Atom.to_string(key), value}
      pair -> pair
    end)
  end

  defp normalize_result(result), do: %{"value" => result}
end