Demystify Ewebmachine DSL
View SourceIt's very likely, as a reader of this documentation, that you already wrote a a
route with Ewebmachine (or at least copy and pasted one), but did you ever
wonder once how does it works under the hood? Maybe you did start looking into
it and were repelled by the heavy use of macro.
This document aims to go through some of Ewebmachine's internals, in order to
explain how, from a bunch of macros, we end up with a whole Plug pipeline.
Let's start with this small module:
defmodule MyApi do
use Ewebmachine.Builder.Resources
resource "/api/path" do after
allowed_methods do: ["GET"]
defh(to_html, do: "<h1>HTML</h1>")
end
endIt imports the macro Ewebmachine.Builder.Resources.resource/[3-4] into the
scope, that we can then use to make the /api/path route.
From this point on, the macro's magic starts :).
How do handlers (allowed_methods and friends) work?
The resource macro creates a module from the given
body.
defmodule Ewebmachine.Builder.Resources do
defmacro resource({:__aliases__, _, route_aliases},route,do: init_block, after: body) do
resource_quote(Module.concat([__CALLER__.module|route_aliases]),route,init_block,body)
end
defmacro resource(route,do: init_block, after: body) do
resource_quote(Module.concat(__CALLER__.module,"EWM"<>route_as_mod(route)),route,init_block,body)
end
def resource_quote(wm_module,route,init_block,body) do
quote do
@wm_routes {unquote(route), unquote(wm_module), unquote(Macro.escape(init_block))}
defmodule unquote(wm_module) do
use Ewebmachine.Builder.Handlers
unquote(body)
plug :add_handlers
end
end
end
# [...]
endDynamic module
Dynamically named module aren't nested under their parent module. That's why
the resource macro concatenates it with the caller's module.
In this module each handler will become a function. As is, each handler is a macro.
The created module uses the Ewebmachine.Builder.Handler module. This module
defines the list of
handlers
(allowed_methods, etc...). For each handler defined in this list, a
macro
is created:
defmodule Ewebmachine.Builder.Handlers do
@resource_fun_names [
:allowed_methods,
# [...]
]
for resource_fun_name<-@resource_fun_names do
Module.eval_quoted(Ewebmachine.Builder.Handlers, quote do
@doc "see `Ewebmachine.Handlers.#{unquote(resource_fun_name)}/2`"
defmacro unquote(resource_fun_name)(do_block) do
name = unquote(resource_fun_name)
handler_quote(name,do_block[:do])
end
end)
end
# [...]
endInside this macro, the called function
handler_quote
takes care of adding the {name, __MODULE__} (where name is the handler's
name) to the module attribute @resource_handlers and defining a function.
defmodule Ewebmachine.Builder.Handlers do
defp handler_quote(name,body,guard,conn_match,state_match) do
quote do
@resource_handlers Map.put(@resource_handlers,unquote(name),__MODULE__)
def unquote(name)(unquote(conn_match)=var!(conn),unquote(state_match)=var!(state)) when unquote(guard) do
res = unquote(body)
wrap_response(res,var!(conn),var!(state))
end
end
end
# [...]
enddefh macro
defh
macro which allows you to pass guard, works the same way underneath and calls
handler_quote too.
Great we now know how handlers are transformed into functions.
But how are handlers called?
Adding custom handlers to the connection
The :add_handlers plug used by the created module takes care of adding
handler names saved into the module's attribute to the connection's private
field :resource_handlers.
use Ewebmachine.Builder.Handler defines a @before_compile Ewebmachine.Builder.Handler attributes in which the add_handlers plug
function
is defined:
defmodule Ewebmachine.Builder.Handlers do
defmacro __before_compile__(_env) do
quote do
defp add_handlers(conn, opts) do
# [ ... ]
Plug.Conn.put_private(conn, :resource_handlers,
Enum.into(@resource_handlers, conn.private[:resource_handlers] || %{}))
end
end
end
endInternal usage of custom handlers
Ewebmachine decision
tree
calls handlers when going through the tree. For instance, the
allowed_methods is
call
as such:
{methods, conn, state} = resource_call(conn, state, :allowed_methods)To use a custom handler, Ewebmachine simply looks up with the handler's name,
into its private connection field :resource_handlers (added by the
:add_handlers plug), which contains a map where keys are handler's names and
values are the handler's module. If you did not define a handler it falls back
to the default one inside the Ewebmachine.Handlers module.
defmodule Ewebmachine.Core.DSL do
def resource_call(conn, state, fun) do
handler = conn.private[:resource_handlers][fun] || Ewebmachine.Handlers
{reply, conn, state} = term = apply(handler, fun, [conn, state])
# [ ... ]
end
# [ ... ]
endHere is what the code would look like if we expand explained macros until now:
defmodule MyApi do
@before_compile Ewebmachine.Builder.Resources
use Plug.Router
import Plug.Router, only: []
import Ewebmachine.Builder.Resources
defp resource_match(conn, _opts) do
conn |> match(nil) |> dispatch(nil)
end
@wm_routes [{"/api/path", MyApi.EWMApiPath, []}]
end
defmodule MyApi.EWMApiPath do
use Plug.Builder
@resource_handlers %{
allowed_methods: __MODULE__,
to_html: __MODULE__
}
def allowed_methods(conn, state) do
res = ["GET"]
{res, conn, state}
end
def to_html(conn, state) do
res = "<h1>HTML</h1>"
{res, conn, state}
end
defp add_handlers(conn, _opts) do
# [...]
Plug.Conn.put_private(conn, :resource_handlers,
Enum.into(@resource_handlers, conn.private[:resource_handlers] || %{}))
end
plug :add_handlers
endHow does Ewebmachine call all of this?
The missing piece of the puzzle is now, how does Ewebmachine call our plug
module MyApi.EWMApiPath.
From the macros' expansion above, we can see that it uses the Plug.Router.
Moreover, the line @before_compile Ewebmachine.Builder.Resources isn't
expanded, let's look into it. Ewebmachine.Builder.Resources calls the
__before_compile__
macro
does the following:
defmacro __before_compile__(_env) do
wm_routes = Module.get_attribute __CALLER__.module, :wm_routes
route_matches = for {route,wm_module,init_block}<-Enum.reverse(wm_routes) do
quote do
Plug.Router.match unquote(route) do
init = unquote(init_block)
var!(conn) = put_private(var!(conn),:machine_init,init)
unquote(wm_module).call(var!(conn),[])
end
end
end
final_match = if !match?({"/*"<>_,_,_},hd(wm_routes)),
do: quote(do: Plug.Router.match _ do var!(conn) end)
quote do
unquote_splicing(route_matches)
unquote(final_match)
end
endwhich produces a Plug.Router's
match, giving us the
following once expanded:
defmodule MyApi do
use Plug.Router
import Plug.Router, only: []
import Ewebmachine.Builder.Resources
defp resource_match(conn, _opts) do
conn |> match(nil) |> dispatch(nil)
end
@wm_routes [{"/api/path", MyApi.EWMApiPath, :irrelevant_stuff}]
Plug.Router.match "/api/path" do
init = :irrelevant_stuff
conn = put_private(conn, :machine_init, init)
MyApiEWMApiPath.call(conn, [])
end
Plug.Router.match _ do conn
endThe only thing left to make the whole thing work is to add a few plugs. That's
what the Ewebmachine.Builder.Resources.resources_plugs macro usually does,
but let's use only the required bits:
defmodule MyApi do
# [...]
Plug.Router.match _ do conn
plug :resource_match
plug Ewebmachine.Plug.Run
plug Ewebmachine.Plug.Send
endThe :resource_match function plug finds a matching route (match(nil)) and
calls it if matching (dispatch(nil)). Once found the connection conn is
returned by the plug module (for instance here MyApiEWMApiPath), and now
contains our resource custom handlers.
Then the Ewebmachine.Plug.Run plug, which contains the Ewebmachine's
decision tree, is called, and its behaviour will change based on our custom
handlers.
Finally, the Ewebmachine.Plug.Send plug is called and sends the response if
the connection wasn't halted before.