Skip to main content

lib/npm/dist.ex

defmodule NPM.Dist do
  @moduledoc """
  Handles package distribution metadata.

  Parses dist info from registry responses including tarball URLs,
  shasum, integrity, and unpacked size.
  """

  @doc """
  Extracts dist metadata from a registry version entry.
  """
  @spec extract(map()) :: map()
  def extract(%{"dist" => dist}) when is_map(dist) do
    %{
      tarball: dist["tarball"],
      shasum: dist["shasum"],
      integrity: dist["integrity"],
      unpacked_size: dist["unpackedSize"],
      file_count: dist["fileCount"],
      npm_signature: dist["npm-signature"]
    }
  end

  def extract(_),
    do: %{
      tarball: nil,
      shasum: nil,
      integrity: nil,
      unpacked_size: nil,
      file_count: nil,
      npm_signature: nil
    }

  @doc """
  Checks if dist metadata has integrity hash.
  """
  @spec has_integrity?(map()) :: boolean()
  def has_integrity?(%{integrity: integrity}) when is_binary(integrity) and integrity != "",
    do: true

  def has_integrity?(_), do: false

  @doc """
  Extracts the tarball URL from dist metadata.
  """
  @spec tarball_url(map()) :: String.t() | nil
  def tarball_url(%{tarball: url}), do: url
  def tarball_url(_), do: nil

  @doc """
  Formats unpacked size for display.
  """
  @spec format_size(non_neg_integer() | nil) :: String.t()
  def format_size(nil), do: "unknown"
  def format_size(bytes) when bytes < 1024, do: "#{bytes} B"
  def format_size(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KB"
  def format_size(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB"

  @doc """
  Constructs a default tarball URL from registry and package info.
  """
  @spec default_tarball_url(String.t(), String.t(), String.t()) :: String.t()
  def default_tarball_url(registry, name, version) do
    encoded = URI.encode(name, &(&1 != ?/ and &1 != ?@))
    "#{String.trim_trailing(registry, "/")}/#{encoded}/-/#{Path.basename(name)}-#{version}.tgz"
  end

  @doc """
  Validates that dist metadata contains minimum required fields.
  """
  @spec valid?(map()) :: boolean()
  def valid?(%{tarball: tarball}) when is_binary(tarball) and tarball != "", do: true
  def valid?(_), do: false
end