lib/ash/error/exception.ex

defmodule Ash.Error.Exception do
  @moduledoc "Tooling for creating an Ash exception"

  defmacro __using__(_) do
    quote do
      import Ash.Error.Exception, only: [def_ash_error: 1, def_ash_error: 2]
    end
  end

  defmacro def_ash_error(fields, opts \\ []) do
    quote location: :keep, generated: true do
      defexception unquote(fields) ++
                     [
                       :changeset,
                       :query,
                       error_context: [],
                       vars: [],
                       path: [],
                       stacktrace: nil,
                       class: unquote(opts)[:class]
                     ]

      def from_json(json) do
        keyword =
          json
          |> Map.to_list()
          |> Enum.map(fn {key, value} -> {Ash.Error.atomize_safely(key), value} end)

        exception(keyword)
      end

      def new(opts), do: exception(opts)

      @impl Exception
      def message(%{vars: vars} = exception) do
        string = Ash.ErrorKind.message(exception)

        string =
          case Ash.Error.breadcrumb(Map.get(exception, :error_context)) do
            "" ->
              string

            context ->
              context <> "\n" <> string
          end

        Enum.reduce(List.wrap(vars), string, fn {key, value}, acc ->
          if String.contains?(acc, "%{#{key}}") do
            String.replace(acc, "%{#{key}}", to_string(value))
          else
            acc
          end
        end)
      end

      def exception(opts) do
        opts =
          if is_nil(opts[:stacktrace]) do
            {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)

            stacktrace =
              %{
                __struct__: Ash.Error.Stacktrace,
                stacktrace: stacktrace
              }

            Keyword.put(opts, :stacktrace, stacktrace)
          else
            opts
          end

        exception = super(opts)
        Map.update(exception, :vars, [], &clean_vars(exception, &1))
      end

      defp clean_vars(exception, vars) when is_map(vars) do
        clean_vars(exception, Map.to_list(vars))
      end

      defp clean_vars(exception, vars) do
        if is_nil(vars) || Keyword.keyword?(vars) do
          vars |> Kernel.||([]) |> Keyword.drop([:field, :message, :path])
        else
          IO.warn(
            "Got malformed `vars` when building exception. #{inspect(exception)} - #{inspect(vars)}"
          )

          []
        end
      end

      defoverridable exception: 1, message: 1, from_json: 1
    end
  end
end