lib/mix/tasks/archeometer.treemap.ex

defmodule Mix.Tasks.Arch.Treemap do
  @moduledoc """
  Mix Task to generate a treemap for an application or a set of modules.

  Usage:

      mix arch.treemap [options]

  The following options are accepted:

    * `--metric` - Temporarily there is just one metric: size (default)
    * `--ns` - Namespace of the modules considered
    * `--app` - Application name
    * `--db` - Database filename
    * `--out` - Output filename
    * `--skip-tests` - Skips test related modules (default)
    * `--format` - Temporarily there is just one format: svg (default)
  """
  @shortdoc "Generates a Treemap diagram"

  require Logger
  use Mix.Task
  use Archeometer.Repo

  alias Archeometer.Analysis.Treemap
  alias Archeometer.Analysis.Treemap.SVGRender

  @impl Mix.Task
  def run(argv) do
    case get_args(argv) do
      [
        metric: metric,
        app: app,
        namespace: namespace,
        out: out_fname,
        db_name: db_name,
        skip_tests: skip_tests
      ] ->
        case treemap_analysis(String.to_atom(metric), app, namespace, db_name, skip_tests) do
          {:ok, svg} ->
            write(svg, out_fname)
            IO.puts(:stderr, "Diagram ready at: '#{out_fname}'")

          {:error, e} = error ->
            Mix.shell().error("Error: #{e}")
            Mix.shell().error("No Treemap is generated.")
            error
        end

      {:error, e} = error ->
        Mix.shell().error("Error: #{e}")
        print_help()
        error
    end
  end

  defp write(svg, "console") do
    IO.puts(svg)
  end

  defp write(svg, file_name) do
    file_name |> Path.dirname() |> File.mkdir_p()
    File.write(file_name, svg)
  end

  defp get_args(argv) do
    {opts, _args, invalid} =
      OptionParser.parse(
        argv,
        strict: [
          metric: :string,
          app: :string,
          ns: :string,
          out: :string,
          format: :string,
          db: :string,
          skip_tests: :boolean
        ]
      )

    case invalid do
      [] ->
        metric = Keyword.get(opts, :metric, "size")
        app = Keyword.get(opts, :app, :none)
        namespace = Keyword.get(opts, :ns, "*")
        out = Keyword.get(opts, :out, "console")
        db_name = Keyword.get(opts, :db, default_db_name())
        skip_tests = Keyword.get(opts, :skip_tests, true)
        format = Keyword.get(opts, :format, "svg")

        case validate_options(%{metric: metric, db: db_name, format: format}) do
          :ok ->
            [
              metric: metric,
              app: app,
              namespace: namespace,
              out: out,
              db_name: db_name,
              skip_tests: skip_tests
            ]

          {:error, error} ->
            {:error, error}
        end

      _ ->
        {:error, :wrong_arguments}
    end
  end

  defp validate_options(%{metric: metric, db: db_name, format: format}) do
    cond do
      # For now only the 'size' metric is supported but we pretend to support different metrics in the near future.
      metric not in ["size"] ->
        {:error, :metric_not_supported}

      not Archeometer.Repo.db_ready?(:full, db_name) ->
        {:error, "Database is not existent or doesn't have the required tables."}

      # For now only svg format is supported but we hope to add more formats in the future.
      format not in ["svg"] ->
        {:error, :unsupported_output_format}

      true ->
        :ok
    end
  end

  defp treemap_analysis(metric, app, namespace, db_name, skip_tests) do
    case Treemap.treemap(metric, app, namespace, db_name, skip_tests) do
      {:error, error} ->
        {:error, error}

      tree ->
        {:ok, SVGRender.render(tree)}
    end
  end

  defp print_help() do
    Mix.shell().info("""
    Usage: mix arch.treemap [opts] namespace

    opts: --metric metric --app app --ns namespace --db db_file_name --format format (svg) --out fname --skip-tests (default)
    """)
  end
end