defmodule Francis do
@moduledoc """
Wrapper around Plug and Bandit to create APIs
"""
import Plug.Conn
defmacro __using__(opts \\ []) do
quote location: :keep do
use Application
def start(_type, _args) do
children = [
{Bandit, [plug: __MODULE__] ++ Keyword.get(unquote(opts), :bandit_opts, [])}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
defoverridable(start: 2)
defp handle_resp(handler, conn, status \\ 200) do
case handler.(conn) do
res when is_struct(res, Plug.Conn) ->
res
res when is_binary(res) ->
conn
|> send_resp(status, res)
|> halt()
res when is_map(res) or is_list(res) ->
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(res))
|> halt()
end
end
use Francis.Plug.Router
plug(:match)
Enum.each(Keyword.get(unquote(opts), :plugs, []), fn
plug when is_atom(plug) -> plug(plug)
{plug, opts} when is_atom(plug) -> plug(plug, opts)
end)
plug(:dispatch)
end
end
defmacro get(path, handler) do
quote location: :keep do
Plug.Router.get(unquote(path), do: handle_resp(unquote(handler), var!(conn)))
end
end
defmacro post(path, handler) do
quote location: :keep do
Plug.Router.post(unquote(path), do: handle_resp(unquote(handler), var!(conn)))
end
end
defmacro put(path, handler) do
quote location: :keep do
Plug.Router.put(unquote(path), do: handle_resp(unquote(handler), var!(conn)))
end
end
defmacro delete(path, handler) do
quote location: :keep do
Plug.Router.delete(unquote(path), do: handle_resp(unquote(handler), var!(conn)))
end
end
defmacro patch(path, handler) do
quote location: :keep do
Plug.Router.patch(unquote(path), do: handle_resp(unquote(handler), var!(conn)))
end
end
defmacro ws(path, handler) do
module_name =
path
|> URI.parse()
|> then(& &1.path)
|> then(&String.split(&1, "/"))
|> Enum.map_join(".", &String.capitalize/1)
|> then(&"#{__MODULE__}.#{&1}")
|> String.to_atom()
handler_ast =
quote do
defmodule unquote(module_name) do
require WebSockAdapter
require Logger
def init(_opts), do: {:ok, %{}}
def handle_in(message, state) do
case unquote(handler).(elem(message, 0)) do
res when is_binary(res) ->
{:push, [{:text, res}], state}
res when is_map(res) ->
{:push, [{:text, Jason.encode!(res)}], state}
end
rescue
e ->
Logger.error("WS Handler error: #{inspect(e)} ")
{:stop, :error, e}
end
def terminate(reason, state) do
Logger.info("WS Handler terminated: #{inspect(reason)} ")
:ok
end
end
end
Code.compile_quoted(handler_ast)
quote location: :keep do
get(unquote(path), fn conn ->
var!(conn)
|> WebSockAdapter.upgrade(unquote(module_name), [], timeout: 60_000)
|> halt()
end)
end
end
defmacro unmatched(handler) do
quote location: :keep do
match _ do
handle_resp(unquote(handler), var!(conn), 404)
end
end
end
end