Modifications to merge logic (#9)

This commit is contained in:
Zach Daniel 2024-05-21 23:03:40 -04:00 committed by GitHub
parent b58526cb5c
commit 906a82e711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 111 additions and 14 deletions

View file

@ -57,6 +57,8 @@ defmodule Splode do
"must supply the `unknown_error` option, pointing at a splode error to use in situations where we cannot convert an error."
)
@merge_with List.wrap(opts[:merge_with])
if Enum.empty?(opts[:error_classes]) do
raise ArgumentError,
"must supply at least one error class to `use Splode`, via `use Splode, error_classes: [class: ModuleForClass]`"
@ -180,19 +182,27 @@ defmodule Splode do
if Keyword.keyword?(values) && values != [] do
[to_error(values, Keyword.delete(opts, :bread_crumbs))]
else
Enum.map(values, &to_error(&1, Keyword.delete(opts, :bread_crumbs)))
values
|> flatten_preserving_keywords()
|> Enum.map(fn error ->
if Enum.any?([__MODULE__ | @merge_with], &splode_error?(error, &1)) do
error
else
to_error(error, Keyword.delete(opts, :bread_crumbs))
end
end)
end
if Enum.count_until(errors, 2) == 1 &&
Enum.at(errors, 0).class == :special do
(Enum.at(errors, 0).class == :special || Enum.at(errors, 0).__struct__.error_class?()) do
List.first(errors)
else
values
|> flatten_preserving_keywords()
errors
|> flatten_errors()
|> Enum.uniq_by(&clear_stacktraces/1)
|> Enum.map(fn value ->
if splode_error?(value, __MODULE__) do
Map.put(value, :splode, __MODULE__)
if Enum.any?([__MODULE__ | @merge_with], &splode_error?(value, &1)) do
Map.put(value, :splode, value.splode || __MODULE__)
else
exception_opts =
if opts[:stacktrace] do
@ -219,16 +229,17 @@ defmodule Splode do
end
defp choose_error(errors) do
errors = Enum.map(errors, &to_error/1)
[error | other_errors] =
Enum.sort_by(errors, fn error ->
# the second element here sorts errors that are already parent errors
{Map.get(@error_class_indices, error.class),
{Map.get(@error_class_indices, error.class) ||
Map.get(@error_class_indices, :unknown),
@error_classes[error.class] != error.__struct__}
end)
parent_error_module = @error_classes[error.class]
parent_error_module =
@error_classes[error.class] || Keyword.get(@error_classes, :unknown) ||
Splode.Error.Unknown
if parent_error_module == error.__struct__ do
%{error | errors: (error.errors || []) ++ other_errors}
@ -271,16 +282,15 @@ defmodule Splode do
def to_error(other, opts) do
cond do
splode_error?(other, __MODULE__) ->
Enum.any?([__MODULE__ | @merge_with], &splode_error?(other, &1)) ->
other
|> Map.put(:splode, __MODULE__)
|> Map.put(:splode, other.splode || __MODULE__)
|> add_stacktrace(opts[:stacktrace])
|> accumulate_bread_crumbs(opts[:bread_crumbs])
is_exception(other) ->
[error: Exception.format(:error, other), splode: __MODULE__]
|> @unknown_error.exception()
|> Map.put(:stacktrace, nil)
|> add_stacktrace(opts[:stacktrace])
|> accumulate_bread_crumbs(opts[:bread_crumbs])
@ -293,6 +303,22 @@ defmodule Splode do
end
end
defp flatten_errors(errors) do
errors
|> Enum.flat_map(&List.wrap/1)
|> Enum.flat_map(fn error ->
if Enum.any?([__MODULE__ | @merge_with], &splode_error?(error, &1)) do
if error.__struct__.error_class?() do
flatten_errors(error.errors)
else
[error]
end
else
[error]
end
end)
end
defp flatten_preserving_keywords(list) do
if Keyword.keyword?(list) do
[list]

View file

@ -13,6 +13,11 @@ defmodule SplodeTest do
use Splode.ErrorClass, class: :sw
end
defmodule ContainerErrorClass do
@moduledoc false
use Splode.ErrorClass, class: :ui
end
# Errors
defmodule CpuError do
@ -45,6 +50,18 @@ defmodule SplodeTest do
def message(err), do: err |> inspect()
end
defmodule ExampleContainerError do
@moduledoc false
use Splode.Error, fields: [:description], class: :ui
def message(err), do: err |> inspect()
end
defmodule ContainerUnknownError do
@moduledoc false
use Splode.Error, fields: [:error], class: :unknown
def message(err), do: err |> inspect()
end
defmodule SystemError do
@moduledoc false
use Splode,
@ -55,6 +72,28 @@ defmodule SplodeTest do
unknown_error: UnknownError
end
defmodule ContainerError do
@moduledoc false
use Splode,
error_classes: [
interaction: ContainerErrorClass,
hw: HwError,
sw: SwError
],
unknown_error: ContainerUnknownError,
merge_with: [SystemError]
end
defmodule ContainerWithoutMergeWith do
@moduledoc false
use Splode,
error_classes: [
interaction: ContainerErrorClass
],
unknown_error: ContainerUnknownError,
merge_with: []
end
test "splode_error?" do
refute SystemError.splode_error?(:error)
refute SystemError.splode_error?(%{})
@ -83,8 +122,15 @@ defmodule SplodeTest do
ram = RamError.exception() |> SystemError.to_error()
div = DivByZeroException.exception() |> SystemError.to_error()
null = NullReferenceException.exception() |> SystemError.to_error()
example_container_error = ExampleContainerError.exception() |> ContainerError.to_error()
%{cpu: cpu, ram: ram, div: div, null: null}
%{
cpu: cpu,
ram: ram,
div: div,
null: null,
example_container_error: example_container_error
}
end
test "wraps errors in error class with same class", %{
@ -123,6 +169,31 @@ defmodule SplodeTest do
assert error == error |> SystemError.to_class()
end
test "to_error flattens nested errors when included in merge_with", %{
cpu: cpu,
ram: ram,
example_container_error: example_container_error
} do
hw_error = [cpu, ram] |> SystemError.to_class()
interaction_error = ContainerError.to_class([hw_error, example_container_error])
assert %{errors: [^cpu, ^ram, ^example_container_error]} = interaction_error
end
test "to_error doesn't flatten nested errors when not included in merge_with", %{
cpu: cpu,
ram: ram,
example_container_error: example_container_error
} do
hw_error = [cpu, ram] |> SystemError.to_class()
interaction_error = ContainerWithoutMergeWith.to_class([hw_error, example_container_error])
assert %{errors: [%SplodeTest.ContainerUnknownError{}, %SplodeTest.ContainerUnknownError{}]} =
interaction_error
end
end
test "to_error" do