defmodule Buckaroo.Router do
@moduledoc ~S"""
An extension to `Plug.Router` now also supporting `websocket`.
"""
@doc false
defmacro __using__(opts) do
quote location: :keep do
use Plug.Router, unquote(opts)
import Buckaroo.Router, only: [sse: 2, websocket: 2]
Module.register_attribute(__MODULE__, :plug_forwards, accumulate: true)
@on_definition {Buckaroo.Router, :on_def}
@before_compile Buckaroo.Router
@has_sse_route false
end
end
@doc false
defmacro __before_compile__(_env) do
quote do
@doc false
@spec __sse__ :: boolean
if @has_sse_route do
def __sse__, do: true
else
def __sse__ do
Enum.any?(@plug_forwards, fn plug ->
{:__sse__, 0} in plug.__info__(:functions) and plug.__sse__()
end)
end
end
import Buckaroo.Router, only: []
end
end
@doc ~S"""
Dispatches to the websocket.
See `Plug.Router.match/3` for more examples.
## Example
```
websocket "/ws", connect: ExampleSocket
```
"""
defmacro websocket(expr, opts) do
method = :websocket
{path, guards} = extract_path_and_guards(expr)
body = quote do: Plug.Conn.put_private(var!(conn), :websocket, unquote(opts[:connect]))
options = Keyword.delete(opts, :connect)
quote bind_quoted: [
method: method,
path: path,
options: options,
guards: Macro.escape(guards, unquote: true),
body: Macro.escape(body, unquote: true)
] do
route = Plug.Router.__route__(method, path, guards, options)
{conn, method, match, params, host, guards, private, assigns} = route
defp do_match(unquote(conn), unquote(method), unquote(match), unquote(host))
when unquote(guards) do
unquote(private)
unquote(assigns)
merge_params = fn
%Plug.Conn.Unfetched{} -> unquote({:%{}, [], params})
fetched -> Map.merge(fetched, unquote({:%{}, [], params}))
end
conn = update_in(unquote(conn).params, merge_params)
conn = update_in(conn.path_params, merge_params)
Plug.Router.__put_route__(conn, unquote(path), fn var!(conn) -> unquote(body) end)
end
end
end
@doc ~S"""
Dispatches to the event source.
Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via HTTP connection.
See `Plug.Router.match/3` for more examples.
## Example
```
sse "/eventsource", source: ExampleEventSource
```
"""
defmacro sse(expr, opts) do
method = :get
{path, guards} = extract_path_and_guards(expr)
body = quote do: Plug.Conn.put_private(var!(conn), :websocket, {:sse, unquote(opts[:source])})
options = Keyword.delete(opts, :source)
quote bind_quoted: [
method: method,
path: path,
options: options,
guards: Macro.escape(guards, unquote: true),
body: Macro.escape(body, unquote: true)
] do
route = Plug.Router.__route__(method, path, guards, options)
{conn, method, match, params, host, guards, private, assigns} = route
defp do_match(unquote(conn), unquote(method), unquote(match), unquote(host))
when unquote(guards) do
unquote(private)
unquote(assigns)
merge_params = fn
%Plug.Conn.Unfetched{} -> unquote({:%{}, [], params})
fetched -> Map.merge(fetched, unquote({:%{}, [], params}))
end
conn = update_in(unquote(conn).params, merge_params)
conn = update_in(conn.path_params, merge_params)
Plug.Router.__put_route__(conn, unquote(path), fn var!(conn), _ -> unquote(body) end)
end
@has_sse_route true
end
end
@doc false
@spec on_def(term, :def | :defp, atom, term, term, term) :: term
# credo:disable-for-next-line
def on_def(env, :defp, :do_match, [{:conn, _, Plug.Router}, _method, _path, _], _guards, _body) do
if forward = Module.get_attribute(env.module, :plug_forward_target) do
unless forward in Module.get_attribute(env.module, :plug_forwards) do
Module.put_attribute(env.module, :plug_forwards, forward)
end
end
end
# credo:disable-for-next-line
def on_def(_env, _type, _name, _args, _guards, _body), do: :ignore
# Extract the path and guards from the path.
defp extract_path_and_guards({:when, _, [path, guards]}), do: {extract_path(path), guards}
defp extract_path_and_guards(path), do: {extract_path(path), true}
defp extract_path({:_, _, var}) when is_atom(var), do: "/*_path"
defp extract_path(path), do: path
end