lib/francis.ex

defmodule Francis do
  import Plug.Conn

  defmacro __using__(opts \\ []) do
    quote location: :keep do
      use Application

      def start(_type, _args) do
        IO.inspect("2")
        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_binary(res) ->
            conn
            |> send_resp(status, res)
            |> halt()

          res when is_map(res) ->
            conn
            |> put_resp_content_type("application/json")
            |> send_resp(status, Jason.encode!(res))
            |> halt()
        end
      end

      use Francis.Plug.Router

      plug(Plug.Logger, Keyword.get(unquote(opts), :logger_opts, []))
      plug(:match)
      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
    quote location: :keep do
      import WebSockAdapter

      module_name =
        unquote(
          path
          |> URI.parse()
          |> then(& &1.path)
          |> then(&String.split(&1, "/"))
          |> Enum.map(&String.capitalize/1)
          |> Enum.join(".")
        )

      defmodule :"#{__MODULE__}.#{module_name}" do
        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.error("WS Handler terminated: #{inspect(reason)} ")
          :ok
        end
      end

      get unquote(path) do
        var!(conn)
        |> WebSockAdapter.upgrade(WS.Handler, [], 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