Skip to content

Instantly share code, notes, and snippets.

@colinbankier
Last active February 4, 2016 13:42
Show Gist options
  • Save colinbankier/e90dcb7c2de9a245269f to your computer and use it in GitHub Desktop.
Save colinbankier/e90dcb7c2de9a245269f to your computer and use it in GitHub Desktop.

Put a plug in it!

Plug

Elixir News

Latest release v1.2.2. 1.2.x brought us: Erlang 18 support

  • Faster compile times
  • Maps now support millions of keys efficiently - Dict/HashDict/Set/HashSet deprecated.

Language improvements

  • Multi aliases alias MyApp.{Foo, Bar, Baz}
  • variables in map keys %{key => value}
  • pin operator in map keys and function clauses
   %{^key => value} = %{key => value}
   fn ^key -> :ok end
  • New with special form to match multiple expressions
   with {:ok, contents} <- File.read("my_file.ex"),
       {res, binding} <- Code.eval_string(contents),
       do: {:ok, res}

More companies switching to Elixir, particularly from Ruby: Semaphore CI, Engineyard.

Regulars

Looking for resources, here is a good index: https://github.com/elixir-lang/elixir/wiki

If you haven't seen them, the last elixirconf videos are still awesome: https://www.youtube.com/playlist?list=PLE7tQUdRKcyZb7L66A9JvYWu_ItURk8qJ

Please let us know things you want to hear about/do at future meetups, and we'd LOVE more volunteers to present, even 5 min lightning talks.

What is Plug?

Plug is small library that provides a contract for web applications in elixir. In this respect it is the equivilent of similar libraries in other technologies such as Rack in Ruby, WSGI in Python and the Servlet API in Java.

Plug also provides some connection adapters for different web servers in the erlang VM.

Why is Plug interesting?

Plug allows you to write truely composable modules in between web applications. For example, a larger web framework like Phoenix is basically composed of a bunch of Plug modules providing abstractions such as Endpoints, Routers and Controllers.

The basic idea of Plug is to provide a single abstraction of a Connection which we operate on, which differs from libraries like Rack which explicitly separate the concepts of request and response.

Understanding Plug is critical to understanding how Phoenix works, and effectively writing your own extensions when it doesn't support the functionality you need by default.

The Plug Specification

Plugs github readme has a good overview and intro: https://github.com/elixir-lang/plug

Another good article is https://medium.com/@kansi/elixir-plug-unveiled-bf354e364641#.jr5jkb6kn

Plugs come in two flavours - Functional Plugs and Module Plugs.

Functional Plugs

In order to act as a plug, a function simply needs to accept a connection struct (%Plug.Conn{}) and options. It also needs to return the connection struct. Any function that meets those criteria will do. Here's an example.

def put_headers(conn, key_values) do
    Enum.reduce key_values, conn, fn {k, v}, conn ->
      Plug.Conn.put_resp_header(conn, k, v)
    end
end

Module Plugs

Module plugs are another type of Plug that let us define a connection transformation in a module. The module only needs to implement two functions: init/1 which initializes any arguments or options to be passed to call/2 call/2 which carries out the connection transformation. call/2 is just a function plug that we saw earlier.

To see this in action, let's see a module plug that puts the :locale key and value into the connection assign for downstream use in other plugs, controller actions, and our views.

defmodule HelloPhoenix.Plugs.Locale do
  import Plug.Conn
  @locales ["en", "fr", "de"]

  def init(default), do: default

  def call(%Plug.Conn{params: %{"locale" => loc}} = conn,
           _default) when loc in @locales do
    assign(conn, :locale, loc)
  end

  def call(conn, default), do: assign(conn, :locale, default) end
end

Let's follow this great tutorial, building a Plug web application that handles HTML templates, database models and all: (I didn't write this tute - why reinvent the wheel when there so much good content out there?!)

https://codewords.recurse.com/issues/five/building-a-web-framework-from-scratch-in-elixir

Putting more Plugs in

