defmodule Avalanche.Error do
@moduledoc """
Common application error.
"""
@type meta :: map() | keyword()
@typedoc "The exception type"
@type t :: %__MODULE__{
:reason => atom(),
:message => String.t(),
:meta => meta(),
:original_error => any(),
stacktrace: nil | Exception.stacktrace()
}
defexception reason: :application_error,
message: "",
meta: [],
original_error: nil,
stacktrace: nil
@doc """
Creates a new Error from a message string or another error
Examples:
# A string will be used as message
iex> alias Avalanche.Error
iex> Error.new("These are the error message")
%Error{message: "These are the error message"}
# Error structs are returned unchanged
iex> Error.new(%Error{reason: :some_reason})
%Error{reason: :some_reason}
# Atoms will be used as reason
iex> Error.new(:some_reason)
%Error{reason: :some_reason}
# Anything else will be used as the `original_error`
iex> Error.new(%RuntimeError{message: "oops!"})
%Error{message: "oops!", original_error: %RuntimeError{message: "oops!"}}
"""
@spec new(any) :: t
def new(%__MODULE__{} = error) do
error
end
def new(message) when is_binary(message) do
%__MODULE__{message: message}
end
def new(reason) when is_atom(reason) do
%__MODULE__{reason: reason}
end
def new(error) when is_exception(error) do
%__MODULE__{original_error: error, message: Exception.message(error)}
end
def new(error) do
%__MODULE__{original_error: error}
end
@doc """
Builds an Error struct.
Examples:
iex> alias Avalanche.Error
iex> Error.new(:bad, "Bad Things", %{data: "things"})
%Error{__exception__: true, message: "Bad Things", meta: %{data: "things"}, reason: :bad}
"""
@spec new(atom(), String.t(), meta()) :: t()
def new(reason, message, meta \\ %{}) when is_binary(message) do
%__MODULE__{reason: reason, message: message, meta: Map.new(meta)}
end
@doc """
Builds an Error struct with a reason of `:application_error`.
Examples:
iex> alias Avalanche.Error
iex> Error.application_error("Bad Things", %{data: "things"})
%Error{__exception__: true, message: "Bad Things", meta: %{data: "things"}, reason: :application_error}
"""
@spec application_error(binary(), meta()) :: t()
def application_error(message, meta \\ %{}) do
new(:application_error, message, meta)
end
@doc """
Builds an Error struct with reason and message deived from the given http status.
Examples:
iex> alias Avalanche.Error
iex> Error.http_status(404, %{data: "things"})
%Error{__exception__: true, message: "Not Found", meta: %{data: "things"}, reason: :not_found}
"""
@spec http_status(integer(), meta()) :: t()
def http_status(status, meta \\ %{}) when is_integer(status) do
reason = Plug.Conn.Status.reason_atom(status)
message = Plug.Conn.Status.reason_phrase(status)
new(reason, message, meta)
end
@doc """
Formats a Error for printing/logging.
This returns a verbose, multi-line string.
Examples:
iex> alias Avalanche.Error
iex> RuntimeError.exception("Failed!") |> Error.new() |> Error.format()
~s<application_error: Failed!
meta: []
original_error: ** (RuntimeError) Failed!>
iex> "Failed!" |> Error.new() |> Error.format()
~s<application_error: Failed!
meta: []
original_error: nil>
iex> 123 |> Error.new() |> Error.format() =~ ~r/original_error: 123/
true
iex> :bad |> Error.new() |> Error.format() =~ ~r/bad/
true
"""
@spec format(t()) :: binary
def format(%__MODULE__{} = error) do
[
"#{error.reason}: #{error.message}",
"meta: #{inspect(error.meta)}",
"original_error: #{format_error(error.original_error)}",
"#{error.stacktrace && Exception.format_stacktrace(error.stacktrace)}"
]
|> Enum.reject(&(String.length(&1) == 0))
|> Enum.join("\n")
end
@doc """
Returns the Error message.
Examples:
iex> alias Avalanche.Error
iex> error = Error.application_error("Bad Things", %{data: "things"})
iex> Error.message(error)
"application_error: Bad Things"
"""
@impl Exception
@spec message(t()) :: binary
def message(%__MODULE__{} = error) do
"#{error.reason}: #{error.message}"
end
@spec format_error(term) :: binary
defp format_error(error) do
if Kernel.is_exception(error) do
"** (" <> inspect(error.__struct__) <> ") " <> Exception.message(error)
else
inspect(error)
end
end
defimpl String.Chars do
@spec to_string(Avalanche.Error.t()) :: binary
def to_string(error) do
Avalanche.Error.message(error)
end
end
end