2.0 KiB · text History 3db9f38
defmodule GitGudWeb.ErrorMessage do
  @moduledoc """
  Turns an `{:error, reason}` into text fit to show whoever made the
  request — an API reply, a flash — without dumping the term itself.

    * a changeset → its field errors ("name has already been taken");
    * a string → as is (it was written to be read);
    * an atom → in words (`:not_found` → "not found");
    * anything else (tuples, exceptions, structs) → "something went
      wrong (ref …)", with the term logged under the same ref, so the
      detail is one log search away for the operator and nowhere for
      the caller.
  """

  require Logger

  @spec describe(term()) :: String.t()
  def describe(%Ecto.Changeset{} = changeset) do
    changeset
    |> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
      Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
    |> flatten()
    |> case do
      [] -> "invalid input"
      errors -> Enum.join(errors, "; ")
    end
  end

  def describe(message) when is_binary(message), do: message

  def describe(reason) when is_atom(reason) and reason not in [nil, true, false],
    do: reason |> Atom.to_string() |> String.replace("_", " ")

  def describe(reason) do
    ref = 4 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower)
    Logger.error("error ref #{ref}: #{inspect(reason)}")
    "something went wrong (ref #{ref})"
  end

  defp flatten(errors, prefix \\ nil) do
    Enum.flat_map(errors, fn
      {field, messages} when is_list(messages) and messages != [] and is_binary(hd(messages)) ->
        Enum.map(messages, &"#{label(prefix, field)} #{&1}")

      {field, nested} when is_map(nested) ->
        flatten(nested, label(prefix, field))

      {field, nested} when is_list(nested) ->
        Enum.flat_map(nested, &flatten(&1, label(prefix, field)))

      _ ->
        []
    end)
  end

  defp label(nil, field), do: to_string(field)
  defp label(prefix, field), do: "#{prefix}.#{field}"
end