Edited at
gumi Inc.Day 18

Elixir: PlugCowboyで簡単なルーティングを試す

PlugCowboyは、ErlangのwebサーバーCowboy用のPlugアダプタです。Pulgのドキュメントは「Installation」でPlugアダプタにPlugCowboyを使うものとしています。もっとも、リリースが2018年10月21日と日が浅いため、まとまった情報が少なく、比較的新しい記事でも古い情報だったりします。本稿では、インストールから簡単なルーティングまでの流れをご紹介します。

[追記: 2019/06/04] 公式ドキュメント「Plug」にもとづいて、本文を大幅に加筆し、テストのコードを加えました。


mixでElixirプロジェクトをつくる

まずは、mix newコマンドでElixirプロジェクトをつくりましょう。--supオプションを加えると、監視ツリーの含まれたOTPアプリケーションとしてPlugを起動できるひな形ができ上がります。つくられるファイルは以下のとおりです。

$ mix new my_plug --sup

* creating README.md

* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/my_plug.ex
* creating lib/my_plug/application.ex
* creating test
* creating test/test_helper.exs
* creating test/my_plug_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

cd my_plug
mix test

Run "mix help" for more commands.


PlugCowboyをインストールする

アプリケーションのディレクトリ(my_plug)に切り替えたら、mix.exsにつぎのようなPlugCowboyの依存関係を加えます。


mix.exs

defmodule MyPlug.MixProject do

# ...[中略]...

defp deps do
[
{:plug_cowboy, "~> 2.0"} # 追加
]
end
end


そして、mix deps.getコマンドで依存関係を解決してください。

$ mix deps.get

Resolving Hex dependencies...

Dependency resolution completed:
Unchanged:
cowboy 2.6.3
cowlib 2.7.3
mime 1.3.1
plug 1.8.2
plug_cowboy 2.0.2
plug_crypto 1.0.0
ranch 1.7.1


サンプルコードを動かす

mixプロジェクトのファイルlib/my_plug.exのモジュールMyPlugに、つぎのコード001のテスト用関数を定めます(「Hello world」参照)。


コード001■lib/my_plug.ex


lib/my_plug.ex

defmodule MyPlug do

import Plug.Conn

def init(options) do
# optionsの初期化
options
end

def call(conn, _opts) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello world")
end
end


そして、mixプロジェクトをiexのセッションで開いてください。

$ iex -S mix

Plugアプリケーションの中で前掲モジュールを動かすのがつぎのコードです。ターミナルからサーバーが起ち上がります。http://localhost:4000/のURLを開くと、ページに"Hello world"のテキストが示されるでしょう。

iex> {:ok, _} = Plug.Cowboy.http MyPlug, []

{:ok, #PID<0.593.0>}


図001■ローカルサーバーで開いたアプリケーションのページ

elixir_plug_009.png


Plug.Conn構造体

前掲コード001について、説明を加えましょう。Plugモジュールは、つぎのふたつの関数を実装しなければなりません。



  • init/1: 引数に受け取ったオプション(options)を初期化します。


  • call/2: 接続(conn)と初期化されたオプション(_opts)を受け取って、新たな接続にして返します(接続はイミュータブルです)。

データは接続から直に読み込まれ、パターンマッチングなどにより処理が加えられます。接続を表すのがPlug.Conn構造体(%Plug.Conn{})です。Plugモジュールは、データを処理するためにPlug.Connに定められた関数が使えます。前掲コード001では、put_resp_content_type/3send_resp/3が用いられました。

接続はサーバーとのインタフェースです。たとえば、send_resp/3は得られたステータスと本文を、ただちにクライアントに返します。


Plug.Routerを使う

ルーターはPlugです。Plug.Routerにより定められ、リクエストのパスとメソッドにもとづいてルーティングします。新たにlib/router.exとして、つぎのコード002のようなルーターモジュール(MyPlug.Router)をつくりまししょう(「Plug.Router」参照)。


コード002■lib/router.ex


lib/router.ex

defmodule MyPlug.Router do

use Plug.Router

plug :match
plug :dispatch

get "/hello" do
send_resp(conn, 200, "Japan\n")
end

match _ do
send_resp(conn, 404, "not found\n")
end
end


ルーターには独自のPlugパイプラインがあります。前掲コード002では、つぎのふたつのPlugを順に呼び出しました。



  • :match: マッチングするルートを探します。


  • :dispatch: マッチングしたコードを実行します。

ほかにもさまざまなPlugがあり、それぞれのPlugパイプラインをもちます。実行は記述した順です。たとえば、つぎのコードでは、ルートのマッチング前にPlug.Loggerが加わります。

plug Plug.Logger

plug :match
plug :dispatch

Plug.Routerは、すべてのルートをひとつの関数にコンパイルします。そして、Erlang VMにより、ルートの検索は、ツリー状に最適化されます。ルートを逐次マッチングする線型状ではありません。Plugのルーティングは極めて高速になるのです。各ルートはPlugの仕様にしたがって、接続を返さなければなりません。

モジュールの最後のmatchブロックは、いずれにもマッチングしなかったルートを拾います。これは関数のエラー(FunctionClauseError)を避けるためです。


ルーターを監視ツリーに加える

mix newコマンドに--supオプションを添えましたので、Plugパイプラインは監視ツリーのもとで動かせます。そのために呼び出すのが、child_spec関数です。lib/my_plug/application.exに、つぎのように監視する子プロセスを加えてください。


lib/my_plug/application.ex

defmodule MyPlug.Application do

def start(_type, _args) do
children = [
Plug.Cowboy.child_spec(scheme: :http, plug: MyPlug.Router, options: [port: 4001])
]

end
end


アプリケーションを起動するのはmix runコマンドです。そのままではすぐに終了してしまうので、--no-haltオプションで動作し続けるようにします。

$ mix run --no-halt

別に開いたコマンドラインツールから、リクエストを送りましょう。ここでは、curlを使います。

$ curl http://localhost:4001/hello

Japan
$ curl http://localhost:4001/oops
not found

これで、監視ツリーに加えたルーターにより、リクエストをルーティングすることができました(図002)。


図002■ルーティングされたページ

elixir_plug_010.png


Plugをテストする

Plugに備わっているテスト用のモジュールがPlug.Testです。このモジュールを使えば、Plugが簡単にテストできます。ひな形につくられているtest/my_plug_test.exsをつぎのコード003のように書き替えてください。


コード003■test/my_plug_test.exs


test/my_plug_test.exs

defmodule MyPlugTest do

use ExUnit.Case, async: true
use Plug.Test

@opts MyPlug.Router.init([])

test "hello Japanを返す" do
# テスト用の接続をつくる
conn = conn(:get, "/hello")

# Plugを起動する
conn = MyPlug.Router.call(conn, @opts)

# レスポンスとステータスを確認する
assert conn.state == :sent
assert conn.status == 200
assert conn.resp_body == "Japan\n"
end
end


mix testは正しく終了するはずです。

$ mix test

.

Finished in 0.03 seconds
1 test, 0 failures