defmodule Oasis.Router do
@moduledoc ~S"""
Base on `Plug.Router` to add a pre-parser to convert and validate the path param(s) in the
final generated HTTP verb match functions.
The generated router module uses `Oasis.Router` to instead of `Plug.Router`, in fact, they don't make a
huge difference, except that specify parameters which will then be available and types coverted
in the function body:
get("/hello/:id",
private: %{
path_schema: %{
"id" => %{
"schema" => %ExJsonSchema.Schema.Root{
schema: %{"type" => "integer"}
}
}
}
}) do
# Notice that the `:id` variable is an integer
send_resp(conn, 200, "hello #{id}")
end
The `:id` parameter will also be available and coverted as an integer in the function body as
`conn.params["id"]` and `conn.path_params["id"]`.
"""
@doc false
defmacro __using__(_opts) do
quote location: :keep do
use Plug.Router
import Plug.Router,
except: [
delete: 3,
delete: 2,
get: 3,
get: 2,
head: 3,
head: 2,
options: 3,
options: 2,
patch: 3,
patch: 2,
post: 3,
post: 2,
put: 3,
put: 2
]
import Oasis.Router
@before_compile Oasis.Router
Module.register_attribute(__MODULE__, :oas_path_schemas, accumulate: true)
def call(conn, opts) do
conn = put_private(conn, :oasis_router, __MODULE__)
super(conn, opts)
end
def match(conn, _opts) do
parse_and_then_do_match(conn, conn.method, Plug.Router.Utils.decode_path_info!(conn))
end
defoverridable [call: 2, match: 2]
end
end
@doc false
defmacro __before_compile__(env) do
compile_match(env)
end
defp compile_match(env) do
oas_path_schemas = Module.get_attribute(env.module, :oas_path_schemas, [])
default_ast =
quote location: :keep do
defp parse_and_then_do_match(conn, _method, match) do
do_match(conn, conn.method, match, conn.host)
end
end
Enum.reduce(oas_path_schemas, [{:__block__, [], [default_ast]}], fn path_schema, acc ->
[compile_match_by_path_schema(path_schema) | acc]
end)
end
defp compile_match_by_path_schema({method, path, type, options}) do
{_vars, match} = Plug.Router.Utils.build_path_match(path)
schemas = map_path_schemas(match, type)
plug_to = options[:to]
quote location: :keep,
bind_quoted: [
method: method,
plug_to: plug_to,
match: Macro.escape(match, unquote: true),
schemas: Macro.escape(schemas, unquote: true)
] do
defp parse_and_then_do_match(conn, unquote(method), unquote(match)) do
match =
unquote(match)
|> Enum.zip(unquote(schemas))
|> Enum.map(fn
{value, nil} ->
value
{value, {key, definition}} ->
try do
Oasis.Validator.parse_and_validate!(definition, "path", key, value)
rescue
reason in Oasis.BadRequestError ->
plug_to = unquote(plug_to)
module = if plug_to == nil, do: __MODULE__, else: plug_to
Plug.ErrorHandler.__catch__(
conn,
:error,
reason,
reason,
__STACKTRACE__,
&module.handle_errors/2
)
end
end)
do_match(conn, conn.method, match, conn.host)
end
end
end
defp map_path_schemas(match, type) do
Enum.map(match, fn
{param_name, _, _} ->
param_name = Atom.to_string(param_name)
{param_name, Macro.escape(Map.get(type, param_name))}
_ ->
nil
end)
end
@doc false
defmacro put_attribute_oas_path_schemas(_method, _path, nil, _options), do: nil
defmacro put_attribute_oas_path_schemas(method, path, path_schema, options) do
quote location: :keep do
Module.put_attribute(
__MODULE__,
:oas_path_schemas,
{
Plug.Router.Utils.normalize_method(unquote(method)),
unquote(path),
unquote(path_schema),
unquote(options)
}
)
end
end
@doc false
defmacro delete(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro get(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro head(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro options(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro patch(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro post(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
@doc false
defmacro put(path, options, contents \\ []) do
expand_route_match(__ENV__, path, options, contents)
end
defp expand_route_match(%Macro.Env{function: {http_verb, _}}, path, options, contents) do
path_schema = extrace_path_schema_from_options(options)
quote location: :keep do
put_attribute_oas_path_schemas(unquote_splicing([http_verb, path, path_schema, options]))
Plug.Router.unquote(http_verb)(unquote_splicing([path, options, contents]))
end
end
defp extrace_path_schema_from_options(options) when is_list(options) do
extrace_path_schema(options[:private])
end
defp extrace_path_schema_from_options(_options), do: nil
defp extrace_path_schema({_, _, opts}), do: Keyword.get(opts, :path_schema)
defp extrace_path_schema(_), do: nil
end