スポンサーリンク

2016年9月19日

alchemist report 002

Goal

  • Plug+Cowboyのサンプルを作成する
  • Phoenixフレームワーク v0.1.1のRouter部分を最小実装する

Dev-Environment

  • OS: Windows8.1
  • Erlang: OTP19
  • Elixir: v1.3.0
    • Plug: v1.1
    • Cowboy: v1.0

Prerequisite 1: Erlang、Elixirはインストール済みであること

Prerequisite 2: 前回の記事

Context

Caution: 知っている人にとってはなんてことない記事のため、有識者の方はブラウザバック推奨!

Phoenixフレームワーク(v0.1.1)のもどきを実装してみようとする
(現在だとコードが違いすぎるので)誰の役にも立たないこの内容…今週も続けていきます。
今回は、Router部分を最小限(?)で実装してみようと思います。
直打ちにしている部分もあるので、あまり大きな期待はしないでください。
また、本記事を書いているときは体調不良につき、よくわからないテンションで書いています。
色々と熟考や推敲が足りないとは思いますので、あしからず…

Mapperモジュールを実装する

さて、Routerを最小限(?)で作ってみると言っておきながらいきなり別のモジュールから作り始めます。
このモジュールは、実際にRouterを実装したとき、ルーティングのDSLを提供しているモジュールになります。
(それ以外にもやっているのですが、まぁとりあえず大きい機能としてはそれなので…)
皆さんもPhoenixフレームワークを使うときに下記のような(get…の部分)、記述を見たことがあると思います。
それをマクロを使って実装しています。

Example:

defmodule RouterExample do
  use Router

  get "/example/show", ControllerExample, :show
end
何はともあれ作っていきましょう。
いつかどこかで掲載したことがある記事の内容に酷似しているのですが、それはそれ、これはこれで…

File: lib/mapper.ex

defmodule Mapper do
  defmacro __using__(_options) do
    quote do
      Module.register_attribute __MODULE__, :routes, accumulate: true,
                                                     persist: false
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(env) do
    routes = Enum.reverse(Module.get_attribute(env.module, :routes))
    routes_ast = Enum.reduce routes, nil, fn route, acc ->
      quote do
        defmatch(unquote(route))
        unquote(acc)
      end
    end

    quote do
      def __routes__ do
        Enum.reverse(@routes)
      end

      unquote(routes_ast)

      def match(conn, method, path) do
        Controller.not_found(conn, method, path)
      end
    end
  end

  defmacro defmatch({http_method, path, controller, action, _options}) do
    quote do
      def unquote(:match)(conn, unquote(http_method), unquote(path)) do
        apply(unquote(controller), unquote(action), [conn])
      end
    end
  end

  defmacro get(path, controller, action, options \\ []) do
    add_route(:get, path, controller, action, options)
  end

  defp add_route(verb, path, controller, action, options) do
    quote bind_quoted: [verb: verb,
                        path: path,
                        controller: controller,
                        action: action,
                        options: options] do

      @routes {verb, path, controller, action, options}
    end
  end
end
HTTPメソッドのget以外は実装していません。(現状の作りだと色々と使えないため)
このモジュールの動きとしては、get/4マクロを使って記述を作ると、add_route/5が呼ばれ、
モジュールアトリビュートへ値をスタックしています。
モジュールアトリビュートへスタックした値は、
before_compile/1で取得され、match/3関数を定義するために使っています。
まぁ直書きでもよかったのですが…あえてマクロ使いました。
一応、直書きしたソースもあるので掲載します。
(下記のソースだとget/4マクロは使えないので削除する必要があります)

Example:

defmodule Mapper do
  defmacro __using__(_options) do
    quote do
      Module.register_attribute __MODULE__, :routes, accumulate: true,
                                                     persist: false
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def match(conn, :get, "/example/show") do
        apply(ControllerExample, :show, [conn])
      end

      def match(conn, method, path) do
        Controller.not_found(conn, method, path)
      end
    end
  end
end

Routerモジュールを実装する

さて、いよいよRouterを実装しましょう。
といっても、先ほどのMapperモジュールより難しくありません。

File: lib/router.ex

defmodule Router do
  defmacro __using__(plug_adapter_options \\ []) do
    quote do
      use Mapper
      @before_compile unquote(__MODULE__)
      use Plug.Builder

      @options unquote(plug_adapter_options)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      plug :dispatch

      def dispatch(conn, _opts \\ []) do
        Router.perform_dispatch(conn, __MODULE__)
      end

      def start do
        IO.puts ">> Running #{__MODULE__} with Cowboy"
        Plug.Adapters.Cowboy.http __MODULE__, []
      end
    end
  end

  def perform_dispatch(conn, router) do
    fetch_query_parames = Plug.Conn.fetch_query_params(conn)
    http_method         = fetch_query_parames.method |> String.downcase |> :erlang.binary_to_atom(:utf8)
    split_path          = fetch_query_parames.path_info |> join

    apply(router, :match, [fetch_query_parames, http_method, split_path])
  end

  def join([]) do
    ""
  end
  def join(split_path) do
    Path.join(split_path)
  end
end
何のことはない、Plug.Builder使ってファンクションPlugを呼び出している。
呼び出している関数でMapperモジュールで作っている、それぞれのルートに対応したmatch/3関数を呼び出している。

Routerを展開してみる

Routerモジュールを作ったので、これを展開するモジュールを作ります。
(ここまできたらほぼ蛇足…)

File: lib/router_example.ex

defmodule RouterExample do
  use Router

  get "example/show", ControllerExample, :show
  get "example/show2", ControllerExample, :show2
end
コントローラを展開しているモジュールも変更します。

File: lib/controller_example.ex

defmodule ControllerExample do
  use Controller

  def show(conn) do
    text conn, "Hello World!!"
  end

  def show2(conn) do
    text conn, "show2!!"
  end
end
おっと忘れていました。
Controllerモジュールの修正をしなければ…(順番が前後してしまい申し訳ない)

File: lib/controller.ex

defmodule Controller do
  import Plug.Conn

  defmacro __using__(_options) do
    quote do
      import Plug.Conn
      import unquote(__MODULE__)
    end
  end

  def text(conn, text) do
    text(conn, 200, text)
  end
  def text(conn, status, text) do
    send_response(conn, status, "text/plain", text)
  end

  def not_found(conn, method, path) do
    text conn, 404, "No route matches #{method} to #{inspect path}"
  end

  def send_response(conn, status, content_type, data) do
   conn
   |> put_resp_content_type(content_type)
   |> send_resp(status, data)
  end
end
実行してみましょう!

Example:

iex> RouterExample.start
上記を実行後、”http://localhost:4000/example/show“もしくは、
http://localhost:4000/example/show2“にアクセスしてみてください。
それぞれの関数で設定しているプレーンテキストが表示されます。

終わりに

勘違いしないようにしていただきたいのが、
あくまで擬似的に再現しているだけのもどきなので、実際には上記のソースコードより色々な処理をやっている。(当然)
ただ、まぁざっくりな流れは掴んでもらえるのではないかと思う。
現状では、パラメータの受け渡しなんかはできない作りになっています。
ここら辺を解決するためにMapperモジュールでもう少し色々やってあげる必要があります。
Phoenix.Router.Pathとかね…(このモジュール意外と重要なことやってたりする)
もっと色々な説明を書かないと足りないと思いますが、体調悪いので今日はもうここまで…(無理ぽ)

Bibliography

人気の投稿