lib/utix.ex

defmodule Utix do
  @moduledoc """
  Miscellaneous helper functions
  """

  @dialyzer {:nowarn_function, app_version: 1}

  @doc """
  Generate `*.config` system config files from the files found in the `config`
  directory.
  """
  @spec write_sys_configs!(String.t(), [String.t()]) :: [String.t()]
  def   write_sys_configs!(out_file_sfx \\ "sys.config", exclude_basenames \\ []) do
    excluded = ["config"] ++ exclude_basenames
    "config/*.exs"
    |> Path.wildcard()
    |> Enum.map(& Path.basename(&1, ".exs"))
    |> Enum.reject(& &1 =~ "runtime")
    |> Enum.filter(& &1 not in excluded)
    |> Enum.map(fn env ->
      env
      |> String.to_atom()
      |> write_sys_config!(Path.join("config", env <> ".exs"), out_file_sfx)
    end)
  end

  @doc """
  Generate system config file (defaults to `${env}.sys.config`) from the Elixir
  `config_filename` provided in the input.
  """
  @spec write_sys_config!(atom(), String.t(), String.t()) :: String.t()
  def   write_sys_config!(env, config_file, out_file_sfx \\ "sys.config")
    when is_atom(env) and is_binary(config_file)
  do
    config =
      config_file
      |> Config.Reader.read!()
      |> then(& :io_lib.format("~p.\n", [&1]))  ## Format it nicely
      |> List.to_string()
    file = "#{env}.#{Path.basename(out_file_sfx)}"
    out_file =
      out_file_sfx
      |> Path.dirname()
      |> Path.join(file)
    :ok = File.write!(out_file, config)
    out_file
  end

  @doc """
  If the list length is not greater than `max_len`, stringify it.  Otherwise
  return the item count in the list as a string.
  """
  @spec str_or_count(list, non_neg_integer) :: binary
  def   str_or_count(list, max_len \\ 10) when is_list(list) do
    case length(list) do
      n when n > max_len -> Integer.to_string(n)
      _                  -> inspect(list)
    end
  end

  @doc """
  Obtain the version of the current application.

  This function must be called from a Mix project file.

  ## Options

  - `verbose: true` - Enable printing of application version

  You can also set the environment variable `SHOW_APP_VERSION=true` to
  print application version instead of passing the keyword list to the function.

  ## Why is this needed?

  When a mix project version is hard-coded in the `mix.exs` file, it is easy
  to get it out of sync with the latest tag in the git history, or the latest
  git revision.  With this method, the app version is automatically determined
  either from the `hex.pm`'s metadata or from git history and is used to set
  the application version in the generated `*.app` file.

  If the project is loaded as a dependency from Hex.pm, then the project
  contains ".hex" file, which contains the version.  If it's loaded from git,
  then we can use "git describe" command to format the version with a revision.

  On the other hand if the dependency tries to use the
  `git describe --tags ...` command by itself, the output of the version will
  be reflecting the version from the parent application's git history, which is
  wrong.

  ## NOTE

  With this method of calculating the version number, you need to make
  sure that in the Github action the checkout includes:
  ```
    - name: Checkout the repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
  ```
  This ensures that the git history is checked out with tags so that
  `git describe --tags` returns the proper version number.
  """
  @spec app_version(Keyword.t) :: String.t
  def   app_version(opts \\ []) do
    hex_spec = Mix.Project.deps_path() |> Path.dirname() |> Path.join(".hex")
    version =
      if File.exists?(hex_spec) do
        hex_spec
        |> File.read!()
        |> :erlang.binary_to_term()
        |> elem(1)
        |> Map.get(:version)
      else
        case System.cmd("git", ~w(describe --always --tags), stderr_to_stdout: true) do
          {ver, 0} ->
            ver = ver |> String.trim() |> String.replace(~r/^v/, "")
            (ver =~ ~r/^[a-f\d]+$/) && ("0.0.0-0-g" <> ver) || ver
          _ ->
            "0.0.0"
        end
      end
    verbose =
      "SHOW_APP_VERSION"
      |> System.get_env("false")
      |> :lists.member(["1", "true"])
    Keyword.get(opts, :verbose, verbose) && IO.puts("AppVersion: " <> version)
    version
  end
end