Splode - Aggregatable and consistent errors for Elixir

Neat! I can think of a few places where this would clean up some of my own code.

A few ergonomics improvements that come to mind:

  1. To ease the transition from existing custom exceptions to Splode and to decrease the API footprint, the existing message callback could be wrapped instead of using a splode_message callback.

  2. Implement a default splode_message (or message if the first suggestion is taken) that, for instance, just inspects the :fields present in the error.

These last two I’m less sure of, but would pretty significantly reduce boilerplate:

  1. Allow module namespaces to be used as implicit error classes when unspecified. For instance:

    defmodule MyApp.Errors do
      use Splode,
        error_classes: [MyApp.Errors.Invalid, MyApp.Errors.Unknown],
        unknown_error: MyApp.Errors.Unknown.Unknown
    end
    
    defmodule MyApp.Errors.Invalid.InvalidThing do
       # Implicitly belongs to the MyApp.Errors.Invalid error class
      use Splode.Error, fields: [:foo, :bar]
    end
    
  2. Define error classes explicitly with use Splode.ErrorClass as opposed to implicitly by including a :errors field. This could inject the splode_message implementation, which it looks like will almost always (or just always?) be the same.

    defmodule MyApp.Errors.Invalid do
      use Splode.ErrorClass
    end
    

Taken all together, before:

defmodule MyApp.Errors do
  use Splode, error_classes: [
    invalid: MyApp.Errors.Invalid,
    unknown: MyApp.Errors.Unknown
  ],
  unknown_error: MyApp.Errors.Unknown.Unknown
end

defmodule MyApp.Errors.Invalid do
  use Splode.Error, fields: [:errors], class: :invalid

  def splode_message(%{errors: errors}) do
    Splode.ErrorClass.error_messages(errors)
  end
end

defmodule MyApp.Errors.Unknown do
  use Splode.Error, fields: [:errors], class: :unknown

  def splode_message(%{errors: errors}) do
    Splode.ErrorClass.error_messages(errors)
  end
end

defmodule MyApp.Errors.Unknown.Unknown do
  use Splode.Error, fields: [:error], class: :unknown

  def splode_message(%{error: error}) do
    if is_binary(error) do
      to_string(error)
    else
      inspect(error)
    end
  end
end

defmodule MyApp.Errors.InvalidArgument do
  use Splode.Error, fields: [:name, :message], class: :invalid

  def splode_message(%{name: name, message: message}) do
    "Invalid argument #{name}: #{message}"
  end
end

And after:

defmodule MyApp.Errors do
  use Splode,
    error_classes: [MyApp.Errors.Invalid, MyApp.Errors.Unknown],
    unknown_error: MyApp.Errors.Unknown.Unknown
end

defmodule MyApp.Errors.Invalid do
  use Splode.ErrorClass
end

defmodule MyApp.Errors.Unknown do
  use Splode.ErrorClass
end

defmodule MyApp.Errors.Unknown.Unknown do
  use Splode.Error, fields: [:error]
end

defmodule MyApp.Errors.InvalidArgument do
  use Splode.Error, fields: [:name, :message]

  def splode_message(%{name: name, message: message}) do
    "Invalid argument #{name}: #{message}"
  end
end