3.5 KiB · text History b868907
defmodule GitGudWeb.Api.Mobile.BadgeController do
  @moduledoc """
  A repository's badges, for `gg badges`: every badge it has, what each
  says right now, and the Markdown and HTML that embed it.

      GET /api/mobile/v1/repos/:owner/:name/badges
        ?style=dot&corners=square&scale=2x&gg=true&theme=forest&format=png

  The query takes the badge sheet's options, each optional, and the
  embeds are built with them. The answer is `GitGudWeb.BadgeCatalog`'s,
  the same the badge sheet's README block uses, so a README a person
  built on the page and one an agent built with the CLI say the same
  thing. `defaults` lists the badges the README block starts with.

  `repo:read`: a badge says nothing the repository's pages don't.
  """
  use GitGudWeb, :controller

  import GitGudWeb.Api.Mobile.Helpers

  alias GitGud.Badges.Spec
  alias GitGudWeb.Api.Mobile.Auth
  alias GitGudWeb.BadgeCatalog

  def index(conn, %{"owner" => owner, "name" => name} = params) do
    with {:ok, user} <- Auth.require_scope(conn, "repo:read"),
         {:ok, repo} <- fetch_repo(conn, user, owner, name),
         {:ok, options} <- options(conn, params) do
      handle = BadgeCatalog.handle(repo)
      items = BadgeCatalog.items(repo)
      keys = Enum.map(items, & &1.key)

      json(conn, %{
        "repository" => "#{handle}/#{repo.name}",
        "options" => %{
          "style" => options.style,
          "corners" => options.corners,
          "scale" => options.scale,
          "gg" => options.gg,
          "theme" => options.theme,
          "format" => options.format
        },
        "defaults" => Enum.filter(BadgeCatalog.readme_defaults(), &(&1 in keys)),
        "badges" =>
          Enum.map(items, fn item ->
            spec = BadgeCatalog.spec(options, item.key, options.theme, options.format)
            badge(item, BadgeCatalog.embed(repo, handle, spec))
          end)
      })
    else
      {:error, conn} -> conn
    end
  end

  defp badge(item, embed) do
    {label, value, status} =
      case item.content do
        :mark -> {"GitGud", nil, nil}
        %{label: label, value: value, slot: slot} -> {label, value, slot}
      end

    %{
      "key" => item.key,
      "kind" => item.kind,
      "group" => item.group,
      "title" => item.title,
      "hint" => item.hint,
      "label" => label,
      "value" => value,
      "status" => status,
      "url" => embed.url,
      "dark_url" => embed.dark_url,
      "light_url" => embed.light_url,
      "link" => embed.link,
      "alt" => embed.alt,
      "markdown" => embed.markdown,
      "html" => embed.html
    }
  end

  # Each option is checked against what the badge URLs accept, and a bad
  # one says what would have been.
  defp options(conn, params) do
    checks = [
      {:style, params["style"], Spec.styles()},
      {:corners, params["corners"], Spec.corners()},
      {:scale, params["scale"], Spec.scales()},
      {:theme, params["theme"], Spec.themes()},
      {:format, params["format"], Spec.formats()}
    ]

    Enum.reduce_while(checks, {:ok, %Spec{}}, fn
      {_field, nil, _allowed}, acc ->
        {:cont, acc}

      {field, value, allowed}, {:ok, spec} ->
        if value in allowed,
          do: {:cont, {:ok, Map.put(spec, field, value)}},
          else:
            {:halt,
             {:error,
              send_error(conn, 422, "#{field} must be one of #{Enum.join(allowed, ", ")}")}}
    end)
    |> case do
      {:ok, spec} -> {:ok, %{spec | gg: params["gg"] in ["true", "1", "yes"]}}
      error -> error
    end
  end
end