lib/bylaw/db/issue.ex

defmodule Bylaw.Db.Issue do
  @moduledoc """
  Issue returned by a failed database validation check.
  """

  alias Bylaw.Db.Target

  @typedoc """
  A database validation issue.

  `check` identifies the check module that produced the issue, `message` is the
  human-readable failure text, `target` is the target that failed when available,
  and `meta` holds structured adapter- or check-specific details.
  """
  @type t :: %__MODULE__{
          check: module(),
          message: String.t(),
          target: Target.t() | nil,
          meta: map()
        }

  @typedoc """
  Formatting option.
  """
  @type format_opt :: {:meta, boolean()}

  @typedoc """
  Formatting options.
  """
  @type format_opts :: list(format_opt())

  defstruct check: nil,
            message: "",
            target: nil,
            meta: %{}

  @doc """
  Formats a database issue for human-readable error output.

  Metadata is omitted by default because issue messages are meant for humans and
  often already contain the actionable details. Pass `meta: true` to include the
  structured metadata for debugging.

  ## Examples

      iex> issue = %Bylaw.Db.Issue{
      ...>   check: MyApp.RequiredColumns,
      ...>   message: "users.email is nullable",
      ...>   meta: %{table: :users}
      ...> }
      iex> Bylaw.Db.Issue.format(issue)
      "MyApp.RequiredColumns: users.email is nullable"

      iex> issue = %Bylaw.Db.Issue{
      ...>   check: MyApp.RequiredColumns,
      ...>   message: "users.email is nullable",
      ...>   meta: %{table: :users}
      ...> }
      iex> Bylaw.Db.Issue.format(issue, meta: true)
      "MyApp.RequiredColumns: users.email is nullable %{table: :users}"
  """
  @spec format(t()) :: String.t()
  def format(%__MODULE__{} = issue), do: format(issue, [])

  @doc """
  Formats a database issue for human-readable error output.
  """
  @spec format(t(), format_opts()) :: String.t()
  def format(%__MODULE__{} = issue, opts) when is_list(opts) do
    base = "#{inspect(issue.check)}: #{issue.message}"

    if Keyword.get(opts, :meta, false) and issue.meta != %{} do
      base <> " " <> inspect(issue.meta)
    else
      base
    end
  end

  @doc """
  Formats many database issues for human-readable error output.

  ## Examples

      iex> issues = [
      ...>   %Bylaw.Db.Issue{check: MyApp.RequiredColumns, message: "users.email is nullable"},
      ...>   %Bylaw.Db.Issue{check: MyApp.RequiredColumns, message: "posts.account_id is nullable"}
      ...> ]
      iex> Bylaw.Db.Issue.format_many(issues)
      "MyApp.RequiredColumns: users.email is nullable\\nMyApp.RequiredColumns: posts.account_id is nullable"
  """
  @spec format_many(list(t())) :: String.t()
  def format_many(issues) when is_list(issues), do: format_many(issues, [])

  @doc """
  Formats many database issues for human-readable error output.
  """
  @spec format_many(list(t()), format_opts()) :: String.t()
  def format_many(issues, opts) when is_list(issues) and is_list(opts) do
    Enum.map_join(issues, "\n", &format(&1, opts))
  end
end