Skip to main content

lib/mix/tasks/bb_jido.add_action.ex

# SPDX-FileCopyrightText: 2026 James Harton
#
# SPDX-License-Identifier: Apache-2.0

if Code.ensure_loaded?(Igniter) do
  defmodule Mix.Tasks.BbJido.AddAction do
    @shortdoc "Scaffolds a Jido action module"
    @moduledoc """
    #{@shortdoc}

    Creates a new module that `use`s `Jido.Action`, with a starter schema and
    a `run/2` callback returning `{:ok, %{}}`.

    ## Examples

    ```bash
    mix bb_jido.add_action MyApp.Actions.PickObject
    mix bb_jido.add_action MyApp.Actions.MovePose --safety-aware
    mix bb_jido.add_action MyApp.Actions.Teleop --name teleop_step
    ```

    ## Arguments

    The first positional argument is the module name for the new action
    (required).

    ## Options

    * `--name` - The Jido `name:` string for the action (defaults to a
      snake_cased version of the module's last segment).
    * `--description` - The Jido `description:` string.
    * `--safety-aware` - Mix in `BB.Jido.Action.SafetyAware` so the action
      refuses to run unless `BB.Safety.state(robot) == :armed`. Adds a
      `:robot` field to the schema.
    """

    use Igniter.Mix.Task

    alias Igniter.Project.Module

    @impl Igniter.Mix.Task
    def info(_argv, _parent) do
      %Igniter.Mix.Task.Info{
        positional: [:action_module],
        schema: [
          name: :string,
          description: :string,
          safety_aware: :boolean
        ],
        aliases: [n: :name, d: :description, s: :safety_aware]
      }
    end

    @impl Igniter.Mix.Task
    def igniter(igniter) do
      action_module = Module.parse(igniter.args.positional.action_module)
      action_name = action_name(igniter, action_module)
      description = Keyword.get(igniter.args.options, :description)
      safety_aware? = Keyword.get(igniter.args.options, :safety_aware, false)

      create_action_module(igniter, action_module, action_name, description, safety_aware?)
    end

    defp create_action_module(igniter, module, action_name, description, safety_aware?) do
      case Module.module_exists(igniter, module) do
        {true, igniter} ->
          igniter

        {false, igniter} ->
          Module.create_module(
            igniter,
            module,
            action_body(action_name, description, safety_aware?)
          )
      end
    end

    defp action_body(action_name, description, safety_aware?) do
      """
      use Jido.Action,
        name: #{inspect(action_name)},#{description_line(description)}
        schema: #{schema_literal(safety_aware?)}
      #{safety_use_line(safety_aware?)}
      @impl Jido.Action
      def run(#{run_params(safety_aware?)}, _context) do
        {:ok, %{}}
      end
      """
    end

    defp description_line(nil), do: ""
    defp description_line(text), do: "\n  description: #{inspect(text)},"

    defp schema_literal(true) do
      """
      [
          robot: [type: :atom, required: true, doc: "Robot module"]
        ]\
      """
    end

    defp schema_literal(false), do: "[]"

    defp safety_use_line(true), do: "\nuse BB.Jido.Action.SafetyAware\n"
    defp safety_use_line(false), do: ""

    defp run_params(true), do: "%{robot: _robot} = _params"
    defp run_params(false), do: "_params"

    defp action_name(igniter, action_module) do
      case Keyword.get(igniter.args.options, :name) do
        nil ->
          action_module
          |> Elixir.Module.split()
          |> List.last()
          |> Macro.underscore()

        name ->
          name
      end
    end
  end
else
  defmodule Mix.Tasks.BbJido.AddAction do
    @shortdoc "Scaffolds a Jido action module"
    @moduledoc false
    use Mix.Task

    def run(_argv) do
      Mix.shell().error("""
      The bb_jido.add_action task requires igniter.

          mix deps.get
          mix bb_jido.add_action MyApp.Actions.MyAction
      """)

      exit({:shutdown, 1})
    end
  end
end