Ok, that tute was neat, but they failed to mention that Plug actually already a lot of for you for free (although that was great to understand how it works). The Plug library includes a bunch of useful default 'plugs', such as Plug.Router, which does a similar thing to what we just implemented, but better. Let's update our Helloplug to include the following: (remove our exiting functions)

  use Plug.Router
  plug :match
  plug :dispatch

  require EEx # you have to require EEx before using its macros outside of functions
  EEx.function_from_file :defp, :template_show_user, "templates/show_user.eex", [:user]

  get "/hello" do
    conn |> Plug.Conn.send_resp(200, "Hello, world!")
  end

  get "users/:user_id" do
    case Helloplug.Repo.get(User, user_id) do
      nil ->
        conn |> Plug.Conn.send_resp(404, "User with that ID not found, sorry")
      user ->
        page_contents = template_show_user(user)
        conn |> Plug.Conn.put_resp_content_type("text/html") |> Plug.Conn.send_resp(200, page_contents)
    end
  end

  match _ do
    send_resp(conn, 404, "oops")
  end

use Plug.Router provides the macro definitions for get and match - you can imagine from our previous tute how such thsings may be implemented. This is a lot neater. Remember though, we talked about defining our own plugs as functions. Let's give that a try. Add the following, ensuring that the first line is above the existing plug :dispatch line.

  plug :put_headers, %{"Rock Star Status" => "Legendary"}

  def put_headers(conn, key_values) do
    Enum.reduce key_values, conn, fn {k, v}, conn ->
      IO.puts "putting header #{k} #{v}"
      Plug.Conn.put_resp_header(conn, k, v)
    end
  end

Ha! Now we can start cooking! We're defining our own composable pipeline of plugs to handle our request. Plug ships a bunch of handy plugs:

  • Plug.CSRFProtection - adds Cross-Site Request Forgery protection to your application. Typically required if you are using Plug.Session;
  • Plug.Head - converts HEAD requests to GET requests;
  • Plug.Logger - logs requests;
  • Plug.MethodOverride - overrides a request method with one specified in headers;
  • Plug.Parsers - responsible for parsing the request body given its content-type;
  • Plug.RequestId - sets up a request ID to be used in logs;
  • Plug.Session - handles session management and storage;
  • Plug.SSL - enforce requests through SSL;
  • Plug.Static - serves static files;

Let's try add plug Plug.Logger to our module just for fun...

Adding a Supervisor

Ok, we're looking good, but it is hell annoying to have to start your http endpoint and the repo manually. Elixir (or rather, OTP) provides built-in constructs to this kind of stuff for us.

In our mix.exs we can specify an application callback module, which can be any module that implements the Application behaviour.

Let's update our application function to be:

  def application do
    [mod: {Helloplug.Supervisor, []}, applications: [:logger, :cowboy, :plug, :ecto, :sqlite_ecto]]
  end

Now, we just need to implement Application. Create a new file under lib with:

defmodule Helloplug.Supervisor do
  use Application

  def start( _type, _args ) do
    import Supervisor.Spec, warn: false

    children = [
      worker(Helloplug, [], function: :run),
      worker(Helloplug.Repo, [])
    ]

    opts = [strategy: :one_for_one, name: Helloplug.Supervisor]
    Supervisor.start_link(children, opts)
  end

  def run do
    { :ok, _ } = Plug.Adapters.Cowboy.http Helloplug, []
  end
end

Now, when we start an iex session, our Cowboy endpoint and our Repo are always started.

A custom mix task

Ok, things are looking good, but what if I wanted to start my application without an iex session? Well, we can use plain old mix like this:

mix run --no-halt

But, wouldn't it be nice to wrap that in a custom mix task? Take a look at this tute for more details on creating mix tasks: http://joeyates.info/2015/07/25/create-a-mix-task-for-an-elixir-project/ Here's the low down: Simply create a file in lib/mix/tasks/ called helloplug.run.ex with the following:

defmodule Mix.Tasks.Helloplug.Run do
  use Mix.Task

  def run(_) do
    IO.puts "Helloplug starting..."
    Mix.Task.run "app.start", []
    :timer.sleep :infinity
  end
end

And that's it! "app.start" is a built in task in mix, which compiles stuff that has changed, starts dependency apps etc. See http://elixir-lang.org/docs/stable/mix/Mix.Tasks.App.Start.html for more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment