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 iex> "Failed!" |> Error.new() |> Error.format() ~s 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 Exception.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