Current section
Files
Jump to
Current section
Files
lib/buckaroo/router.ex
defmodule Buckaroo.Router do
@moduledoc ~S"""
An extension to `Plug.Router` now also supporting `websocket`.
"""
@doc false
defmacro __using__(opts) do
{helpers, opts} = Keyword.pop(opts, :helpers, [])
helpers =
Enum.flat_map(helpers, fn
:json -> [json!: 2, json!: 3]
end)
Module.register_attribute(__CALLER__.module, :buckaroo_docs, accumulate: true)
quote location: :keep do
use Plug.Router, unquote(opts)
import Buckaroo.Router, only: [sse: 2, websocket: 2]
import Buckaroo.Helpers, only: unquote(helpers)
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
docs = %Buckaroo.Docs{
router: env.module,
endpoints: Module.get_attribute(env.module, :buckaroo_docs, [])
}
api_docs =
"\n\n## Endpoints\n" <>
Enum.join(
Enum.map(docs.endpoints, fn endpoint ->
"""
### #{endpoint.method} `/#{Enum.map_join(endpoint.path, "/", &to_string/1)}`
#{endpoint.doc}
"""
end),
"\n\n"
)
quote do
@doc false
def __docs__, do: unquote(Macro.escape(docs))
@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
if @moduledoc do
@moduledoc @moduledoc <> unquote(api_docs)
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
config = %Buckaroo.Docs.Config{}
doc = Module.delete_attribute(env.module, :doc)
clean_method = method(method)
filter_path =
path |> path() |> Enum.map(&if(is_binary(&1), do: &1, else: inspect(&1))) |> Enum.join("/")
ignore? =
doc === false or
Enum.any?(config.reject, fn {m, p} -> clean_method in m and filter_path =~ p end)
if forward = Module.get_attribute(env.module, :plug_forward_target) do
unless forward in Module.get_attribute(env.module, :plug_forwards) do
unless ignore? do
p = path(path)
prefix = "/" <> Enum.join(p, "/")
Enum.each(forward.__docs__().endpoints, fn doc ->
Module.put_attribute(
env.module,
:buckaroo_docs,
doc
|> Map.update!(:path, &(p ++ &1))
|> Map.update!(:examples, fn examples ->
Enum.map(examples, fn example ->
Map.update!(example, :tests, fn tests ->
Enum.map(tests, fn test = %{request: req} ->
%{test | request: Map.update!(req, :path, &(prefix <> &1))}
end)
end)
end)
end)
)
end)
end
Module.put_attribute(env.module, :plug_forwards, forward)
end
else
unless ignore? do
docs = extract_doc(doc)
Module.put_attribute(
env.module,
:buckaroo_docs,
Map.merge(docs, %{method: clean_method, path: path(path), router: env.module})
)
end
end
end
# credo:disable-for-next-line
def on_def(_env, _type, _name, _args, _guards, _body), do: :ignore
defp method(method)
defp method({:_, _, _}), do: :all
defp method(method), do: method
defp path(path, acc \\ [])
defp path([], acc), do: acc |> :lists.reverse()
defp path({var, _, _}, acc), do: path([var], acc)
defp path([h | t], acc) do
case h do
part when is_binary(part) -> path(t, [part | acc])
part when is_atom(part) -> path(t, [part | acc])
{:|, _, [p, {:glob, _, _}]} -> path([p | t], acc)
{:|, _, sub} -> path(sub ++ t, acc)
{atom, _, _} when is_atom(atom) -> path(t, [atom | acc])
end
end
# 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
defp extract_doc(doc)
defp extract_doc({_, doc}) when is_binary(doc), do: Buckaroo.Docs.extract(doc)
defp extract_doc(_), do: Buckaroo.Docs.extract("")
end