Skip to content

Commit

Permalink
Modifications to merge logic (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel authored May 22, 2024
1 parent b58526c commit 906a82e
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 15 deletions.
52 changes: 39 additions & 13 deletions lib/splode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]`"
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -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])

Expand All @@ -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]
Expand Down
75 changes: 73 additions & 2 deletions test/splode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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?(%{})
Expand Down Expand Up @@ -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()

%{cpu: cpu, ram: ram, div: div, null: null}
example_container_error = ExampleContainerError.exception() |> ContainerError.to_error()

%{
cpu: cpu,
ram: ram,
div: div,
null: null,
example_container_error: example_container_error
}
end

test "wraps errors in error class with same class", %{
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 906a82e

Please sign in to comment.