『Programming Phoenix』を読んでみた(まだドラフト版)

ElixirによるRoR実装と表現される、Phoenix Frameworkの作者による書籍『Programming Phoenix』を読んでみました。今はドラフト版ですが、こういうフレームワークの基本的な思想とか好きなのと、PhoenixはElixirのmacroを駆使しているという話をみて、気になって買って読んでみました。(Elixir、macroがほぼそのままLips形式の記述なのですが、そこも著者は気に入っているそう。)

以下ではElixir 1.1.0、Phoenix 1.0.3を対象としてます。

内容としては、簡単なサンプルを作りながらPhoenixの話、Ecto、Plug.Conn、Ecto.QueryなどのElixirの基礎要素の話がありました。テストの話なんかはまだ書きかけ。Phoenixの話ばかりと思ってたら、結構Elixirの周辺の機構の主要な要素をかいつまんでいて、良い意味でびっくりでした。

個人的には、

https://github.com/KazuCocoa/web_qa_vote

で作っていたことがこの書籍読んだあとだったらもっとサクッとできたのかな、という感じでした。ある程度Elixirの文法わかって、簡単なWebアプリ作ったことがある人が、中身を理解するという段階で有用な書籍っぽい。

技術的なところは置いておいて、個人的に面白かったのは、所々でてくる、著者らによるコラムです。なんでこうしたか?というところに、どのような意図があってこうした、というのが散らばってます。

例えば、Phoenixの内部処理ではAtom keyを使うけれど、外部(開発者)はString keyでパラメータを書く。これは、データの安全性への考慮を含めているから、というような。Phonenixの web ディレクトリと lib ディレクトリの役割の違いなど。あと、HTMLのtemplateの処理がPhoenixではなぜ早いか?というところに対しては、Phonenixは、templateをStringではなくlinked listsとして扱っているから、そのぶん性能的な改善が見られる、らしい。

以下、書籍のまとめとかではないけれど、個人的に派生して読んだり学んだことのメモ。


Phoenixのテンプレート的な話

Dive to logic of authorization

認証機構の実装の話も扱っていたので、ついでにGuardianの中身も覗いてみました。結構似たことしているので、初見では読み悩んでいた箇所もスラスラ読めるようになっててびっくり。

例えば、sign_inした状態のsessionを作り出すときの話。Guardianを使う場合、以下のような処理を書いたりします。

  def create(conn, params = %{}) do
    user = Repo.one(UserQuery.by_email(params["user"]["email"] || ""))
    if user do
      changeset = User.login_changeset(user, params["user"])
      if changeset.valid? do
        conn
        |> put_flash(:info, "Logged in.")
        |> Guardian.Plug.sign_in(user, :token, perms: %{ default: Guardian.Permissions.max })
        |> redirect(to: user_path(conn, :index))
      else
        render(conn, "new.html", changeset: changeset)
      end
    else
      changeset = User.login_changeset(%User{}) |> Ecto.Changeset.add_error(:login, "not found")
      render(conn, "new.html", changeset: changeset)
    end
  end

この中で、 Guardian.Plug.sign_in/4 を覗いてみると以下のような処理をしています。

  • guardian/lib/guardian/plug.ex
@spec sign_in(Plug.Conn.t, any, atom | String.t, Map) :: Plug.Conn.t
def sign_in(conn, object, type, claims) do
  the_key = Dict.get(claims, :key, :default)
  claims = Dict.delete(claims, :key)

  case Guardian.encode_and_sign(object, type, claims) do
    { :ok, jwt, full_claims } ->
      conn
      |> Plug.Conn.put_session(base_key(the_key), jwt)
      |> set_current_resource(object, the_key)
      |> set_claims(full_claims, the_key)
      |> set_current_token(jwt, the_key)
      |> Guardian.hooks_module.after_sign_in(the_key)

    { :error, reason } -> Plug.Conn.put_session(conn, base_key(the_key), { :error, reason }) # TODO: handle this failure
  end
end

この中でパイプでつながっている set_* 系のものを見ていると、 Plug.Conn.* のPlugの機構の中で put_session とかしてて、いろいろ connMap.put して情報を付与していることがわかります。

つまるとこ、Guardianは conn の構造体にあるassignsやstateなんかのキーに値を追加していっているのですね。なるほど。ちなみに、 put_session は、 Plug.ConnのprivateのMapに独自の情報を追加していくのですね。

assignput_session をみてみると以下な感じ。

  • plug/lib/plug/conn.ex
  @spec assign(t, atom, term) :: t
  def assign(%Conn{assigns: assigns} = conn, key, value) when is_atom(key) do
    %{conn | assigns: Map.put(assigns, key, value)}
  end

  @spec put_session(t, String.t | atom, any) :: t
  def put_session(%Conn{state: state}, _key, _value) when not state in @unsent,
    do: raise AlreadySentError
  def put_session(conn, key, value) do
    put_session(conn, &Map.put(&1, session_key(key), value))
  end

  ...

  defp put_session(conn, fun) do
    private = conn.private
              |> Map.put(:plug_session, get_session(conn) |> fun.())
              |> Map.put_new(:plug_session_info, :write)

    %{conn | private: private}
  end


ちなみに、Plug.Connの構造は以下ですね。

%Plug.Conn{
  adapter: {Plug.Conn, :...},
  assigns: %{},
  before_send: [],
  body_params: %Plug.Conn.Unfetched{aspect: :body_params},
  cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  halted: false,
  host: "www.example.com",
  method: "GET",
  owner: nil,
  params: %Plug.Conn.Unfetched{aspect: :params},
  path_info: [],
  peer: nil,
  port: 0,
  private: %{},
  query_params: %Plug.Conn.Unfetched{aspect: :query_params},
  query_string: "",
  remote_ip: nil,
  req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  req_headers: [],
  request_path: "",
  resp_body: nil,
  resp_cookies: %{},
  resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
  scheme: :http,
  script_name: [],
  secret_key_base: nil,
  state: :unset,
  status: nil
}

Query API

Ectoの話にも結構言及していたので、その中でEcto.QuaryのAPIをメモ。

    * Comparison operators: `==`, `!=`, `=`, ``
    * Boolean operators: `and`, `or`, `not`
    * Inclusion operator: `in/2`
    * Search functions: `like/2` and `ilike/2`
    * Null check functions: `is_nil/1`
    * Aggregates: `count/1`, `avg/1`, `sum/1`, `min/1`, `max/1`
    * Date/time intervals: `datetime_add/3`, `date_add/3`
    * General: `fragment/1`, `field/2` and `type/2`

Ectoでは、上記のAPIのうち fragment/1 を使うことで、String baseのクエリを独自で拡張して発行できるようになってます。

こんな感じ。

from p in Post,
  where: fragment(title: ["$eq": ^some_value])

締め

いろいろなところでPhoenixや他のライブラリやフレームワークを覗いてみると、Elixir標準の機構である PlugEcto を使ってより使いやすくまとめた、というものが多いですね。言語とその周辺環境自体が、要素ごとに基礎となるパーツを用意することで、それに取り掛かる人たちがそこらへんを使えば大丈夫、という感じのエコシステムが強く働いているぽい印象を受けます。

Erlangの基礎の上にこういうLips形式でも実装できるmacroを積んだElixir、こういうの好きな人には美味しいのだろうなー。

『Programming Phoenix』を読んでみた(まだドラフト版)」への1件のフィードバック

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中