lib/icon/schema/error.ex

defmodule Icon.Schema.Error do
  @moduledoc """
  This module defines an ICON 2.0 error.
  """
  use Icon.Schema.Type
  alias Icon.Schema
  alias Icon.Schema.Type

  @typedoc """
  Domain of the error.
  """
  @type domain :: :request | :contract | :internal

  @typedoc """
  Errors.
  """
  @type error ::
          :parse_error
          | :invalid_request
          | :method_not_found
          | :invalid_params
          | :internal_error
          | :server_error
          | :system_error
          | :pool_overflow
          | :pending
          | :executing
          | :not_found
          | :lack_of_resource
          | :timeout
          | :system_timeout
          | score_error()

  @typedoc """
  SCORE errors.
  """
  @type score_error ::
          :score_unknown_failure
          | :score_contract_not_found
          | :score_method_not_found
          | :score_method_not_payable
          | :score_illegal_format
          | :score_invalid_parameter
          | :score_invalid_instance
          | :score_invalid_container_access
          | :score_access_denied
          | :score_out_of_step
          | :score_out_of_balance
          | :score_timeout_error
          | :score_stack_overflow
          | :score_skip_transaction
          | :score_reverted

  @doc """
  An error.
  """
  defstruct [:code, :reason, :domain, :message, :data]

  @typedoc """
  An error.
  """
  @type t :: %__MODULE__{
          code: integer(),
          reason: error(),
          domain: domain(),
          message: binary(),
          data: any()
        }

  @spec load(any()) :: {:ok, t()} | :error
  @impl Icon.Schema.Type
  def load(value)

  def load(value) when is_map(value) do
    value =
      value
      |> Type.to_atom_map()
      |> Map.take([:code, :message, :data])
      |> new()

    {:ok, value}
  rescue
    _ ->
      :error
  end

  def load(_) do
    :error
  end

  @spec dump(any()) :: {:ok, map()} | :error
  @impl Icon.Schema.Type
  def dump(error)

  def dump(%__MODULE__{code: code, message: message, data: data}) do
    value =
      %{
        "code" => code,
        "message" => message,
        "data" => data
      }
      |> Stream.reject(fn {_, value} -> is_nil(value) end)
      |> Map.new()

    {:ok, value}
  end

  def dump(_) do
    :error
  end

  @doc """
  Creates a new error given a `schema_or_map_or_keyword`.
  """
  @spec new(Schema.state() | map() | keyword()) :: t()
  def new(schema_or_map_or_keyword)

  def new(%Schema{is_valid?: false} = schema) do
    %__MODULE__{
      code: -32_602,
      reason: reason(schema),
      domain: :internal,
      message: message(schema)
    }
  end

  def new(error) do
    reason = error[:reason]
    code = error[:code] || if reason, do: code(reason), else: -32_000

    %__MODULE__{
      code: code,
      reason: reason || reason(code),
      domain: domain(code),
      message: message(code, error[:message]),
      data: error[:data]
    }
  end

  #################
  # Private helpers

  @spec code(error()) :: integer()
  defp code(reason)
  defp code(:parse_error), do: -32_700
  defp code(:invalid_request), do: -32_600
  defp code(:method_not_found), do: -32_601
  defp code(:invalid_params), do: -32_602
  defp code(:internal_error), do: -32_603
  defp code(:server_error), do: -32_000
  defp code(:system_error), do: -31_000
  defp code(:pool_overflow), do: -31_001
  defp code(:pending), do: -31_002
  defp code(:executing), do: -31_003
  defp code(:not_found), do: -31_004
  defp code(:lack_of_resource), do: -31_005
  defp code(:timeout), do: -31_006
  defp code(:system_timeout), do: -31_007
  defp code(:score_unknown_failure), do: -30_001
  defp code(:score_contract_not_found), do: -30_002
  defp code(:score_method_not_found), do: -30_003
  defp code(:score_method_not_payable), do: -30_004
  defp code(:score_illegal_format), do: -30_005
  defp code(:score_invalid_parameter), do: -30_006
  defp code(:score_invalid_instance), do: -30_007
  defp code(:score_invalid_container_access), do: -30_008
  defp code(:score_access_denied), do: -30_009
  defp code(:score_out_of_step), do: -30_010
  defp code(:score_out_of_balance), do: -30_011
  defp code(:score_timeout_error), do: -30_012
  defp code(:score_stack_overflow), do: -30_013
  defp code(:score_skip_transaction), do: -30_014
  defp code(:score_reverted), do: -30_032

  @spec reason(integer() | Schema.state()) :: error()
  defp reason(code)
  defp reason(%Schema{}), do: :invalid_params
  defp reason(-32_700), do: :parse_error
  defp reason(-32_600), do: :invalid_request
  defp reason(-32_601), do: :method_not_found
  defp reason(-32_602), do: :invalid_params
  defp reason(-32_603), do: :internal_error
  defp reason(code) when -32_000 >= code and code >= -32_099, do: :server_error
  defp reason(-31_000), do: :system_error
  defp reason(-31_001), do: :pool_overflow
  defp reason(-31_002), do: :pending
  defp reason(-31_003), do: :executing
  defp reason(-31_004), do: :not_found
  defp reason(-31_005), do: :lack_of_resource
  defp reason(-31_006), do: :timeout
  defp reason(-31_007), do: :system_timeout
  defp reason(-30_001), do: :score_unknown_failure
  defp reason(-30_002), do: :score_contract_not_found
  defp reason(-30_003), do: :score_method_not_found
  defp reason(-30_004), do: :score_method_not_payable
  defp reason(-30_005), do: :score_illegal_format
  defp reason(-30_006), do: :score_invalid_parameter
  defp reason(-30_007), do: :score_invalid_instance
  defp reason(-30_008), do: :score_invalid_container_access
  defp reason(-30_009), do: :score_access_denied
  defp reason(-30_010), do: :score_out_of_step
  defp reason(-30_011), do: :score_out_of_balance
  defp reason(-30_012), do: :score_timeout_error
  defp reason(-30_013), do: :score_stack_overflow
  defp reason(-30_014), do: :score_skip_transaction

  defp reason(code) when -30_032 >= code and code >= -30_999,
    do: :score_reverted

  @spec domain(integer()) :: domain()
  defp domain(code)
  defp domain(code) when -30_000 >= code and code >= -30_999, do: :contract
  defp domain(_), do: :request

  @spec message(nil | binary() | Schema.state()) :: binary()
  @spec message(nil | integer(), nil | binary() | Schema.state()) :: binary()
  defp message(code \\ nil, message)
  defp message(_code, message) when is_binary(message), do: message
  defp message(-32_700, _), do: "Parse error"
  defp message(-32_600, _), do: "Invalid request"
  defp message(-32_601, _), do: "Method not found"
  defp message(-32_602, _), do: "Invalid params"
  defp message(-32_603, _), do: "Internal error"
  defp message(-31_000, _), do: "System error"
  defp message(-31_001, _), do: "Pool overflow"
  defp message(-31_002, _), do: "Pending"
  defp message(-31_003, _), do: "Executing"
  defp message(-31_004, _), do: "Not found"
  defp message(-31_005, _), do: "Lack of resource"
  defp message(-31_006, _), do: "Timeout"
  defp message(-31_007, _), do: "System timeout"
  defp message(-30_001, _), do: "Unknown failure"
  defp message(-30_002, _), do: "Contract not found"
  defp message(-30_003, _), do: "Method not found"
  defp message(-30_004, _), do: "Method not payable"
  defp message(-30_005, _), do: "Illegal format"
  defp message(-30_006, _), do: "Invalid parameter"
  defp message(-30_007, _), do: "Invalid instance"
  defp message(-30_008, _), do: "Invalid container access"
  defp message(-30_009, _), do: "Access denied"
  defp message(-30_010, _), do: "Out of step"
  defp message(-30_011, _), do: "Out of balance"
  defp message(-30_012, _), do: "Timeout error"
  defp message(-30_013, _), do: "Stack overflow"
  defp message(-30_014, _), do: "Skip transaction"

  defp message(code, _) when -32_000 >= code and code >= -32_099 do
    "Server error"
  end

  defp message(code, _) when -30_032 >= code and code >= -30_999 do
    "Reverted"
  end

  defp message(_code, %Schema{errors: errors}) do
    flatten_errors(errors)
  end

  @spec flatten_errors(nil | binary(), binary() | map()) :: binary()
  defp flatten_errors(root \\ nil, errors)

  defp flatten_errors(nil, errors) when is_map(errors) do
    errors
    |> Stream.map(fn {key, value} -> flatten_errors("#{key}", value) end)
    |> Enum.join(", ")
  end

  defp flatten_errors(root, errors) when is_map(errors) do
    errors
    |> Stream.map(fn {key, message} ->
      flatten_errors("#{root}.#{key}", message)
    end)
    |> Enum.join(", ")
  end

  defp flatten_errors(key, message) do
    "#{key} #{message}"
  end
end