Skip to main content

lib/image/plug/error.ex

defmodule Image.Plug.Error do
  @moduledoc """
  Tagged error type returned by every public function in `Image.Plug`.

  An error carries a stable `:tag` (an atom suitable for pattern matching
  and for mapping to HTTP status codes), a human-readable `:message`,
  and optional `:details` for diagnostic context.

  Errors are returned as `{:error, %Image.Plug.Error{}}`. They are not
  raised — `try/rescue` is reserved for true system boundaries per the
  project's code style.
  """

  @typedoc """
  A stable error tag. The set is open; callers should match the tags
  documented for the function they call and treat unknown tags as `:internal`.
  """
  @type tag ::
          :unknown_option
          | :invalid_option
          | :malformed_url
          | :variant_not_found
          | :variant_already_exists
          | :source_not_found
          | :source_fetch_error
          | :source_too_large
          | :unsupported_source_format
          | :unsupported_output_format
          | :unsupported_option
          | :output_too_large
          | :request_timeout
          | :pipeline_failed
          | :signature_required
          | :invalid_signature
          | :signature_expired
          | :not_implemented
          | :internal

  @type t :: %__MODULE__{
          tag: tag(),
          message: String.t(),
          details: map()
        }

  @enforce_keys [:tag, :message]
  defstruct tag: :internal, message: "", details: %{}

  @doc """
  Builds an error struct.

  ### Arguments

  * `tag` is an atom from `t:tag/0` describing the error class.

  * `message` is a human-readable description of the failure.

  ### Options

  * `:details` is a map of extra diagnostic context. Defaults to `%{}`.

  ### Returns

  * An `Image.Plug.Error` struct.

  ### Examples

      iex> error = Image.Plug.Error.new(:unknown_option, "no such option", details: %{key: "wat"})
      iex> error.tag
      :unknown_option
      iex> error.details
      %{key: "wat"}

  """
  @spec new(tag(), String.t(), keyword()) :: t()
  def new(tag, message, options \\ []) when is_atom(tag) and is_binary(message) do
    %__MODULE__{
      tag: tag,
      message: message,
      details: Keyword.get(options, :details, %{})
    }
  end

  @doc """
  Maps an error tag to a default HTTP status code.

  ### Arguments

  * `error_or_tag` is either an `Image.Plug.Error` struct or an
    error tag atom.

  ### Returns

  * An integer HTTP status code.

  ### Examples

      iex> Image.Plug.Error.status(:variant_not_found)
      404

      iex> Image.Plug.Error.status(Image.Plug.Error.new(:invalid_option, "bad"))
      400

      iex> Image.Plug.Error.status(:internal)
      500

  """
  @spec status(t() | tag()) :: 100..599
  def status(%__MODULE__{tag: tag}), do: status(tag)

  def status(:unknown_option), do: 400
  def status(:invalid_option), do: 400
  def status(:malformed_url), do: 400
  def status(:variant_not_found), do: 404
  def status(:variant_already_exists), do: 409
  def status(:source_not_found), do: 404
  def status(:source_fetch_error), do: 502
  def status(:source_too_large), do: 413
  def status(:unsupported_source_format), do: 415
  def status(:unsupported_output_format), do: 415
  def status(:unsupported_option), do: 400
  def status(:output_too_large), do: 413
  def status(:request_timeout), do: 504
  def status(:pipeline_failed), do: 500
  def status(:signature_required), do: 401
  def status(:invalid_signature), do: 401
  def status(:signature_expired), do: 401
  def status(:not_implemented), do: 501
  def status(:internal), do: 500
  def status(_other), do: 500
end