lib/mix/tasks/archeometer.collect.ecto.ex

defmodule Mix.Tasks.Arch.Collect.Ecto do
  @moduledoc """
  Mix Task to collect information of Ecto schema modules and
  their relationships.

  Usage:

      mix arch.collect.ecto [options]

  The following options are accepted:

    * `--include-deps` -  Wildcard glob style to filter dependencies

  For more information see:
  [Basic usage guide](guides/introduction/basic_usage.md#include-dependencies-in-the-analysis)
  """

  @shortdoc "Dumps ecto schemas and associations into Archeometer DB"

  use Mix.Task
  alias Archeometer.Collect.Ecto
  alias Archeometer.Util.DumpStats
  alias Archeometer.Collect.Project

  @impl Mix.Task
  def run(argv) do
    case get_args(argv) do
      [
        include_deps: deps_filter
      ] ->
        if Archeometer.Repo.db_ready?(:full) do
          Mix.shell().info("Starting ecto schemas analysis...\n")

          Mix.shell().info("Listing ecto schemas...")
          schema_modules = Ecto.ecto_schemas(append_deps(deps_filter))

          Mix.shell().info("Saving ecto schemas...")

          schema_modules
          |> Enum.map(&Macro.to_string/1)
          |> DumpStats.add_ecto_schemas()

          Ecto.clear_references()

          Mix.shell().info("Saving ecto associations...")

          schema_modules
          |> Ecto.get_associations()
          |> DumpStats.add_ecto_associations()

          Mix.shell().info("Done!")
        else
          Mix.shell().error("Please run 'mix arch.collect' first")
          print_help()
          {:error, :no_static_analysis_found}
        end

      {:validate_error, error_type} ->
        Mix.shell().error("Error: #{error_type}")
        {:error, error_type}

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

  defp get_args(argv) do
    {opts, _args, invalid} =
      OptionParser.parse(
        argv,
        strict: [
          include_deps: :keep
        ]
      )

    case invalid do
      [] ->
        deps_filter = Keyword.get_values(opts, :include_deps)

        validate_options(deps_filter)

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

  defp validate_options(deps_filter) do
    case Project.filter_deps(deps_filter) do
      [] ->
        {:validate_error, "Filter doesn't match any dependency."}

      _ ->
        [
          include_deps: deps_filter
        ]
    end
  end

  defp append_deps(deps_filter) do
    deps = Project.filter_deps(deps_filter)
    if deps, do: Keyword.keys(deps), else: []
  end

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

    opts: --include-deps 'deps_filter'

    Where `deps_filter` is a glob Unix-like pattern, matches dependencies name
    --include-deps can be used more than once.

    - `*` matches none or many tokens
    - `?` matches exactly one token
    - `[abc]` matches a set of tokens
    - `[a-z]` matches a range of tokens
    - `[!...]` matches anything but a set of tokens
    """)
  end
end