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のテンプレート的な話
- Viewを生成するとき、以下の
Phoneix.HTML.safe_to_stringの関数を通して、:safe要素を持つTupleでViewのhtmlを表現したりしている。 - https://github.com/phoenixframework/phoenix_html/blob/master/lib/phoenix_html.ex#L156
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 とかしてて、いろいろ conn に Map.put して情報を付与していることがわかります。
つまるとこ、Guardianは conn の構造体にあるassignsやstateなんかのキーに値を追加していっているのですね。なるほど。ちなみに、 put_session は、 Plug.ConnのprivateのMapに独自の情報を追加していくのですね。
assign や put_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標準の機構である Plug や Ecto を使ってより使いやすくまとめた、というものが多いですね。言語とその周辺環境自体が、要素ごとに基礎となるパーツを用意することで、それに取り掛かる人たちがそこらへんを使えば大丈夫、という感じのエコシステムが強く働いているぽい印象を受けます。
Erlangの基礎の上にこういうLips形式でも実装できるmacroを積んだElixir、こういうの好きな人には美味しいのだろうなー。
1 Comment