ElixirでPhoenix以外のWEBフレームワークを使ってみる(Webmachine編)

More than 1 year has passed since last update.

この記事は Elixir Advent Calendar 2016 - Qiita の 19 日目の記事です。

Elixir歴3ヶ月ほどの新人です。まだまだ分かっていませんが、Elixir楽しいです。

今回はPhoenix以外のWEBフレームワークについて調べてみました。
Elixirで何らかのWEBアプリケーションを作る場合はPhoenixを選ぶのが普通だと思います。
Railsと似ていて入門しやすく、情報量からいっても妥当な選択だと思うのですが、
他の選択肢もあるだろうということで、今回はWebmachineを少し調べてみました。

Webmachineとは

WebmachineはRiakで有名なBashoが発祥のerlangで書かれたソフトウェアです。
実際にはWEBフレームワークではなくREST Toolkitと称している通り、
フレームワークとしてみると機能が足りない点が多いので、
足りないものは自分で他のライブラリを使うなりして補っていくスタイルです。

特徴的な点としては、Resourceを定義したのちに、その中でひたすら関数で設定などを書いていく点です。
Phoenixと比べるとより関数型らしいと言えるかもしれません。

アプリケーションを書いてみる

まずはともかく、サンプルアプリケーションを書いてみましょう。

create project

mixで新しいプロジェクトを作ります。

$ mix new elixir_webmachine_sample --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/elixir_webmachine_sample.ex
* creating test
* creating test/test_helper.exs
* creating test/elixir_webmachine_sample_test.exs

$ cd elixir_webmachine_sample

mix.exsにwebmachineの依存を追加

Webmachineの依存を追加します。

mix.exs

