lib/circlex/emulator/api.ex

defmodule Circlex.Emulator.Api do
  import Plug.Conn

  alias Circlex.Emulator.State
  alias Circlex.Emulator.State.WalletState

  defmacro __using__([]) do
    quote do
      @on_definition {Circlex.Emulator.Api, :define_route}
      @before_compile {Circlex.Emulator.Api, :define_routes}

      Module.register_attribute(__MODULE__, :route, [])
      Module.register_attribute(__MODULE__, :__routes__, accumulate: true)

      use Plug.Router
      alias Circlex.Emulator.State
      import Circlex.Emulator.Api
      require Circlex.Emulator.Api

      plug(:match)
      plug(:dispatch)

      plug(Plug.Parsers,
        parsers: [:json],
        json_decoder: {Jason, :decode!, [[keys: :atomz]]}
      )
    end
  end

  def define_route(%{module: module}, _kind, name, _args, _guards, _body) do
    case Module.get_attribute(module, :route) do
      path when is_binary(path) ->
        Module.put_attribute(module, :__routes__, {path, :get, name, false})
        Module.put_attribute(module, :route, nil)

      opts when is_list(opts) ->
        path = Keyword.get(opts, :path)
        method = Keyword.get(opts, :method)
        no_data_key = Keyword.get(opts, :no_data_key, false)
        Module.put_attribute(module, :__routes__, {path, method, name, no_data_key})
        Module.put_attribute(module, :route, nil)

      _ ->
        nil
    end
  end

  defmacro define_routes(%{module: module}) do
    for {route, method, fun, no_data_key} <- Module.get_attribute(module, :__routes__) do
      quote bind_quoted: [
              module: module,
              fun: fun,
              no_data_key: no_data_key,
              route: route,
              method: method
            ] do
        match route, via: method do
          require Logger

          parser_opts = [parsers: [:json], json_decoder: Jason]

          conn = Plug.Parsers.call(var!(conn), Plug.Parsers.init(parser_opts))
          Process.put(:state_pid, conn.private[:state_pid])
          Process.put(:signer_proc, conn.private[:signer_proc])
          params = Circlex.Emulator.Api.api_params(conn)

          Logger.info("#{conn.method} #{conn.request_path}")
          {elapsed, res} = :timer.tc(unquote(module), unquote(fun), [params])
          conn = Circlex.Emulator.Api.api_handle(res, conn, unquote(no_data_key))

          elapsed_str =
            case floor(elapsed / 1000) do
              0 ->
                "#{elapsed}µs"

              els ->
                "#{els}ms"
            end

          case res do
            {:ok, _} ->
              Logger.info("[Success] #{conn.method} #{conn.request_path} (#{elapsed_str})")

            {:error, _} ->
              Logger.error("[Error] #{conn.method} #{conn.request_path} (#{elapsed_str})")

            {:error, _code, _} ->
              Logger.error("[Error] #{conn.method} #{conn.request_path} (#{elapsed_str})")
          end

          conn
        end
      end
    end
  end

  def json!(value, conn, status \\ 200) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(value))
  end

  defp deep_keys_to_atoms(map) do
    for {k, v} <- map, into: %{} do
      k_sym =
        cond do
          is_binary(k) ->
            String.to_atom(k)

          is_atom(k) ->
            k
        end

      {k_sym, if(is_map(v), do: deep_keys_to_atoms(v), else: v)}
    end
  end

  def api_params(conn) do
    deep_keys_to_atoms(conn.params)
  end

  def api_handle(res, conn, no_data_key) do
    case res do
      {:ok, val} ->
        if no_data_key do
          json!(val, conn)
        else
          json!(%{data: val}, conn)
        end

      {:error, error} ->
        json!(%{error: to_string(error)}, conn, 500)

      {:error, code, error} ->
        json!(%{code: code, message: to_string(error)}, conn, code)
    end
  end

  def check_idempotency_key(idempotency_key) do
    case State.check_idempotency_key(idempotency_key) do
      :ok ->
        :ok

      :reused_key ->
        {:error, 409, "Conflicts with another request."}
    end
  end

  def get_master_wallet() do
    with {:ok, master_wallet} <- WalletState.master_wallet() do
      {:ok, master_wallet}
    else
      :not_found ->
        {:error, "System Configuration Issue: no main \"merchant\" wallet specified"}
    end
  end

  def get_master_source() do
    with {:ok, master_wallet} <- get_master_wallet() do
      {:ok, %{type: "wallet", id: master_wallet.wallet_id}}
    end
  end
end