4.2 KiB · text History a625083
defmodule GitGud.Packages.About do
  @moduledoc """
  What a package *is*, for people: a title, a one-line summary, a
  longer description and whether it has an icon — one answer for the
  packages pages, the org catalog and the mobile API.

  Explicit wins. A summary or description set on the package (on the
  packages page, or with `X-Package-Summary` / `X-Package-Description`
  at upload) is what the publisher chose to say, so it goes first for
  every format. After that each format's artifact speaks for itself
  where it can — a wheel's `Summary`, a crate's `description`, a
  .deb's synopsis — read from the newest version's metadata, which the
  adapters already keep. Formats that carry nothing (an APK, a raw
  file, an image) say nothing rather than repeating the name.

  The title is the name a person would call it: an APK's `android:label`
  rather than `org.example.app`. Everything else is the package name.
  """

  alias GitGud.Packages
  alias GitGud.Packages.Package
  alias GitGud.Packages.PackageFile
  alias GitGud.Packages.PackageVersion

  @type t :: %{
          title: String.t(),
          summary: String.t() | nil,
          description: String.t() | nil,
          icon?: boolean()
        }

  @doc """
  About every package in `packages`, keyed by id. One query for the
  lot — a catalog asks about a whole namespace at once.
  """
  @spec for_packages([Package.t()]) :: %{integer() => t()}
  def for_packages(packages) do
    latest = packages |> Enum.map(& &1.id) |> Packages.latest_files()
    Map.new(packages, fn package -> {package.id, build(package, Map.get(latest, package.id))} end)
  end

  @doc "About one package."
  @spec for_package(Package.t()) :: t()
  def for_package(%Package{} = package),
    do: package |> List.wrap() |> for_packages() |> Map.fetch!(package.id)

  defp build(%Package{} = package, file) do
    file_meta = (file && file.metadata) || %{}
    version_meta = version_metadata(file)
    title = present(file_meta["label"]) || package.name

    description =
      present(package.description) || native_description(package.format, version_meta)

    summary =
      present(package.summary) ||
        native_summary(package.format, version_meta) ||
        first_line(description)

    %{
      title: title,
      summary: unless(summary && same_text?(summary, title), do: summary),
      description: description,
      icon?: is_binary(file_meta["icon_key"])
    }
  end

  defp version_metadata(%PackageFile{package_version: %PackageVersion{metadata: meta}})
       when is_map(meta),
       do: meta

  defp version_metadata(_), do: %{}

  # Each format's own word for itself, from what its adapter stored.
  defp native_summary("pypi", meta), do: present(meta["summary"])
  defp native_summary("rpm", %{"rpm" => rpm}), do: present(rpm["summary"])
  # A Debian description's first line *is* its synopsis, by policy.
  defp native_summary("deb", %{"control" => %{"Description" => d}}), do: first_line(d)
  defp native_summary(_format, _meta), do: nil

  defp native_description(format, meta) when format in ~w(cargo hex),
    do: present(meta["description"])

  defp native_description("rpm", %{"rpm" => rpm}), do: present(rpm["description"])

  # The extended part, below the synopsis; continuation lines are
  # indented one space and a lone "." is a paragraph break.
  defp native_description("deb", %{"control" => %{"Description" => d}}) when is_binary(d) do
    case String.split(d, "\n", parts: 2) do
      [_synopsis, rest] ->
        rest
        |> String.split("\n")
        |> Enum.map_join("\n", fn line ->
          case String.trim(line) do
            "." -> ""
            text -> text
          end
        end)
        |> present()

      _ ->
        nil
    end
  end

  defp native_description(_format, _meta), do: nil

  defp first_line(nil), do: nil
  defp first_line(text), do: text |> String.split("\n", parts: 2) |> hd() |> present()

  defp present(text) when is_binary(text) do
    case String.trim(text) do
      "" -> nil
      trimmed -> trimmed
    end
  end

  defp present(_), do: nil

  # A summary that only restates the title is worse than none.
  defp same_text?(a, b), do: normalize(a) == normalize(b)

  defp normalize(text),
    do: text |> String.trim() |> String.trim_trailing(".") |> String.downcase()
end