Plug is an important part of Elixir's story for web applications. It describes itself as "a specification for composable modules in between web applications." You can think of plugs as Rack middlewares, if you're familiar with the Ruby library ecosystem. Let's have a look at what it takes to build one.

Project

Go ahead and kick off a new project, with mix new plug_playground and cd into the directory.

Out of the gate, we have to add the dependency to the mixfile: (mix.exs)

defp deps do
  [
    {:plug, github: "elixir-lang/plug"},
    {:cowboy, "~> 0.9", github: "extend/cowboy"}
  ]
end

Then fetch them with mix deps.get.

Now, the first thing we'll do is build the 'hello world' plug out of the documentation. Open up test/hello_plug_test.exs and let's start out with a test:

defmodule HelloPlugTest do
  use ExUnit.Case, async: true
  # Plug ships with its own test helpers that you can pull in by Using Plug.Test
  use Plug.Test

  @opts HelloPlug.init([])

  test "returns hello world" do
    # Create a test connection using the `conn` helper that Plug imported when
    # you used Plug.Test
    conn = conn(:get, "/")

    # Invoke the plug
    conn = HelloPlug.call(conn, @opts)

    # Test the output
    assert conn.state == :sent
    assert conn.status == 200
    assert conn.resp_body == "Hello world"
  end
end

Go ahead and run the tests, and they'll fail because there's no HelloPlug module yet. Go to the top of the test file, and we'll see what defining a plug looks like.

defmodule HelloPlug do
  import Plug.Connection

  def init(options) do
    # initialize your options here

    options
  end

  # Call is the primary interface to a plug
  # This is where the helpers that importing Plug.Connection gives you pay off
  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello world")
  end
end

Alright, run the tests, and they should pass. Now go ahead and extract HelloPlug into the lib dir.

Now, this is just showing that our Plug responds as the interface suggests, but we haven't set up a web server yet that routes requests to our Plug. This is trivial, so let's look at it. We'll make an integration test using httpc:

mkdir test/integration

Now, in order to use httpc, we need to start inets before our tests run. Open up test/test_helper.exs:

:application.start(:inets)

Now, open up test/integration/hello_plug_integration_test.exs:

defmodule Integration.HelloPlugIntegrationTest do
  use ExUnit.Case

  setup do
    Plug.Adapters.Cowboy.http HelloPlug, []
    :ok
  end

  test "fetching root gets Hello world" do
    body = fetch('/')
    assert body == 'Hello world'
  end

  def fetch(url) do
    { :ok, {{_version, 200, _reason_phrase}, _headers, body } } = :httpc.request('http://localhost:4000/' ++ url )
    body
  end
end

Alright, so you've stood up the full http stack and talked to your plug, and you're comfortable that it's working as suggested. Now, let's look at Plug.Builder to build a Plug that composes a chain of other plugs.

Open up test/reversing_plug_test.exs, copy the HelloPlugTest, and make a couple of tweaks:

defmodule ReversingPlugTest do
  use ExUnit.Case, async: true
  use Plug.Test

  @opts ReversingPlug.init([])

  test "returns Hello world reversed" do
    # Create a test connection
    conn = conn(:get, "/")

    # Invoke the plug
    conn = ReversingPlug.call(conn, @opts)

    # Test the output
    assert conn.state == :sent
    assert conn.status == 200
    assert conn.resp_body == "dlrow olleH"
  end
end

Alright, now we need to define our reversing plug. We'll put it at the top of the test for now:

defmodule ReversingPlug do
  use Plug.Builder
end

Now here, we're using Plug.Builder. Using this module allows you to build a plug stack. We're going to define a function plug that acts like the hello plug did, and we're going to add another function plug that reverses whatever content is already outgoing. Finally, we'll add a plug to actually send the content down the connection.

defmodule ReversingPlug do
  use Plug.Builder
  import Plug.Connection

  # So we'll plug in three function plugs
  plug :hello
  plug :reverse
  plug :sender

  # The hello function just sets the connection's response body and returns the
  # updated connection
  def hello(conn, _opts) do
    conn.resp_body("Hello world")
  end

  # The reverse function calls String.reverse on the connection's response body,
  # returning the updated connection
  def reverse(conn, _opts) do
    conn.resp_body(String.reverse(conn.resp_body))
  end

  # The sender actually sends the response down the socket to the web browser.
  # We had to add a plug responsible for this because, unlike Rack, plugs can
  # speak directly to the underlying connection.  This is why we couldn't reuse
  # our HelloPlug, since by the time our reversing plug got to the connection it
  # would have already sent its data down the wire.
  def sender(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, conn.resp_body)
  end
end

Go ahead and run the tests...and they'll pass.

Summary

In today's episode, we took a brief look at Plug and how to test it. See you soon!

Resources