lib/gear_application/error_handler.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule Antikythera.GearApplication.ErrorHandler do
  @moduledoc """
  Helper module for gear's custom error handler.

  To customize HTTP responses returned on errors, gear must implement an error handler module which

  - is named `YourGear.Controller.Error`, and
  - defines the following functions:
      - Mandatory error handlers
          - `error(Antikythera.Conn.t, Antikythera.ErrorReason.gear_action_error_reason) :: Antikythera.Conn.t`
          - `no_route(Antikythera.Conn.t) :: Antikythera.Conn.t`
          - `bad_request(Antikythera.Conn.t) :: Antikythera.Conn.t`
      - Optional error handlers
          - `bad_executor_pool_id(Antikythera.Conn.t, Antikythera.ExecutorPool.BadIdReason.t) :: Antikythera.Conn.t`
          - `ws_too_many_connections(Antikythera.Conn.t) :: Antikythera.Conn.t` (when your gear uses websocket)

  This module generates `YourGear.error_handler_module/0` function, which is called by antikythera when handling errors.
  """

  alias Antikythera.GearName
  alias AntikytheraCore.GearModule

  @all_handlers [
    error: 2,
    no_route: 1,
    bad_request: 1
  ]

  @doc false
  defun find_error_handler_module(gear_name :: v[GearName.t()]) :: nil | module do
    # during compilation of gear, allowed to call unsafe function
    mod = GearModule.error_handler_unsafe(gear_name)

    try do
      exported_funs = mod.module_info(:exports)
      not_defined_handlers = Enum.reject(@all_handlers, &(&1 in exported_funs))

      if Enum.empty?(not_defined_handlers) do
        mod
      else
        IO.puts("""
        [antikythera] warning: #{mod} exists but some of the error handlers are not defined: #{inspect(not_defined_handlers)};
        [antikythera]   #{mod} is not eligible for a custom error handler module.
        """)

        nil
      end
    rescue
      # module not defined
      UndefinedFunctionError -> nil
    end
  end

  defmacro __using__(_) do
    quote do
      # Assuming that module attribute `@gear_name` is defined in the __CALLER__'s context
      @error_handler_module Antikythera.GearApplication.ErrorHandler.find_error_handler_module(
                              @gear_name
                            )
      defun error_handler_module() :: nil | module do
        @error_handler_module
      end
    end
  end
end