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の依存関係を加えます。
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
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■ローカルサーバーで開いたアプリケーションのページ
Plug.Conn
構造体
前掲コード001について、説明を加えましょう。Plugモジュールは、つぎのふたつの関数を実装しなければなりません。
-
init/1
: 引数に受け取ったオプション(options
)を初期化します。 -
call/2
: 接続(conn
)と初期化されたオプション(_opts
)を受け取って、新たな接続にして返します(接続はイミュータブルです)。
データは接続から直に読み込まれ、パターンマッチングなどにより処理が加えられます。接続を表すのがPlug.Conn
構造体(%Plug.Conn{}
)です。Plugモジュールは、データを処理するためにPlug.Conn
に定められた関数が使えます。前掲コード001では、put_resp_content_type/3
とsend_resp/3
が用いられました。
接続はサーバーとのインタフェースです。たとえば、send_resp/3
は得られたステータスと本文を、ただちにクライアントに返します。
Plug.Router
を使う
ルーターはPlugです。Plug.Router
により定められ、リクエストのパスとメソッドにもとづいてルーティングします。新たにlib/router.ex
として、つぎのコード002のようなルーターモジュール(MyPlug.Router
)をつくりまししょう(「Plug.Router
」参照)。
コード002■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
に、つぎのように監視する子プロセスを加えてください。
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■ルーティングされたページ
Plugをテストする
Plugに備わっているテスト用のモジュールがPlug.Test
です。このモジュールを使えば、Plugが簡単にテストできます。ひな形につくられているtest/my_plug_test.exs
をつぎのコード003のように書き替えてください。
コード003■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