Current section
Files
Jump to
Current section
Files
lib/camarero.ex
defmodule Camarero do
@moduledoc false
require Plug.Router
defmacro __using__(opts \\ []) do
scaffold = Keyword.get(opts, :scaffold, :full)
into = Keyword.get(opts, :into, {:%{}, [], []})
[
quote(location: :keep, do: @after_compile({Camarero, :handler!})),
case scaffold do
:full ->
quote(
# bind_quoted: [into: into],
location: :keep,
do: use(Camarero.Plato, into: unquote(into))
)
:access ->
quote(
# bind_quoted: [into: into],
location: :keep,
do: use(Camarero.Tapas, into: unquote(into))
)
:none ->
[]
end
]
end
# idea by Dave Thomas https://twitter.com/pragdave/status/1077775018942185472
def handler!(env, _bytecode) do
if Code.ensure_compiled?(Camarero.Handler) do
:code.purge(Camarero.Handler)
:code.delete(Camarero.Handler)
end
ast = handler_ast()
Module.create(
Camarero.Handler,
quote(do: unquote(Macro.expand(ast, env))),
Macro.Env.location(env)
)
end
@spec handler_wrapper(endpoint :: binary(), block :: any()) :: any()
defp handler_wrapper(endpoint, block) do
path = endpoint
route = Plug.Router.__route__(:get, path, true, [])
{conn, method, match, params, host, guards, private, assigns} = route
quote do
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 conn -> unquote(block) end)
end
end
end
defp handler_ast() do
root = ("/" <> (:camarero |> Application.get_env(:root, ""))) |> String.trim("/")
{routes, ast} =
:camarero
|> Application.get_env(:carta, [])
|> Enum.filter(&Code.ensure_compiled?/1)
|> Enum.sort_by(&(&1 |> apply(:plato_route, []) |> String.length()), &<=/2)
|> Enum.reduce(
{[], []},
fn module, {routes, ast} ->
endpoint = Enum.join([root, module |> apply(:plato_route, []) |> String.trim("/")], "/")
get_all_block =
quote do
values = apply(unquote(module), :plato_all, [])
send_resp(
conn,
200,
Jason.encode!(%{key: "★", value: values})
)
end
get_all = handler_wrapper(endpoint, get_all_block)
param = Macro.var(:param, nil)
get_param_block = quote(do: response!(conn, unquote(module), unquote(param)))
get_param = handler_wrapper(Enum.join([endpoint, ":param"], "/"), get_param_block)
{[{endpoint, module} | routes], [get_all, get_param | ast]}
end
)
full_path = Macro.var(:full_path, nil)
catch_all_block =
quote do
[param | path] = Enum.reverse(unquote(full_path))
path = path |> Enum.reverse() |> Enum.join("/")
with {nil, _} <- {Camarero.Catering.Routes.get(path <> "/" <> param), ""},
{nil, _} <- {Camarero.Catering.Routes.get(path), param} do
send_resp(
conn,
400,
Jason.encode!(%{
error: "Handler was not found",
path: Enum.join([unquote(root) | unquote(full_path)], "/")
})
)
else
{module, param} -> response!(conn, module, param)
end
end
catch_dynamic = handler_wrapper(Enum.join([root, "*full_path"], "/"), catch_all_block)
catch_all = handler_wrapper("/*full_path", catch_all_block)
quote location: :keep do
@moduledoc false
use Plug.Router
plug(:match)
@root unquote(root)
Module.register_attribute(__MODULE__, :routes, accumulate: true, persist: true)
defp response!(conn, module, param) do
{status, response} =
case apply(module, :plato_get, [param]) do
{:ok, value} -> {200, %{key: param, value: value}}
:error -> {404, %{key: param, error: :not_found}}
{:error, {status, cause}} -> {status, cause}
end
send_resp(conn, status, Jason.encode!(response))
end
def routes, do: unquote(routes)
unquote_splicing(Enum.reverse([catch_all, catch_dynamic | Enum.reverse(ast)]))
plug(:dispatch)
end
end
defmodule Handler do
use Plug.Router
@root "/" <> (:camarero |> Application.get_env(:root, "") |> String.trim("/"))
get("#{@root}/*full_path") do
send_resp(
conn,
503,
Jason.encode!(%{
error: "Warming up...",
path: Enum.join([@root | full_path], "/")
})
)
end
plug(:dispatch)
end
defmodule Endpoint do
@moduledoc false
use Plug.Builder
plug(Plug.Logger)
plug(Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Jason
)
plug(Camarero.Handler)
end
end