Skip to main content

lib/npm/package/repository.ex

defmodule NPM.Package.Repository do
  @moduledoc """
  Repository field parsing and URL generation from package.json.
  """

  @doc """
  Extracts repository info from package.json data.
  """
  @spec extract(map()) :: map() | nil
  def extract(%{"repository" => %{"type" => type, "url" => url} = repo}) do
    %{
      type: type,
      url: clean_url(url),
      directory: repo["directory"]
    }
  end

  def extract(%{"repository" => url}) when is_binary(url) do
    %{type: "git", url: resolve_shorthand(url), directory: nil}
  end

  def extract(_), do: nil

  @doc """
  Returns the browse URL (human-readable web URL).
  """
  @spec browse_url(map()) :: String.t() | nil
  def browse_url(data) do
    case extract(data) do
      nil -> nil
      %{url: url, directory: nil} -> url
      %{url: url, directory: dir} -> "#{url}/tree/HEAD/#{dir}"
    end
  end

  @doc """
  Returns the HTTPS clone URL.
  """
  @spec clone_url(map()) :: String.t() | nil
  def clone_url(data) do
    case extract(data) do
      nil -> nil
      %{url: url} -> ensure_git_suffix(url)
    end
  end

  @doc """
  Detects the hosting provider.
  """
  @spec provider(map()) :: atom() | nil
  def provider(data) do
    case extract(data) do
      nil ->
        nil

      %{url: url} ->
        cond do
          String.contains?(url, "github.com") -> :github
          String.contains?(url, "gitlab.com") -> :gitlab
          String.contains?(url, "bitbucket.org") -> :bitbucket
          true -> :other
        end
    end
  end

  @doc """
  Checks if repository info is present.
  """
  @spec has_repository?(map()) :: boolean()
  def has_repository?(data), do: extract(data) != nil

  defp clean_url(url) do
    url
    |> String.replace(~r/^git\+/, "")
    |> String.replace(~r/\.git$/, "")
    |> String.replace("git://", "https://")
    |> String.replace("ssh://git@", "https://")
  end

  defp resolve_shorthand(str) do
    cond do
      String.starts_with?(str, "github:") ->
        "https://github.com/#{String.trim_leading(str, "github:")}"

      String.contains?(str, "://") ->
        clean_url(str)

      String.contains?(str, "/") and not String.starts_with?(str, ".") ->
        "https://github.com/#{str}"

      true ->
        str
    end
  end

  defp ensure_git_suffix(url) do
    if String.ends_with?(url, ".git"), do: url, else: "#{url}.git"
  end
end