どうも、靖宗です。
あまりにもPlugPlug言われるのでPlugのドキュメントを一通り読んでおきます。
Plugとは
✌('ω')。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
の箇所ですが、ここはこれ以降の所に書いてあるget
やmatch
のマクロで展開されるどの関数を呼び出すかをリクエストが来たら選定します。
マクロだらけで分かりにくくなっていますが、おそらく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をきっちり理解したわけではないですが、おおまかな概要はつかめてきた気がします。