defmodule Mix.Tasks.Raxol.Convert.BaseManager do @moduledoc """ Converts GenServer modules to use BaseManager behavior. ## Usage mix raxol.convert.base_manager lib/path/to/module.ex mix raxol.convert.base_manager lib/dir --batch --dry-run ## Options * `--dry-run` - Preview changes without modifying files * `--batch` - Process all GenServer modules in directory * `--validate` - Check if conversion was successful """ use Mix.Task alias Raxol.Core.Runtime.Log @shortdoc "Convert GenServer modules to BaseManager" def run(args) do {opts, paths, _} = OptionParser.parse(args, switches: [dry_run: :boolean, batch: :boolean, validate: :boolean] ) paths |> expand_paths(opts) |> Enum.each(&convert_file(&1, opts)) end defp expand_paths(paths, opts) do case Keyword.get(opts, :batch, false) do true -> paths |> Enum.flat_map(&find_genserver_files/1) false -> paths end end defp find_genserver_files(path) do case File.dir?(path) do true -> Path.wildcard(Path.join(path, "**/*.ex")) |> Enum.filter(&uses_genserver?/1) false -> [path] end end defp uses_genserver?(file_path) do case File.read(file_path) do {:ok, content} -> String.contains?(content, "use GenServer") and not String.contains?(content, "use Raxol.Core.Behaviours.BaseManager") _ -> false end end defp convert_file(file_path, opts) do case File.read(file_path) do {:ok, content} -> converted = convert_content(content) case Keyword.get(opts, :dry_run) do true -> Log.info("Would convert: #{file_path}") Log.info("Changes:") show_diff(content, converted) _ -> File.write!(file_path, converted) Log.info("Converted: #{file_path}") case Keyword.get(opts, :validate) do true -> validate_conversion(file_path) _ -> :ok end end {:error, reason} -> Log.info("Error reading #{file_path}: #{reason}") end end defp convert_content(content) do content |> replace_use_genserver() |> add_init_manager_callback() |> update_start_link_conflicts() end defp replace_use_genserver(content) do String.replace( content, "use GenServer", "use Raxol.Core.Behaviours.BaseManager" ) end defp add_init_manager_callback(content) do case Regex.run(~r/@impl GenServer\s+def init\((.*?)\) do(.*?)end/s, content) do [full_match, args, body] -> new_callback = """ @impl Raxol.Core.Behaviours.BaseManager def init_manager(#{normalize_init_args(args)}) do#{body}end @impl GenServer def init(opts) do init_manager(opts) end """ String.replace(content, full_match, new_callback) _ -> # No @impl GenServer, try plain init case Regex.run(~r/def init\((.*?)\) do(.*?)end/s, content) do [full_match, args, body] -> new_callback = """ @impl Raxol.Core.Behaviours.BaseManager def init_manager(#{normalize_init_args(args)}) do#{body}end @impl GenServer def init(opts) do init_manager(opts) end """ String.replace(content, full_match, new_callback) _ -> content end end end defp normalize_init_args(args) do # Convert various init argument patterns to opts case String.trim(args) do # Tuple destructuring "{" <> _ -> "opts" # List destructuring "[" <> _ -> "opts" # Map destructuring "%{" <> _ -> "opts" _ -> args end end defp update_start_link_conflicts(content) do # BaseManager provides start_link/1, so rename any conflicting start_link/2 case Regex.match?(~r/def start_link\(.*?,.*?\)/, content) do true -> # Has start_link with multiple args - might conflict String.replace( content, ~r/def start_link\((.*?)\s*\\\\s*=/, "def start_link_legacy(\\1 \\\\=" ) false -> content end end defp show_diff(original, converted) do original_lines = String.split(original, "\n") converted_lines = String.split(converted, "\n") Enum.zip(original_lines, converted_lines) |> Enum.with_index(1) |> Enum.each(fn {{orig, conv}, line_num} -> case orig != conv do true -> Log.info("Line #{line_num}:") Log.info(" - #{orig}") Log.info(" + #{conv}") false -> :ok end end) end defp validate_conversion(file_path) do System.cmd("mix", ["compile", "--force", file_path], stderr_to_stdout: true) |> case do {_, 0} -> Log.info("✓ Compilation successful") :ok {output, _} -> Log.info("✗ Compilation failed:") Log.info(output) :error end end end