defmodule ElixirWebmachineSample.Mixfile do
  use Mix.Project

  def project do
    [app: :elixir_webmachine_sample,
     version: "0.1.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  def application do
    [applications: [:logger, :webmachine], # 追加
     mod: {ElixirWebmachineSample, []}]
  end

  defp deps do
    [{:webmachine,
      git: "https://github.com/webmachine/webmachine.git",
      branch: "master"}] # 追加
  end
end

ライブラリの取得をしておきます。

$ mix do deps.get, compile

サーバを子プロセスとして起動させる

Webmachineを起動する設定を書いていきます。

lib/elixir_webmachine_sample.ex

defmodule ElixirWebmachineSample do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Webmachine用設定
     web_config = [ip: {127,0,0,1},
                  port: 8080,
                  dispatch: []]

    # 子プロセスとしてサーバをスタートさせる
    children = [
      worker(:webmachine_mochiweb,
        [web_config],
        function: :start,
        modules: [:mochiweb_socket_server])
    ]

    opts = [strategy: :one_for_one, name: ElixirWebmachineSample.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

これを書いたら、以下でサーバを起動させてみます。

$ mix run --no-halt

http://127.0.0.1:8080/にアクセスしてみます。

webmachine_404.png

NotFoundですがサーバ自体は動いていそうです。
サーバはCtrl-Cなりで一旦終了させておきましょう。

リソースの追加

WebmachineではRESTでいうところのリソース単位でファイルを作成し、処理を書いていきます。

まずはリソースファイルを保存するためのディレクトリを作成します。

$ mkdir -p lib/elixir_webmachine_sample/resources

リソースの作成

ひとつリソースを作ってみます。あまりリソースっぽくないですが、helloとします。

lib/elixir_webmachine_sample/resources/hello.ex

defmodule ElixirWebmachineSample.Hello do
  # 初期化作業
  def init(_) do
    {:ok, nil}
  end

  # デフォルトではto_htmlという関数が呼ばれるようになっているので、実装しておく。
  def to_html(req_data, state) do
    {"<html><body>Hello, World!</body></html>", req_data, state}
  end
end

サーバにリソースを追加する

lib/elixir_webmachine_sample.ex

    # Webmachine用設定
    web_config = [ip: {127,0,0,1},
                  port: 8080,
                  dispatch: [
                    {[], ElixirWebmachineSample.Hello, []}
                  ]]

さきほどと同様にサーバを起動させ、アクセスしてみます。

roor_hello.png

表示されました!

JSON APIを作ってみる

これだけだとあまり面白みがないので、JSONを返すAPIっぽいものも作ってみましょう。
Userリソースを作成し、Acceptヘッダの値によってhtmlかjsonのどちらかを返すようにします。

config(dispatch)変更

dispatchに新しいリソースを追加しておきます。
先頭に['user', :user_id]とありますがこれはルーティングの設定で、
この定義で「/user/1」でアクセスするとUserリソースが選ばれ、
のちに専用のメソッドで:user_idを指定することでこの例だと1を取得することができます。
ここの指定方法はなかなか独特ですが、詳しくはこちらを参考にしてください。

さらに注意点なのですが、文字列の設定を行う場合は、
string(「"」で囲んだもの)ではなくChar List(「'」で囲んだもの)で設定してください。
古めのerlangのライブラリではstring(binary)が使えないらしく、Char Listを使うようです。
常識なのかもしれませんが自分は気付かず、エラーになるでもなく、
しかし意図した動作にならなくてハマりました。

lib/elixir_webmachine_sample.ex

    # Webmachine用設定
    web_config = [
      ip: {127,0,0,1},
      port: 8080,
      dispatch: [
        {[], ElixirWebmachineSample.Hello, []},
        {['user', :user_id], ElixirWebmachineSample.User, []} # <-ここ!
      ]
    ]

リソース追加

Userリソースを新規作成します。
(ところどころデバッグ用の出力をいれてあります)

lib/elixir_webmachine_sample/resources/user.ex

defmodule ElixirWebmachineSample.User do
  def init(_) do
    {:ok, nil}
  end

  # 使用可能なHTTP Methodを指定する
  def allowed_methods(req_data, state) do
    # ここではGETのみ許可。atomで指定すること。
    # 他のメソッドでアクセスされるとMethod Not Allowedが起きる。
    {[:GET], req_data, state}
  end

  # Content TypeをWebmachineに伝えるためのcallback
  def content_types_provided(req_data, state) do
    # Acceptヘッダの内容により後続の処理を分離できる。
    types = [
      {'application/json', :to_json},
      {'text/html',        :to_html}
    ]
    {types, req_data, state}
  end

  # jsonを返す場合のメソッド
  def to_json(req_data, state) do
    req_data |> inspect |> IO.puts

    # HTTPメソッドはこれで取得できる。
    # 今回はGETのみ許可なので意味はないが、複数使用可能な場合はこの値で処理を分岐させる。
    method = :wrq.method(req_data)
    IO.puts "HTTP Metod:#{method}"

    # :wrq.path_infoを使うことでpathに埋め込まれた値を取得できる。
    # 取得した値もstringではなくChar Listなので注意すること!
    user_id = :wrq.path_info(:user_id, req_data)
    case get_user(user_id) do
      nil ->
        # {:halt, レスポンスコード}を戻り値にするとエラーを返せる。
        {{:halt, 404}, req_data, state}
      user ->
        {:ok, content} = Poison.encode(user)
        {content, req_data, state}
    end
  end

  # htmlを返す場合のメソッド。こちらはエラー処理などは手抜き。
  def to_html(req_data, state) do
    req_data |> inspect |> IO.puts
    user_id = :wrq.path_info(:user_id, req_data)
    user = get_user(user_id)
    {"<html><body>Hello, #{user.name} </br>(text/html)</body></html>", req_data, state}
  end

  defp get_user(user_id) do
    IO.puts user_id
    users = %{
      '1' => %{user_id: 1, name: "TARO TANAKA"},
      '2' => %{user_id: 2, name: "JIRO SATO"}
    }
    Map.get(users, user_id)
  end
end

ここでの肝はWebmachineで規定されたcallback用関数群(init, allowed_methods, content_types_provided)です。
決められた関数に必要な設定をしておくことで自動で読み込まれるわけです。
これらの関数は認証されているかどうかのチェック用など他にもいろいろあります。
Resource Functions · webmachine/webmachine Wiki

実行

サーバを起動し、さっそく試してみましょう。
ライブリロードなんて機能は無さそうなので、再起動してください。

json

$ curl -i -X GET -H 'Accept: application/json' http://localhost:8080/user/1
HTTP/1.1 200 OK
Vary: Accept
Server: MochiWeb/2.12.2 WebMachine/1.10.8-49-g6c42658 (cafe not found)
Date: Sat, 17 Dec 2016 19:29:33 GMT
Content-Type: application/json
Content-Length: 34

{"user_id":1,"name":"TARO TANAKA"}

html

$ curl -i -X GET -H 'Accept: text/html' http://localhost:8080/user/1
HTTP/1.1 200 OK
Vary: Accept
Server: MochiWeb/2.12.2 WebMachine/1.10.8-49-g6c42658 (cafe not found)
Date: Sat, 17 Dec 2016 19:29:58 GMT
Content-Type: text/html
Content-Length: 61

<html><body>Hello, TARO TANAKA </br>(text/html)</body></html>

意図通り動いているようです。

感想

少し触っただけでしかありませんが、Phoenixと比べるとかなりシンプルであり、
マクロもなく関数だけでなんでも行う感じが、こういうのでいいんだよ感と言いますか
個人的にはかなり好みです。
一方で複雑なアプリをつくる場合は機能不足感は否めないので、
採用するなら単純なつくりのものが向いているでしょう。
以前Goのバッチで統計を取得するWebAPIをつくってみたことがあり、
Elixirで同じようなことをしたいと思っていたのですが、
軽量なのでこういう用途にも使えそうな気がしています。
(今回そこまでやってみたかったけど力尽きたのでいずれ・・・)

参考

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.