Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
gumiTECH
gumi
Python、Erlang、Elixir などちょっと変わった技術でゲームをつくったりする会社。プログラマだけじゃなく、企画、デザイン、イラストなど開発全般揃ってます。
http://gu3.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした