defmodule GenRouter do @moduledoc """ Router to parse messages, modeled by the idea of simplified Plug.Conn router. """ alias GenRouter.Conn @doc """ Match specific route in scope of routes """ @spec match_in_scope(module(), atom(), [{String.t, atom(), atom()}], [atom()], String.t, %Conn{}, Keyword.t) :: %Conn{} def match_in_scope(router_module, scope, routes, scope_pipeline, path_suffix, conn, opts) do Enum.find(routes, :next_scope, fn({path, _controller, _action}) -> path === path_suffix or path === "/#{path_suffix}" end) |> case do {_path, controller, action} -> conn = Enum.reduce(scope_pipeline, conn, fn(pipeline, conn) -> if is_nil(conn.code) do apply(router_module, pipeline, [conn, opts]) else conn end end) if is_nil(conn.code) do apply(controller, action, [conn, opts]) else conn end :next_scope -> conn = %{conn | __skip__: Map.put(conn.__skip__, scope, true)} router_module.do_match(conn, opts) end end @doc """ Defines a pipeline to send the connection through. See `pipeline/2` for more information. """ defmacro pipe_through(pipes) do quote do if scope_pipeline = @scope_pipeline do @scope_pipeline (unquote(pipes) ++ scope_pipeline) else raise "cannot define pipe_through at the router level, match must be defined inside a scope" end end end @doc """ Defines default route or a route inside a pipeline. See `pipeline/2` for more information. """ defmacro match("*", controller, action) do quote do @default_route_set true def do_match(%Conn{} = conn, opts) do apply(unquote(controller), unquote(action), [conn, opts]) end def scopes, do: @scopes end end defmacro match("/", controller, action) do quote do if routes = @routes do @routes [{"", unquote(controller), unquote(action)} | routes] else raise "cannot define match at the router level, match must be defined inside a scope" end end end defmacro match(path, controller, action) do quote do if routes = @routes do @routes [{unquote(path), unquote(controller), unquote(action)} | routes] else raise "cannot define match at the router level, match must be defined inside a scope" end end end @doc """ Defines a plug inside a pipeline. See `pipeline/2` for more information. """ defmacro plug(plug, opts \\ []) do quote do if pipeline = @pipeline do @pipeline [{unquote(plug), unquote(opts), true} | pipeline] else raise "cannot define plug at the router level, plug must be defined inside a pipeline" end end end @doc """ Defines a plug pipeline. Pipelines are defined at the router root and can be used from any scope. """ defmacro pipeline(pipe, do: block) do block = quote do @pipeline [] unquote(block) end compiler = quote do def unquote(pipe)(%Conn{} = conn, _) do Enum.reduce(@pipeline, conn, fn {plug, opts, guards}, acc -> apply(plug, :call, [conn, opts]) end) end @pipeline nil end quote do try do unquote(block) unquote(compiler) after :ok end end end @doc """ Defines scope of routers with pipelines. """ defmacro scope(scope, scope_path, do: block) do block = quote do scopes = @scopes @scopes [unquote(scope) | scopes] @scope_pipeline [] @routes [] unquote(block) end skip = Map.put(%{}, scope, false) |> Macro.escape() path_length = String.length(scope_path) compiler = quote do def do_match( %Conn{ path: <> <> path_suffix, __skip__: unquote(skip) } = conn, opts ) when (path_prefix === unquote(scope_path)) do GenRouter.match_in_scope(__MODULE__, unquote(scope), @routes, @scope_pipeline, path_suffix, conn, opts) end @routes nil @scope_pipeline nil end quote do try do unquote(block) unquote(compiler) after :ok end end end @doc """ Custom router factory """ defmacro __using__(_opts) do quote do import GenRouter alias GenRouter.Conn @behaviour GenRouter.Behaviour @pipeline nil @scopes [] @scope_pipeline nil @routes nil @doc """ Delegate to scope based matching, generated by router config macros """ @spec match_message(map(), String.t, map(), map(), Keyword.t) :: Conn.t def match_message(message, path, scope \\ %{}, assigns \\ %{}, opts \\ []) do __MODULE__.match_message(__MODULE__, message, path, scope, assigns, opts) end end end end