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