Current section

Files

Jump to
domo lib domo error_builder.ex
Raw

lib/domo/error_builder.ex

defmodule Domo.ErrorBuilder do
@moduledoc false
def build_precond_type_error(message) do
{:bypass, message}
end
def build_precond_field_error(opts) do
spec_string = opts[:spec_string]
{template, keywords} = build_field_error(spec_string)
template =
template <>
" And a true value from the precondition" <>
" function \"%{precond_description}\" defined for %{precond_type} type."
precond_opts = Keyword.take(opts, [:precond_description, :precond_type])
keywords = Keyword.merge(keywords, precond_opts)
{template, keywords}
end
def build_field_error(spec_string) do
{"Expected the value matching the %{type} type.", type: spec_string}
end
def precond_error?({:bypass, _message}), do: true
def precond_error?({_template, keywords}), do: Keyword.has_key?(keywords, :precond_description)
def precond_error?(_value), do: false
def pretty_error_by_key(_error, _filter_preconds? \\ false, _bypass_preconds? \\ false)
def pretty_error_by_key({:error, {_, _, field, _, _, _}} = error, filter_preconds?, bypass_preconds?) do
{field || :t, pretty_error(error, filter_preconds?, bypass_preconds?)}
end
def pretty_error(_error, _filter_preconds? \\ false, _bypass_preconds? \\ false)
def pretty_error({:error, {:type_mismatch, _, _, _, _, [{:bypass, _message} = error]}}, true = _filter_preconds?, true = _bypass_preconds?) do
error
end
def pretty_error({:error, {:type_mismatch, _, _, _, _, [{:bypass, message}]}}, filter_preconds?, _bypass_preconds?) do
if filter_preconds? do
[message]
else
message
end
end
def pretty_error(
{:error, {:type_mismatch, struct_module, field, value, _expected_types, [single_error_template]}},
filter_preconds?,
bypass_preconds?
) do
precond? = precond_error?(single_error_template)
message =
case {precond?, filter_preconds?, bypass_preconds?} do
{true, true, true} ->
single_error_template
{true, true, false} ->
general_error_message(single_error_template)
_ ->
invalid_value = invalid_value_message(value, field, struct_module)
general_error_string = general_error_message(single_error_template)
"#{invalid_value} #{general_error_string}"
end
if filter_preconds? do
List.wrap(message)
else
message
end
end
def pretty_error({:error, {:type_mismatch, struct_module, field, value, expected_types, error_templates}}, filter_preconds?, bypass_preconds?) do
underlying_errors = collect_deepest_underlying_errors(error_templates)
precond_errors =
if filter_preconds? do
underlying_errors
|> List.flatten()
|> Enum.filter(&precond_error?/1)
|> Enum.map(fn {template, keywords} = error -> if(bypass_preconds?, do: error, else: interpolate_error_template(template, keywords)) end)
else
[]
end
message =
if precond_errors == [] do
top_level_error = generate_top_level_error(expected_types)
invalid_value = invalid_value_message(value, field, struct_module)
general_error_string = general_error_message(top_level_error)
underlying_errors_string = underlying_errors_message(underlying_errors)
"#{invalid_value} #{general_error_string}#{underlying_errors_string}"
else
precond_errors
end
if filter_preconds? do
List.wrap(message)
else
message
end
end
defp generate_top_level_error(expected_types) do
expected_types
|> Enum.join(" | ")
|> build_field_error()
end
defp collect_deepest_underlying_errors([[_ | _] | _] = error_or_templates) do
error_by_count =
error_or_templates
|> Enum.reduce([], fn list, templates_and_length ->
updated_list = maybe_drop_top_level_error(list)
if match?([], updated_list) do
templates_and_length
else
[{updated_list, Enum.count(updated_list)} | templates_and_length]
end
end)
|> Enum.reverse()
max_length =
error_by_count
|> Enum.map(fn {_list, length} -> length end)
|> Enum.max(&>=/2, fn -> 0 end)
error_by_count
|> Enum.filter(fn {_list, length} -> length == max_length end)
|> Enum.map(fn {list, _length} -> list end)
end
defp collect_deepest_underlying_errors(error_templates) do
error_templates
end
defp maybe_drop_top_level_error(list) do
first_error = hd(list)
{_template, args} = first_error
if match?([type: _], args) do
tl(list)
else
list
end
end
defp invalid_value_message(value, nil = _field, _struct_module) do
"Invalid value #{short_inspect(value)}."
end
defp invalid_value_message(value, field, struct_module) do
"Invalid value #{short_inspect(value)} for field #{inspect(field)} of %#{inspect(struct_module)}{}."
end
defp short_inspect(value) do
inspect_value = inspect(value)
if byte_size(inspect_value) > 500 do
String.slice(inspect_value, 0..499) <> "..."
else
inspect_value
end
end
defp general_error_message({template, args} = _error) do
interpolate_error_template(template, args)
end
defp underlying_errors_message([[_ | _] | _] = error_or_groups) do
lines =
error_or_groups
|> Enum.map(&interpolate_first_indent/1)
|> List.flatten()
["\nUnderlying errors:"]
|> Enum.concat(lines)
|> Enum.join("\n")
end
defp underlying_errors_message([_ | _] = errors) do
lines = interpolate_indent_unerlying(errors)
["\nUnderlying errors:"]
|> Enum.concat(lines)
|> Enum.join("\n")
end
defp underlying_errors_message(_errors), do: nil
defp interpolate_first_indent(errors) do
errors
|> Enum.with_index()
|> Enum.map(fn {{template, args}, level} ->
String.duplicate(" ", Enum.min([level, 1])) <> " - " <> interpolate_error_template(template, args)
end)
end
defp interpolate_error_template(:bypass, message) do
message
end
defp interpolate_error_template(template, args) do
Enum.reduce(args, template, fn {key, value}, template ->
string = if is_nil(value), do: "nil", else: to_string(value)
String.replace(template, "%{#{key}}", string)
end)
end
defp interpolate_indent_unerlying(errors) do
Enum.map(errors, fn {template, args} ->
updated_args = maybe_indent_underlying(args)
" - " <> interpolate_error_template(template, updated_args)
end)
end
defp maybe_indent_underlying([error: error] = args) do
parts = String.split(error, "\nUnderlying errors:\n", parts: 2)
if Enum.count(parts) == 2 do
shifted_underlying =
parts
|> Enum.at(1)
|> String.split("\n")
|> Enum.map_join("\n", &(" " <> &1))
[error: Enum.join(["the following:", shifted_underlying], "\n")]
else
args
end
end
defp maybe_indent_underlying(args) do
args
end
end