mmag

ハマったことメモなど

ElixirのPlug

今日はPlugです。

Plugとは

まずPlugとはなんぞやというところですが

  1. A specification for composable modules in between web applications
  2. Connection adapters for different web servers in the Erlang VM

だそうです。イメージはRackです。今はCowboyというサーバにのみ対応してます。

導入

$ mix new hoge
# mix.exs (一部略)

def application do
  [applications: [:logger, :cowboy, :plug]]
end

defp deps do
  [{:cowboy, "~> 1.0"}, {:plug, "~> 0.6"}]
end
$ mix do deps.get, compile

このへんの詳しい情報は公式見るなりググってください

基本

まずはmodule plugと呼ばれるものを見てみます。init/1call/2が必要です。

# lib/hello_plug.ex

defmodule HelloPlug do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, opts) do
    name = Keyword.get(opts, :name, "")

    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello #{name}")
  end
end

Plug.Adapters.Cowboy.http HelloPlug, [name: "John"]
$ mix run --no-halt lib/hello_plug.ex

localhost:4000にアクセスすると、"Hello John"が返ってきます。init/1はオプションを受け取って初期化を担当します。こいつが返したものはcall/2の第2引数になります。call/2が受け取っているconnPlug.Conn構造体の変数で、リクエストメソッドとかヘッダといったリクエストフィールドを読んだり、クッキーやらセッションやら読み書きして、レスポンスヘッダとかボディとか200とか404とかセットしてこれを返すのですね。もう何でもできそうですね。WAFとか要らないですよね(嘘)

とはいえコレだけでは何かと限界があるので、Plug.Builderを使ってみましょう。

プラグスタック

defmodule StackedPlug do
  import Plug.Conn
  use Plug.Builder

  plug :append, "Hello"
  plug :append, "World"
  plug :respond

  def append(conn, word) do
    msg = (conn.assigns[:msg] || "") <> word
    assign(conn, :msg, msg)
  end

  def respond(conn, word) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, conn.assigns[:msg])
  end
end


Plug.Adapters.Cowboy.http StackedPlug, []

きっと"HelloWorld"が返ってくるかと思いますです。5~7行目に書いたplug :関数名 引数が上から順に実行されてる感じです。この複数のplugを順に適用する仕組みをプラグスタックと呼びます。appendとかrespondはfunction plugと呼ぶらしいです。また次のように書いてmodule plugをスタックに組み込むこともできます。

plug :my_func01
plug MyPlug
plug :my_func02

書いた順に処理されるなら、果たして本当にスタックなのか。ここには触れないでおきましょう。

便利なPlug

Plug.Router

ここまで来ると、こんなことを考え始める人もいるかと思います。

plug :match, path: "/",      msg; "Welcome"
plug :match, path: "/hello", msg: "Hello"
plug :not_found

"/"へのアクセスには"Welcome"を、"/hello"へのアクセスには"Hello"を返し、それ以外は404っていうルータみたいなこと。リクエストメソッドでも振り分ければ夢が広がりますね。こういう用途のために、Plug.Routerってのが用意されてます。

defmodule ExampleRouter do
  use Plug.Router
  import Plug.Conn

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/users/:id" do
    send_resp(conn, 200, "Hello, I am No.#{id}")
  end

  post "/users" do
    ...
  end

  match _ do
    send_resp(conn, 404, "Not Found")
  end
end

2, 5, 6行目が必須。特に5, 6行目の順番は大切。use Plug.Builderuse Plug.Routerがやってくれるので要りません。各ブロック内に突然connという変数が出現しているのですが、こいつはマクロが上手く処理してくれるのでエラーにはなりません。ただしconnという変数名を前提に作られているので、conとかconnectionとかにすると怒られます。あとは雰囲気でいけるでしょうかね。

Plug.Static

静的ファイルを配信するためのplugとしてPlug.Staticモジュールが用意されています。さっきのルータの例を

plug Plug.Static, at: "/images", from: "assets/imgs"
plug :match
plug :dispatch

こんな風にすると、assets/imgs以下にあるファイルを/images/hogehoge.pngみたいにサーブできます。

他にもPlug.Sessionとかあるっぽいんですが、疲れたんで。

まとめ

書いたことをまとめると、

  • ElixirのPlugは、RubyのRackに相当するものである
  • プラグスタックという仕組みによって、複数のPlugを組み合わせることができる
  • 便利なPlugがあらかじめ用意されている

Rackとの違いとしては、Rackアプリがリクエストを受けてレスポンスを返す構成であるのに対して、Plugはコネクションを受けてコネクションを返す構成であるという点が挙げられるらしいです。スタックのどの位置からもコネクションを通してレスポンスを返せるようにしてあるとか何とか。このへん自信無いので誰か教えてください。

テーマはFB matteをベースにしてます。作者さんに感謝を込めて。