lib/mix/tasks/avrora.reg.schema.ex

defmodule Mix.Tasks.Avrora.Reg.Schema do
  use Mix.Task

  @moduledoc """
  Register either one schema or all schemas in the `Avrora.Config.schemas_path`
  directory (or your private client schemas path).

      mix avrora.reg.schema [--all] [--name NAME] [--as NEW_NAME] [--module MODULE]

  The search of the schemas will be performed under path configured in `schemas_path`
  configuration option. One of either option must be given.

  ## Command line options

    * `--name` - the full name of the schema to register (exclusive with `--all`)
    * `--as` - the name which will be used to register schema (i.e subject)
    * `--all` - register all found schemas
    * `--module` - the private Avrora client module (i.e MyClient)
    * `--appconfig` - the app config file name to load from `config/` folder (i.e runtime)

  The `--module` option allows to use your private Avrora client module instead of
  the default `Avrora`.

  The `--as` option is possible to use only together with `--name`.

  The `--name` option expects that given schema name will comply to
  `Avrora.Storage.File` module rules.

  The `--appconfig` option expects just application config file name without an extension
  and will load it additionally to default. Note: runtime config doesn't support imports!

  For example, if the schema name is `io.confluent.Payment` it should be stored
  as `<schemas path>/io/confluent/Payment.avsc`

  ## Usage

      mix avrora.reg.schema --name io.confluent.Payment
      mix avrora.reg.schema --name io.confluent.Payment --as MyCustomName
      mix avrora.reg.schema --all --appconfig runtime
      mix avrora.reg.schema --all --module MyClient
      mix avrora.reg.schema --all
  """
  @shortdoc "Register schema(s) in the Confluent Schema Registry"

  alias Mix.Tasks

  @cli_options [
    strict: [
      as: :string,
      all: :boolean,
      name: :string,
      module: :string,
      appconfig: :string
    ]
  ]

  @impl Mix.Task
  def run(argv) do
    Tasks.Loadpaths.run(["--no-elixir-version-check", "--no-archives-check"])

    {opts, _, _} = OptionParser.parse(argv, @cli_options)
    {module_name, opts} = Keyword.pop(opts, :module, "Avrora")
    {app_config, opts} = Keyword.pop(opts, :appconfig)

    module = Module.concat(Elixir, String.trim(module_name))
    config = Module.concat(module, Config)
    registrar = Module.concat(module, Utils.Registrar)

    {:ok, _} = Application.ensure_all_started(:avrora)
    {:ok, _} = module.start_link()

    unless is_nil(app_config) do
      Mix.Project.config()
      |> Keyword.get(:config_path)
      |> Path.dirname()
      |> Path.join("#{app_config}.exs")
      |> List.wrap()
      |> Tasks.Loadconfig.run()
    end

    case opts |> Keyword.keys() |> Enum.sort() do
      [:all] ->
        [config.self().schemas_path(), "**", "*.avsc"]
        |> Path.join()
        |> Path.wildcard()
        |> Enum.each(fn file_path ->
          file_path
          |> Path.relative_to(config.self().schemas_path())
          |> Path.rootname()
          |> String.replace("/", ".")
          |> register_schema_by_name(registrar)
        end)

      [:name] ->
        opts[:name] |> String.trim() |> register_schema_by_name(registrar)

      [:as, :name] ->
        opts[:name]
        |> String.trim()
        |> register_schema_by_name(registrar, as: String.trim(opts[:as]))

      _ ->
        message = """
        don't know how to handle `#{Enum.join(argv, " ")}'
        please use #{IO.ANSI.yellow()}mix help avrora.reg.schema#{IO.ANSI.reset()} for help
        """

        Mix.shell().error(message)
        exit({:shutdown, 1})
    end
  end

  defp register_schema_by_name(name, registrar, opts \\ []) do
    opts = Keyword.merge(opts, force: true)

    case registrar.register_schema_by_name(name, opts) do
      {:ok, _} ->
        case Keyword.get(opts, :as) do
          nil -> Mix.shell().info("schema `#{name}' will be registered")
          new_name -> Mix.shell().info("schema `#{name}' will be registered as `#{new_name}'")
        end

      {:error, error} ->
        Mix.shell().error("schema `#{name}' will be skipped due to an error `#{error}'")
    end
  end
end