Current section
Files
Jump to
Current section
Files
lib/errors/wrapped_error.ex
defmodule Errors.WrappedError do
alias Errors.Stacktrace
@enforce_keys [:result, :reason]
defexception [:result, :reason, :context, :stacktrace, :metadata, :message]
# Offer `new/3` as a way to create `WrappedErrors` so that the `message` is set
# but also create `message/1` callback in case an exception is created manually
# See: https://hexdocs.pm/elixir/Exception.html#c:message/1
def new(result, context, stacktrace, metadata \\ %{})
when is_binary(context) or is_nil(context) do
reason =
case result do
:error ->
nil
{:error, reason} ->
reason
other ->
raise ArgumentError, "Errors wrap either :error or {:error, _}, got: #{inspect(other)}"
end
exception =
%__MODULE__{
result: result,
reason: reason,
context: context,
stacktrace: stacktrace,
metadata: Map.new(metadata)
}
%{exception | message: message(exception)}
end
def new_raised(exception, func, stacktrace)
when is_exception(exception) and is_function(func) do
exception =
%__MODULE__{
# If `result` isn't :ok/:error/{:ok, _}/{:error, _} then it was a raised exception
result: exception,
reason: exception,
context: func,
stacktrace: stacktrace,
metadata: %{}
}
%{exception | message: message(exception)}
end
def message(%__MODULE__{} = error) when is_binary(error.context) or is_nil(error.context) do
errors = unwrap(error)
context_string =
errors
|> Enum.map_join("\n", fn error ->
context_desc =
cond do
is_function(error.context) ->
function_info = Function.info(error.context)
"#{inspect(function_info[:module])}.#{function_info[:name]}/1"
is_binary(error.context) ->
error.context
is_nil(error.context) ->
nil
end
parts_string =
[format_line(error), context_desc, format_metadata(error)]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
" [CONTEXT] #{parts_string}"
end)
message = Errors.result_details(List.last(errors).result).message
"#{message}\n#{context_string}"
end
def message(%__MODULE__{} = error) when is_function(error.context) do
errors = unwrap(error)
context_string =
errors
|> Enum.map_join("\n", fn error ->
context_desc =
if is_function(error.context) do
function_info = Function.info(error.context)
"#{inspect(function_info[:module])}.#{function_info[:name]}/1"
else
error.context
end
parts_string =
[format_line(error), context_desc, format_metadata(error)]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
" [CONTEXT] #{parts_string}"
end)
message = Errors.result_details(List.last(errors).result).message
"#{message}\n#{context_string}"
end
def unwrap(%__MODULE__{reason: %__MODULE__{} = nested_error} = error) do
nested_errors = unwrap(nested_error)
[error | nested_errors]
end
def unwrap(%__MODULE__{} = error) do
[error]
end
defp format_line(error) do
entry =
error.stacktrace
|> Stacktrace.most_relevant_entry()
if entry do
if line = Stacktrace.format_file_line(entry) do
"#{line}:"
end
end
end
defp format_metadata(error) do
if map_size(error.metadata) > 0 do
inspect(error.metadata, custom_options: [sort_maps: true])
end
end
end
# defimpl Inspect, for: Errors.WrappedError do
# import Inspect.Algebra
#
# def inspect(%{reason: reason} = wrapped_error, opts) do
# %{mod: mod, message: message} = Errors.result_details(reason)
#
# {concat([mod, ": ", message]), opts}
# end
# end
if Code.ensure_loaded?(JSON.Encoder) do
defimpl JSON.Encoder, for: Errors.WrappedError do
def encode(error, encoder) do
errors = Errors.WrappedError.unwrap(error)
message = Errors.result_details(List.last(errors).result).message
encoder.(%{
message: message,
contexts: Enum.map(errors, &%{label: &1.context, metadata: &1.metadata})
})
end
end
end
# if Code.ensure_loaded?(Jason.Encoder) do
defimpl Jason.Encoder, for: Errors.WrappedError do
def encode(error, opts) do
errors = Errors.WrappedError.unwrap(error)
message = Errors.result_details(List.last(errors).result).message
Jason.Encode.map(
%{
message: message,
contexts: Enum.map(errors, &%{label: &1.context, metadata: &1.metadata})
},
opts
)
end
end
# end
defimpl Inspect, for: Errors.WrappedError do
import Inspect.Algebra
def inspect(wrapped_error, opts) do
errors = Errors.WrappedError.unwrap(wrapped_error)
contexts_doc =
errors
|> Enum.map(&(&1.context || inspect(&1.metadata)))
|> Enum.intersperse(" => ")
|> concat()
message = Errors.result_details(List.last(errors).result).message
# {doc, opts} = to_doc_with_opts(MapSet.to_list(map_set), opts)
{concat(["Errors.WrappedError<<", contexts_doc, " | ", message, ">>"]), opts}
end
end