defmodule Francis do @moduledoc """ Module responsible for starting the Francis server and to wrap the Plug functionality This module performs multiple tasks: * Uses the Application module to start the Francis server * Defines the Francis.Router which uses Francis.Plug.Router, :match and :dispatch * Defines the macros get, post, put, delete, patch and ws to define routes for each operation You can also set the following options: * :bandit_opts - Options to be passed to Bandit * :plugs - List of plugs to be used by Francis """ import Plug.Conn defmacro __using__(opts \\ []) do quote location: :keep do use Application def start, do: start(nil, nil) 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 @doc """ Defines a GET route ## Examples ``` defmodule Example.Router do use Francis get "/hello", fn conn -> "Hello World!" 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 @doc """ Defines a POST route ## Examples ``` defmodule Example.Router do use Francis post "/hello", fn conn -> "Hello World!" 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 @doc """ Defines a PUT route ## Examples ``` defmodule Example.Router do use Francis put "/hello", fn conn -> "Hello World!" 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 @doc """ Defines a DELETE route ## Examples ``` defmodule Example.Router do use Francis delete "/hello", fn conn -> "Hello World!" 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 @doc """ Defines a PATCH route ## Examples ```elixir defmodule Example.Router do use Francis patch "/hello", fn conn -> "Hello World!" 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 @doc """ Defines a WebSocket route that sends text type responses ## Examples ```elixir defmodule Example.Router do use Francis ws "/hello", fn _ -> "Hello World!" 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 @doc """ Defines an action for umatched routes and returns 404 """ defmacro unmatched(handler) do quote location: :keep do match _ do handle_resp(unquote(handler), var!(conn), 404) end end end end