lib/mix/tasks/archeometer.report.ex

defmodule Mix.Tasks.Arch.Report do
  @moduledoc """
  Mix Task to generate a project analysis report. The format of the report
  can be static html pages or [Livebook](https://livebook.dev/) notebooks.

  Usage:

      mix arch.report [options]

  Options:

    * `--format` - Report format. Supported formats are `html` and `livemd`.
      Default is `html`.
    * `--limit` - Restricts the maximum number of results displayed from the
      queries in the report, Default is 10.
    * `--db` - Database filename

  For more information see:
  [Basic usage guide](guides/introduction/basic_usage.md)

   Report is generated at `reports/dev/static/html`.
  """
  @shortdoc "Generates an analysis report for the project"
  @supported_formats ~w(html livemd)

  require Logger
  require EEx
  use Mix.Task
  use Archeometer.Repo

  import Archeometer.Query
  import Archeometer.Reports.Utils

  @impl Mix.Task
  def run(argv) do
    case parse_args(argv) do
      {:ok, %{format: format, limit: limit, db_name: db_name}} ->
        cfg = %{
          modules: apps(db_name),
          root_page_def: &Archeometer.Reports.PageDefinition.Project.definition/1,
          module_page_def: &Archeometer.Reports.PageDefinition.Application.definition/1,
          renderer: Archeometer.Reports.Render.Html,
          db_name: db_name,
          limit: limit
        }

        case format do
          "html" ->
            Archeometer.Reports.Generator.generate(cfg)
            Mix.shell().info("HTML report ready at '#{report_path(:html)}'")

          "livemd" ->
            Archeometer.Reports.LiveBookGenerator.generate(cfg)
            Mix.shell().info("Livebook report ready at '#{report_path(:livemd)}'")
        end

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

  defp apps(db_name) do
    Archeometer.Repo.all(
      from(a in Archeometer.Schema.App,
        select: [name: a.name]
      ),
      [],
      db_name
    )
    |> Map.get(:rows)
    |> List.flatten()
  end

  defp parse_args(argv) do
    {opts, _data, invalid_switches} =
      OptionParser.parse(
        argv,
        strict: [
          format: :string,
          limit: :integer,
          db: :string
        ]
      )

    case invalid_switches do
      [] ->
        format = Keyword.get(opts, :format, "html")
        limit = Keyword.get(opts, :limit, 10)
        db_name = Keyword.get(opts, :db, default_db_name())
        validate_options(format, limit, db_name)

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

  defp validate_options(format, limit, db_name) do
    cond do
      format not in @supported_formats ->
        {:error, :invalid_format}

      limit <= 0 ->
        {:error, :invalid_limit}

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

      true ->
        {:ok, %{format: format, limit: limit, db_name: db_name}}
    end
  end

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

    opts: --format format (html) --limit 'integer' --db db_file_name
    """)
  end
end