Skip to main content

lib/systemd/unit_name.ex

defmodule Systemd.UnitName do
  @moduledoc """
  Pure helpers for formatting systemd unit names.

  Systemd unit names are ordinary strings, but callers often need to format the
  same patterns repeatedly: typed names such as `dbus.service`, template names
  such as `my_app@.service`, and instance names such as `my_app@4000.service`.

  This module keeps that formatting explicit without introducing a struct or a
  deployment-specific abstraction.

  ## Examples

      iex> Systemd.UnitName.new("dbus", :service)
      "dbus.service"

      iex> Systemd.UnitName.template("my_app", :service)
      "my_app@.service"

      iex> Systemd.UnitName.instance("my_app", 4000, :service)
      "my_app@4000.service"

      iex> Systemd.UnitName.ensure_type("my_app@4000", :service)
      "my_app@4000.service"

      iex> Systemd.UnitName.drop_type("my_app@4000.service")
      "my_app@4000"
  """

  @type unit_type :: :service | :socket | :timer | :target | :mount | :path | String.t()

  @doc """
  Formats a typed systemd unit name.

  If `name` already ends with the requested type suffix, it is returned
  unchanged.
  """
  @spec new(String.t(), unit_type()) :: String.t()
  def new(name, type) when is_binary(name), do: ensure_type(name, type)

  @doc """
  Formats a systemd template unit name.
  """
  @spec template(String.t(), unit_type()) :: String.t()
  def template(name, type) when is_binary(name) do
    name
    |> base_name()
    |> Kernel.<>("@")
    |> ensure_type(type)
  end

  @doc """
  Formats a systemd instance unit name.
  """
  @spec instance(String.t(), String.Chars.t(), unit_type()) :: String.t()
  def instance(name, instance, type) when is_binary(name) do
    base = base_name(name)
    instance = to_string(instance)

    base
    |> Kernel.<>("@" <> instance)
    |> ensure_type(type)
  end

  @doc """
  Ensures a unit name has the suffix for `type`.
  """
  @spec ensure_type(String.t(), unit_type()) :: String.t()
  def ensure_type(name, type) when is_binary(name) do
    suffix = suffix(type)

    if String.ends_with?(name, suffix) do
      name
    else
      name <> suffix
    end
  end

  @doc """
  Drops the final systemd unit type suffix from a name.
  """
  @spec drop_type(String.t()) :: String.t()
  def drop_type(name) when is_binary(name) do
    Enum.reduce_while(known_suffixes(), name, fn suffix, name ->
      if String.ends_with?(name, suffix) do
        {:halt, String.trim_trailing(name, suffix)}
      else
        {:cont, name}
      end
    end)
  end

  defp base_name(name) do
    name
    |> drop_type()
    |> String.trim_trailing("@")
  end

  defp known_suffixes do
    ~w(.service .socket .timer .target .mount .path)
  end

  defp suffix(type) when is_atom(type), do: "." <> Atom.to_string(type)

  defp suffix(type) when is_binary(type) do
    type = String.trim_leading(type, ".")
    "." <> type
  end
end