Packages
mazurka
0.3.25
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
hypermedia api toolkit
Current section
Files
Jump to
Current section
Files
lib/mazurka/protocol/http/router.ex
defmodule Mazurka.Protocol.HTTP.Router do
@moduledoc """
A DSL for mazurka resources
"""
@doc false
defmacro __using__(_) do
quote location: :keep do
import Mazurka.Protocol.HTTP.Router
@before_compile Mazurka.Protocol.HTTP.Router
use Mazurka.Protocol.HTTP.Request
use Plug.Builder
defp match(conn, _opts) do
{mod, params} = do_match(conn.method, conn.path_info, conn.host)
conn
|> Plug.Conn.put_private(:mazurka_route, mod)
|> Plug.Conn.put_private(:mazurka_params, params)
end
def resolve(mod) do
resolve(mod, %{})
end
defp dispatch(%Plug.Conn{assigns: assigns} = conn, _opts) do
route = Map.get(conn.private, :mazurka_route)
params = Map.get(conn.private, :mazurka_params)
Mazurka.Protocol.HTTP.Router.__handle__(route, params, conn)
end
defoverridable [match: 2, dispatch: 2]
end
end
@doc false
defmacro __before_compile__(env) do
test_mod = if Mix.env == :test do
tests = Mazurka.Resource.Test.get_tests(env.module)
quote do
defmodule Tests do
tests = unquote(Macro.escape(tests))
defmacro __using__(_) do
tests = unquote(Macro.escape(tests))
quote do
import ExUnit.Callbacks
import ExUnit.Assertions
import ExUnit.Case
import ExUnit.DocTest
unquote(tests)
end
end
end
end
end
quote do
import Mazurka.Protocol.HTTP.Router, only: []
unquote(test_mod)
def resolve(_, _) do
{:error, :not_found}
end
end
end
defmacro match(path, options, contents \\ []) do
options = options |> Mazurka.Compiler.Utils.eval(__CALLER__)
contents = contents |> Mazurka.Compiler.Utils.eval(__CALLER__)
compile(nil, path, options, contents)
end
for method <- [:get, :post, :put, :patch, :delete, :options, :head] do
defmacro unquote(method)(path, options, contents \\ []) do
options = options |> Mazurka.Compiler.Utils.eval(__CALLER__)
contents = contents |> Mazurka.Compiler.Utils.eval(__CALLER__)
compile(unquote(method), path, options, contents)
end
end
## TODO use forward macro from Plug.Router.forward
@doc false
def __route__(method, path, guards, options) do
{method, guards} = build_methods(List.wrap(method || options[:via]), guards)
{vars, match} = Plug.Router.Utils.build_path_match(path)
{map_params, list_params} = format_params(vars)
{method, match, map_params, list_params, Plug.Router.Utils.build_host_match(options[:host]), guards}
end
def format_params(vars) do
map_params = Enum.map(vars, fn(var) ->
{:erlang.list_to_binary(:erlang.atom_to_list(var)), {var, [], nil}}
end)
list_params = Enum.map(vars, fn(var) ->
{var, [], nil}
end)
{{:%{}, [], map_params}, list_params}
end
@doc false
def __resolve__(method, path, _host) do
{format_method(method),
format_path(path), []}
end
defp format_method({:_, _, _}), do: "GET"
defp format_method(method), do: method
defp format_path({:_path, _, _}), do: []
defp format_path(path), do: path
def __handle__(mod, _params, conn) do
accepts = Plug.Conn.get_req_header(conn, "accept") |> Mazurka.Protocol.HTTP.AcceptHeader.handle()
dispatch = conn.private[:mazurka_dispatch]
apply(mod, :action, [conn, &dispatch.resolve/7, accepts])
|> handle_resource_resp(conn)
end
defp handle_resource_resp({:ok, body, conn, content_type}, _) do
conn
|> Plug.Conn.put_resp_content_type(content_type)
|> handle_transition()
|> handle_invalidations()
|> handle_response(body)
end
defp handle_resource_resp({:error, :unacceptable}, conn) do
conn
|> Plug.Conn.send_resp(:not_acceptable, "Not Acceptable")
end
defp handle_transition(%Plug.Conn{private: %{mazurka_transition: location}, status: status} = conn) do
## https://en.wikipedia.org/wiki/HTTP_303
conn = Plug.Conn.put_resp_header(conn, "location", location)
status = status || 303
%{conn | status: status}
end
defp handle_transition(conn) do
conn
end
defp handle_invalidations(%Plug.Conn{private: %{mazurka_invalidations: invalidations}} = conn) do
Enum.reduce(invalidations, conn, &(put_resp_header(&2, "x-invalidates", &1)))
end
defp handle_invalidations(conn) do
conn
end
defp handle_response(conn, nil) do
status = conn.status || 204
Plug.Conn.send_resp(conn, status, "")
end
defp handle_response(conn, body) do
Plug.Conn.send_resp(conn, choose_status(conn), body)
end
defp choose_status(%Plug.Conn{private: %{mazurka_error: true}, status: status}) do
status || 500
end
defp choose_status(%Plug.Conn{status: status}) do
status || 200
end
defp put_resp_header(%Plug.Conn{resp_headers: headers} = conn, key, value) do
%{conn | resp_headers: [{key, value} | headers]}
end
defp compile(method, expr, options, contents) do
{mod, options} =
cond do
is_atom(options) ->
{options, []}
is_atom(contents) ->
{contents, options}
true ->
raise ArgumentError, message: "expected module handler to be an atom"
end
{path, guards} = extract_path_and_guards(expr)
matches = quote bind_quoted: [method: method,
path: path,
options: options,
guards: Macro.escape(guards, unquote: true),
mod: mod] do
{method, match, map_params, list_params, host, guards} = Mazurka.Protocol.HTTP.Router.__route__(method, path, guards, options)
{res_method, res_match, res_host} = Mazurka.Protocol.HTTP.Router.__resolve__(method, match, host)
defp do_match(unquote(method), unquote(match), unquote(host)) when unquote(guards) do
{unquote(mod), unquote(map_params)}
end
def resolve(unquote(mod), unquote(map_params) = params) do
has_values = Enum.all?(params, fn
({_, nil}) -> false
({_, :undefined}) -> false
({_, _}) -> true
end)
if has_values do
{:ok, unquote(res_method), unquote(res_match)}
else
{:error, :not_found}
end
end
def resolve(unquote(mod), unquote(list_params) = params) do
has_values = Enum.all?(params, fn
(nil) -> false
(:undefined) -> false
(_) -> true
end)
if has_values do
{:ok, unquote(res_method), unquote(res_match)}
else
{:error, :not_found}
end
end
end
info = if Mix.env !== :dev && is_elixir_module?(mod) do
quote do
require unquote(mod)
## Auto include resource tests
:erlang.function_exported(unquote(mod), :tests, 1) and unquote(mod).tests(__MODULE__)
end
end
[matches, info]
end
defp is_elixir_module?(mod) do
mod |> to_string |> String.downcase |> String.to_atom != mod
end
# Convert the verbs given with `:via` into a variable and guard set that can
# be added to the dispatch clause.
defp build_methods([], guards) do
{quote(do: _), guards}
end
defp build_methods([method], guards) do
{Plug.Router.Utils.normalize_method(method), guards}
end
defp build_methods(methods, guards) do
methods = Enum.map methods, &Plug.Router.Utils.normalize_method(&1)
var = quote do: method
guards = join_guards(quote(do: unquote(var) in unquote(methods)), guards)
{var, guards}
end
defp join_guards(fst, true), do: fst
defp join_guards(fst, snd), do: (quote do: unquote(fst) and unquote(snd))
# 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