diff --git a/lib/splode.ex b/lib/splode.ex index 26cab99..6d65f8c 100644 --- a/lib/splode.ex +++ b/lib/splode.ex @@ -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] diff --git a/test/splode_test.exs b/test/splode_test.exs index de309e4..1eea7ae 100644 --- a/test/splode_test.exs +++ b/test/splode_test.exs @@ -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