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