defmodule Retort.Response.Error do @moduledoc """ A JSON-RPC [Response Error object](http://www.jsonrpc.org/specification#error_object) """ alias Alembic.{Document, Source} alias JaSerializer.Formatter.Utils # Struct defstruct code: nil, message: nil, data: nil # Types @type t :: %__MODULE__{ code: integer, message: String.t, data: term } # Functions @doc """ Uses rendered `changset` as data for error. """ @spec from_changeset(Ecto.Changeset.t) :: t def from_changeset(changeset = %Ecto.Changeset{}) do "Changeset error" |> predefined |> struct(data: Document.from_ecto_changeset(changeset, %{format_key: &Utils.format_key/1})) end @doc """ Uses JSON API `document` as data for error. Only use when the `document` is a JSON API errors document. """ @spec from_document(Document.t) :: t def from_document(document = %Document{errors: errors}) when is_list(errors) do "JSONAPI error" |> predefined |> struct(data: document) end @doc """ Converts JSON decoded map with `String.t` keys to `t`. # From error with data iex> Retort.Response.Error.from_json(%{"code" => 0, "data" => "Data", "message" => "Zero"}) %Retort.Response.Error{code: 0, data: "Data", message: "Zero"} # From error without data iex> Retort.Response.Error.from_json(%{"code" => 0, "message" => "Zero"}) %Retort.Response.Error{code: 0, data: nil, message: "Zero"} """ @spec from_json(%{required(binary) => integer, required(binary) => binary, optional(term) => term}) :: t def from_json(json = %{"code" => code, "message" => message}) when is_integer(code) and is_binary(message) do %__MODULE__{ code: code, # data is optional data: data_from_json(json["data"]), message: message } end @code_by_message %{ "Changeset error" => 422, "Internal error" => -32_603, "Invalid params" => -32_602, "Invalid Request" => -32_600, "JSONAPI error" => 422, "Method not found" => -32_601, "Parse error" => -32_700 } @doc """ Looks up [predefined](http://www.jsonrpc.org/specification#error_object) `t`. """ @spec predefined(String.t) :: t for {message, code} <- @code_by_message do def predefined(unquote(message)) do %__MODULE__{ code: unquote(code), message: unquote(message) } end end @doc """ Converts `t` to errors returned by `Calcinator` and `Calcinator.Resources` callbacks. """ @spec to_calcinator_error(t, maybe_changeset :: Ecto.Changeset.t | nil) :: {:error, :bad_gateway} | {:error, :not_found} | {:error, :sandbox_access_disallowed} | {:error, Ecto.Changeset.t} def to_calcinator_error( %__MODULE__{ code: 422, data: %Document{ errors: errors } }, changeset ) do case {errors, changeset} do {[%Alembic.Error{code: "500"}], _} -> {:error, :bad_gateway} {[%Alembic.Error{status: "403"}], _} -> {:error, :unauthorized} {[%Alembic.Error{status: "404"}], _} -> {:error, :not_found} { [ %Alembic.Error{ source: %Alembic.Source{ pointer: "/meta/beam" }, status: "422", title: "Sandbox Access Disallowed" } ], _ } -> {:error, :sandbox_access_disallowed} # length is at least 1 and there is a changeset to put errors into {[_ | _], %Ecto.Changeset{}} -> changeset_with_errors = Retort.Response.Error.Ecto.Changeset.add_alembic_errors(changeset, errors) {:error, changeset_with_errors} end end ## Private Functions defp data_from_json(json) do case Document.from_json( json, %Alembic.Error{ source: %Source{ pointer: "" } } ) do {:ok, document} -> document _ -> json end end end