lib/kvasir/command/registry.ex

defmodule Kvasir.Command.RegistryGenerator do
  @moduledoc false

  @spec is_command?(module) :: boolean
  defp is_command?(module) do
    Keyword.has_key?(module.__info__(:attributes), :__command__)
  rescue
    _ -> false
  end

  @spec command_type(module) :: String.t()
  defp command_type(module), do: List.first(module.__info__(:attributes)[:__command__])

  @doc ~S"""
  Index all events and other registries.
  """
  @spec create :: :ok
  def create do
    commands = Enum.filter(ApplicationX.modules(), &is_command?/1)
    ensure_unique_commands!(commands)
    registry = Enum.into(commands, %{}, &{command_type(&1), &1})

    lookup =
      Enum.reduce(registry, nil, fn {k, v}, acc ->
        quote do
          unquote(acc)
          def lookup(unquote(k)), do: unquote(v)
        end
      end)

    Code.compiler_options(ignore_module_conflict: true)

    Module.create(
      Kvasir.Command.Registry,
      quote do
        @moduledoc ~S"""
        Kvasir Command Registry.
        """

        @doc ~S"""
        Lookup a command by a given `type`.

        ## Examples

        ```elixir
        iex> lookup("some-command")
        <cmd>
        iex> lookup("none-existing")
        nil
        ```
        """
        @spec lookup(String.t()) :: module | nil
        def lookup(type)
        unquote(lookup)
        def lookup(_), do: nil

        @doc ~S"""
        List all commands.

        ## Examples

        ```elixir
        iex> list()
        [<cmd>]
        ```
        """
        @spec list :: [module]
        def list, do: Map.values(commands())

        @doc ~S"""
        List all commands matching the filter.

        ## Examples

        ```elixir
        iex> list(namespace: "...")
        [<cmd>]
        ```
        """
        @spec list(filter :: Keyword.t()) :: [module]
        def list(filter) do
          commands = commands()

          commands =
            if ns = filter[:namespace] do
              ns = to_string(ns) <> "."
              Enum.filter(commands, &String.starts_with?(to_string(elem(&1, 1)), ns))
            else
              commands
            end

          commands =
            if ns = filter[:type_namespace] do
              ns = ns <> "."
              Enum.filter(commands, &String.starts_with?(elem(&1, 0), ns))
            else
              commands
            end

          Enum.map(commands, &elem(&1, 1))
        end

        @doc ~S"""
        All available commands indexed on type.

        ## Examples

        ```elixir
        iex> commands()
        [<cmd>]
        ```
        """
        @spec commands :: map
        def commands, do: unquote(Macro.escape(registry))
      end,
      Macro.Env.location(__ENV__)
    )

    Code.compiler_options(ignore_module_conflict: false)

    :ok
  end

  defp ensure_unique_commands!(commands) do
    duplicates =
      commands
      |> Enum.group_by(&command_type(&1))
      |> Enum.filter(&(Enum.count(elem(&1, 1)) > 1))

    if duplicates == [] do
      :ok
    else
      error =
        duplicates
        |> Enum.map(fn {k, v} ->
          "  \"#{k}\":\n#{v |> Enum.map(&"    * #{inspect(&1)}") |> Enum.join("\n")}"
        end)
        |> Enum.join("\n\n")

      raise "Duplicate Command Types\n\n" <> error
    end
  end
end

Kvasir.Command.RegistryGenerator.create()