Skip to main content

lib/npm/version_util.ex

defmodule NPM.VersionUtil do
  @moduledoc """
  Utilities for npm version string manipulation.

  Helpers for parsing, comparing, and formatting npm version strings
  that go beyond what Elixir's `Version` module handles.
  """

  @doc """
  Parse a version string into `{major, minor, patch}` tuple.

  Returns `:error` for invalid versions.
  """
  @spec parse_triple(String.t()) ::
          {:ok, {non_neg_integer(), non_neg_integer(), non_neg_integer()}} | :error
  def parse_triple(version) do
    case Version.parse(version) do
      {:ok, %{major: maj, minor: min, patch: patch}} -> {:ok, {maj, min, patch}}
      :error -> :error
    end
  end

  @doc """
  Compare two version strings.

  Returns `:gt`, `:eq`, or `:lt`.
  """
  @spec compare(String.t(), String.t()) :: :gt | :eq | :lt
  def compare(a, b) do
    Version.compare(a, b)
  end

  @doc """
  Check if version `a` is greater than version `b`.
  """
  @spec gt?(String.t(), String.t()) :: boolean()
  def gt?(a, b), do: compare(a, b) == :gt

  @doc """
  Check if version `a` is less than version `b`.
  """
  @spec lt?(String.t(), String.t()) :: boolean()
  def lt?(a, b), do: compare(a, b) == :lt

  @doc """
  Get the major version number from a version string.
  """
  @spec major(String.t()) :: non_neg_integer() | :error
  def major(version) do
    case parse_triple(version) do
      {:ok, {maj, _, _}} -> maj
      :error -> :error
    end
  end

  @doc """
  Get the minor version number from a version string.
  """
  @spec minor(String.t()) :: non_neg_integer() | :error
  def minor(version) do
    case parse_triple(version) do
      {:ok, {_, min, _}} -> min
      :error -> :error
    end
  end

  @doc """
  Check if a version is a prerelease (has a pre tag).
  """
  @spec prerelease?(String.t()) :: boolean()
  def prerelease?(version) do
    case Version.parse(version) do
      {:ok, %{pre: pre}} -> pre != []
      :error -> false
    end
  end

  @doc """
  Increment the patch version.
  """
  @spec bump_patch(String.t()) :: String.t() | :error
  def bump_patch(version) do
    case parse_triple(version) do
      {:ok, {maj, min, patch}} -> "#{maj}.#{min}.#{patch + 1}"
      :error -> :error
    end
  end

  @doc """
  Increment the minor version (resets patch to 0).
  """
  @spec bump_minor(String.t()) :: String.t() | :error
  def bump_minor(version) do
    case parse_triple(version) do
      {:ok, {maj, min, _}} -> "#{maj}.#{min + 1}.0"
      :error -> :error
    end
  end

  @doc """
  Increment the major version (resets minor and patch to 0).
  """
  @spec bump_major(String.t()) :: String.t() | :error
  def bump_major(version) do
    case parse_triple(version) do
      {:ok, {maj, _, _}} -> "#{maj + 1}.0.0"
      :error -> :error
    end
  end

  @doc """
  Sort a list of version strings.
  """
  @spec sort([String.t()]) :: [String.t()]
  def sort(versions) do
    versions
    |> Enum.flat_map(fn v ->
      case Version.parse(v) do
        {:ok, ver} -> [{v, ver}]
        :error -> []
      end
    end)
    |> Enum.sort_by(&elem(&1, 1), Version)
    |> Enum.map(&elem(&1, 0))
  end

  @doc """
  Get the latest (highest) version from a list.
  """
  @spec latest([String.t()]) :: String.t() | nil
  def latest(versions) do
    sort(versions) |> List.last()
  end
end