defmodule DataSchema.CastFunctionError do @moduledoc """ An error for when a casting function does not return the correct data. All casting functions get wrapped in a `rescue` to catch any unexpected exceptions. This lets us add more useful information about where the error occurred that the stack trace cannot provide - mainly which field failed the casting. We then raise using the original stacktrace and printing the captured error message. ### Matching on Specific Exceptions If a user wishes to capture specific exceptions as part of casting they may do so by matching on the `:wrapped_error` field. For example: try do DataSchema.to_struct(my_input, MySchema) rescue %DataSchema.CastFunctionError{wrapped_error: %RuntimeError{}} -> Logger.error("Expected Runtime Error") error -> reraise error, __STACKTRACE__ end """ defexception [ :message, :casted_value, :leaf_field, :path, :wrapped_error, :stacktrace_of_wrapped_error ] @doc """ Creates an appropriate error message from the given struct. """ def error_message(%__MODULE__{ casted_value: value, path: path, leaf_field: leaf_field, wrapped_error: wrapped_error }) do """ Unexpected error when casting value #{inspect(value)} #{field_message(path, leaf_field)} Full path to field was: #{inspect_path(path)} The casting function raised the following error: #{Exception.format(:error, wrapped_error)} """ end defp field_message(path, leaf_field) do case List.last(path) do {schema, field} when is_atom(field) -> """ for field #{inspect(field)} in schema #{inspect(schema)} """ field when is_atom(field) -> """ for field #{inspect(field)} in this part of the schema: #{formatted_field(leaf_field)} """ end end defp inspect_path(path) do [first | rest] = path |> Enum.reverse() rest |> Enum.reduce(" #{node_to_string(first)}", fn field, acc -> acc <> "Under " <> node_to_string(field) end) end defp node_to_string({module, field}) do "Field #{inspect(field)} in #{inspect(module)}\n" end defp node_to_string(field) do "Field #{inspect(field)}\n" end # What happens with aggregate? defp formatted_field({:field, rest}) do "field: #{inspect(rest)}," end defp formatted_field({:has_one, rest}) do "has_one: #{inspect(rest)}," end defp formatted_field({:has_many, rest}) do "has_many: #{inspect(rest)}," end defp formatted_field({:list_of, rest}) do "list_of: #{inspect(rest)}," end defp formatted_field({:aggregate, {field, path, cast, opts}}) do """ @aggregate_fields [ #{Enum.map_join(path, "\n ", &formatted_field/1)} ] aggregate: {#{inspect(field)}, @aggregate_fields, #{inspect(cast)}, #{inspect(opts)}}, """ end defp formatted_field({:aggregate, {field, path, cast}}) do """ @aggregate_fields [ #{Enum.map_join(path, "\n ", &formatted_field/1)} ] aggregate: {#{inspect(field)}, @aggregate_fields, #{inspect(cast)}}, """ end end