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