技術メモ

プログラミングとか電子工作とか

Phoenix入門 (番外 Plug)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
あまりにもPlugPlug言われるのでPlugのドキュメントを一通り読んでおきます。

hexdocs.pm

Plugとは

  1. Webアプリケーション間の構成可能なモジュールのための仕様
  2. Erlang VMのウェブサーバー間の接続アダプター

✌('ω')。o(????????????)
説明だけでは全く分かりません。
とりあえずドキュメントを読み進めます。

Hello world

プログラミングはコードが全てです。とりあえずサンプルを動かしていきます。
一応プロジェクトとして作成します。mix new HelloPlugで作成しました。

PS > mix new hello_plug
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/hello_plug.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_plug_test.exs

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

    cd hello_plug
    mix test

Run "mix help" for more commands.
PS > cd .\hello_plug\

Plugを使用するのでmix.exsの依存関係にPlugを追加。

...
  defp deps do
    [
      {:plug_cowboy, "~> 2.0"}
    ]
  end
...

mixで生成されたlib/hello_plug.exを編集。

defmodule HelloPlug do
  import Plug.Conn

  def init(options) do
    # initialize options

    options
  end

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

依存関係を取得してコンパイル

PS > mix deps.get
Resolving Hex dependencies...
...

PS > mix compile
...
Generated hello_plug app

iex -S mixでiexを起動して試してみる

iex(1)> c "lib/hello_plug.ex"
warning: redefining module HelloPlug (current version loaded from _build/dev/lib/hello_plug/ebin/Elixir.HelloPlug.beam)
  lib/hello_plug.ex:1

[HelloPlug]
iex(2)> {:ok, _} = Plug.Cowboy.http HelloPlug, []
{:ok, #PID<0.203.0>}

これでhttp://localhost:4000/にアクセスするとHello worldとだけ書かれたページが表示される。
とりあえず世界に挨拶はできたがまだイマイチ分かりません。
HelloPlugモジュールのcall関数がリクエストが来たときに呼び出され、connという接続情報を受け取ってレスポンスデータを送っていくというような流れでしょうか?

The Plug.Conn struct

Plugには関数PlugとモジュールPlugの2通りあるそうです。
関数Plugはコネクションとオプションを受けてデータを加工してコネクションを返す挙動だそうです。

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

モジュールPlugは初期化関数がついた関数Plugみたいなものです。おそらく最初の初期化のタイミングでなにがしかの設定を記憶し、コネクションから反応がある度にcallが呼び出される仕組みでしょう。

Plug.Connの構造、すなわち関数に渡されるconnは下記のようになっているそうです。

%Plug.Conn{host: "www.example.com",
           path_info: ["bar", "baz"],
           ...}

つまるところリクエストの情報が詰まったマップが渡されてくるような感じでしょうか。
put_resp_content_typeでレスポンスの形式の情報を追加し、send_respでレスポンスを送信するようです。

Plug.Router

Plug自体にルーティング機能があるようです。おそらくPhoenixはこの機能を使っている?
とりあえず見ていきます。

defmodule MyRouter do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/hello" do
    send_resp(conn, 200, "world")
  end

  forward "/users", to: UsersRouter

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

まず

  plug :match

の箇所ですが、ここはこれ以降の所に書いてあるgetmatchのマクロで展開されるどの関数を呼び出すかをリクエストが来たら選定します。
マクロだらけで分かりにくくなっていますが、おそらくplug :match周辺より下はマクロが展開されたときに関数の定義が並ぶことになるんでしょう。
その次に

  plug :dispatch

:matchで選定した関数を実行します。

Supervised handlers

Elixirでウェブサーバーを実行するということは、当然Superviser tree下で管理したいところです(再起動などを良い感じにやってくれるので)。
その場合はPlugに提供されるchild_spec/3でSupervisorのchildrenに追加すれば良いようです。
以前はSupervisorのchild_specで追加しましたが、今回はPlugのchild_specでchildrenに追加しているようです。

mix new my_app --sup

でプロジェクトを作成したと仮定すると、lib/my_app/application.exを下記のように変更します。

defmodule MyApp do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      Plug.Cowboy.child_spec(scheme: :http, plug: MyRouter, options: [port: 4001])
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

特に連携もしてないのでstrategyは:one_for_oneになってますが、ココは適宜変更って感じだと思います。

Testing plugs

Plug用のテストも色々用意されているようです。
ExUnit.Caseをuseした後にPlug.Testをuseするようです。

defmodule MyPlugTest do
  use ExUnit.Case, async: true
  use Plug.Test

  @opts MyRouter.init([])

  test "returns hello world" do
    # Create a test connection
    conn = conn(:get, "/hello")

    # Invoke the plug
    conn = MyRouter.call(conn, @opts)

    # Assert the response and status
    assert conn.state == :sent
    assert conn.status == 200
    assert conn.resp_body == "world"
  end
end

おそらくconn/2という関数が追加されているのだと思います。
適当な接続情報がconn/2で返ってきて、それを使ってPlugのテストをしていく想定でしょう。
assertあたりで自分の想定する挙動を書けばよさそうです。

まだこれでPlugをきっちり理解したわけではないですが、おおまかな概要はつかめてきた気がします。