Elixir Plug unveiled

Recently I have been trying to wrap my head around Phoenix Framework during which I stumbled upon Plug which is surely an interesting project going on in the Elixir world. What makes Plug interesting is the way with which it allows you to write clean and composable code. Understanding how to use Plug is pretty easy, thanks to the good documentation it has and examples which illustrate its usage. Following are few good places where you can learn about it :

So what is this “Plug” project really ?? Simply put, Plug allows you define modules which act as an interfaces between different web applications hence allowing them to communicate.


In this post I am not going to discuss how to use Plug but rather explore how it works ! This is because I find the code written using Plug to be really neat and if you have read the first link you would feel the same ! But if you didn't I will try to trigger your curiosity with the following example which, by the way, is taken from the Phoenix project:

defmodule HelloPhoenix.MessageController do
use HelloPhoenix.Web, :controller

def show(conn, params) do
case authenticate(conn) do
{:ok, user} ->
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/")
message ->
case authorize_message(conn, params["id"])
:ok ->
render conn, :show, page: find_page(params["id"])
:error ->
conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/")
end
end
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/")
end
end
end

Don't be intimidated by the above code just focus on the show() function here. Observe the case statements, in outermost case statement we try to authenticate user, if this operation succeeds we have another nested case statement that tries to find message and if it succeeds we have yet another nested case statement which tires to authorize it. Notice how these set of dependant operations, by dependant I mean that the next operation is executed only if the previous is executed successfully, leads to a nested and unreadable code. Now the above code when written using Plug looks as follows:

defmodule HelloPhoenix.MessageController do
use HelloPhoenix.Web, :controller

plug :authenticate
plug :find_message
plug :authorize_message


def show(conn, params) do
render conn, :show, page: find_page(params["id"])
end

defp authenticate(conn, _) do
case Authenticator.find_user(conn) do
{:ok, user} ->
assign(conn, :user, user)
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/") |> halt
end
end

defp find_message(conn, _) do
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/") |> halt
message ->
assign(conn, :message, message)
end
end

defp authorize_message(conn, _) do
if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
conn
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/") |> halt
end
end
end

Notice that all the nested operations have been converted into individual functions and on top we have defined an invocation sequence i.e.

  plug :authenticate
plug :find_message
plug :authorize_message

using the plug macro. Isn't this cool ! You can write whatever functions you want and define a pipeline sequence (as above) over them. This way the error handling for a particular operation is contained in the function itself rather than being all over the place.

I hope the above example is intriguing enough to investigate the sorcery behind Plug ! But before we have look at the source code let's see how to write a plug

defmodule MyPlug do
use Plug.Builder

plug :set_header
plug :send_ok

def set_header(conn, _opts) do
put_resp_header(conn, "x-header", "set")
end

def send_ok(conn, _opts) do
send(conn, 200, "ok")
end
end

The above code example has been taken from Elixir docs and we will call it “the basic plug”, keep this name in mind as I will be using it quite a few times. The code seems pretty much similar to previous plug code we have seen, apart from the use Plug.Builder statement. To understand this code we dive into the Plug source code which is openly available !

The link below is the only module you will need to understand from the Plug source code to get a basic picture of what is going on.

https://github.com/elixir-lang/plug/blob/master/lib/plug/builder.ex

Also there some links available at the end of this article which will prove to be useful while we dissect the above source code. So I suggest you open them in separate tabs, come back and read on … don't get let stuck on those links while opening new tabs … do come back !


Now that we have written our “the basic plug” we will try to compile our module. We know that during compilation process before the code is converted to binary, macros are expanded and replaced with their definitions (If you didn't know … now you do !). As I mentioned previously our “the basic plug” implementation calls Plug.Builder with use macro, so the macro will be expanded before our module gets compiled to binary. The use macro invokes __using__/1 (see below) from the builder.ex module.

defmacro __using__(opts) do
quote do
@behaviour Plug
@plug_builder_opts unquote(opts)
    def init(opts) do
opts
end
    def call(conn, opts) do
plug_builder_call(conn, opts)
end

defoverridable [init: 1, call: 2]

import Plug.Conn
import Plug.Builder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: true)
@before_compile Plug.Builder
end
end
_______
file: builder.ex

Notice that upon expansion the above macro will inject @behaviour Plug in the calling module. The Plug behaviour basically makes init/1 and call/2 callbacks necessary. Next we initialize @plug_builder_opts and then we have two imports statements after this. We also register a :plugs attribute using the register_attribute/3 function. Lastly we call Plug.Builder with before_compile macro i.e.

@before_compile Plug.Builder

which will call __before_compile__/1 from Plug.Builder. Since before_compile is a macro, here again we will end up expanding it. The __before_compile__/1 in builder.ex is defined as follows,

defmacro __before_compile__(env) do
plugs = Module.get_attribute(env.module, :plugs)
builder_opts = Module.get_attribute(env.module, :plug_builder_opts)
  if plugs == [] do
raise "no plugs have been defined in #{inspect env.module}"
end
  {conn, body} = Plug.Builder.compile(env, plugs, builder_opts)
  quote do
defp plug_builder_call(unquote(conn), _), do: unquote(body)
end
end
_____
file: builder.ex

The above code gets all the plugs and check if the plugs list is empty and raises exception if it is. Lastly it injects plug_build_call/2 function back into the caller which is __using__/1 . Notice that this newly injected function i.e. plug_build_call/2 is already called in call/2 function in __using__/1. One thing you might be wondering here is, how did the plugs variable get the plug list. To that I would say we extracted it from the module attribute plugs using get_attribute/2. But when did the plug list get stored in the module attribute plugs ?? Good question ! Recall our “the basic plug” code where we were building our first plug, we had the following statements:

  plug :set_header
plug :send_ok

here plug is macro which is defined in builder.ex as follows

defmacro plug(plug, opts \\ []) do
quote do
@plugs {unquote(plug), unquote(opts), true}
end
end

So all our plug :function_name will be expanded to

@plugs {unquote(plug), unquote(opts), true}

also recall that we registered a module attribute called :plugs and if you have a look at the docs here (look at the example usage) you will understand how the plugs get aggregated. After this point code injection is complete and our module’s compilation moves to the final phase i.e. convert to binary.

Our plug can be used as follows,

Plug.Adapters.Cowboy.http MyPlug, []

where the http function calls the init/1 of our plug. Whenever Cowboy receives a request call/2 function is invoked which executes the plug pipeline from :plugs list.

So, this is it ! Plug basically works by injecting a lot of code (based on our module environment) just before code is compiled to binary (in the AST phase).