スポンサーリンク

2015年8月31日

[Elixir]自作マクロでdo~end記述をできるようにする

とある錬金術師の万能薬(Elixir)

Goal

自作マクロでdo~end記述を可能にする。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4

Wait a minute

ふと思ったんです。
自分で定義するマクロで、
do~endの記述をできるようにするには・・・どうしたらいいのか?
以下のような記述がしたい。
macro_name do
  関数や処理
end

Index

do ~ end in your own macro
|> Create example
|> Let’s run!
|> Extra

Create example

まずは簡単な例を作成します。
どうやってやろうかと思いましたが・・・
if文でやってんだから、
if/2マクロをコピペして真似ればできるんじゃね?

Example:

defmodule Sample do
  defmacro hoge(clauses) do
    do_clause = Keyword.get(clauses, :do, nil)

    quote do
      IO.puts unquote(do_clause)
    end
  end
end
if/2マクロのdoに該当する部分だけコピペしてきた。
ただ、do部分を受け取って、内容を表示するだけのマクロ。

Let’s run!

それでは使ってみます。

Result:

iex> require Sample
nil
iex> Sample.hoge do
...>   "do block"
...> end
do block
:ok
やった!
成功です。

Extra

do以外にもできるのかなと思いやってみました。
(無理だと思いますが・・・一応)

Example:

defmodule Sample do
  defmacro hoge(clauses) do
    foo_clause = Keyword.get(clauses, :foo, nil)
    bar_clause = Keyword.get(clauses, :bar, nil)

    quote do
      IO.puts unquote(foo_clause)
      IO.puts unquote(bar_clause)
    end
  end
end

Result:

iex> Sample.hoge foo
** (FunctionClauseError) no function clause matching in Keyword.get/3
    (elixir) lib/keyword.ex:118: Keyword.get({:foo, [line: 4], nil}, :foo, nil)
             expanding macro: Sample.hoge/1
             iex:4: (file)
iex> Sample.hoge foo: "foo", bar: "bar"
foo
bar
:ok
当然ですが、できませんね。

Speaking to oneself

大した内容ではなかったですよね・・・
if、unlessをやった時に検証しておけば良かったのですが、
その時は意識が向いていませんでした。
なので、今回実施することとなりました。
何にせよ、Elixirすげぇな!!(信仰)

Bibliography

[Rails Tutorial for Phoenix]Upgrade version of Phoenix-Framework

Goal

Phoenix-Frameworkをv1.0.0にバージョンアップします。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5
Phoenix Framework: v0.13.1 → v1.0.0
PostgreSQL: postgres (PostgreSQL) 9.4.4
Safetybox: v0.1.2
Scrivener: v0.11.0

Wait a minute

Phoenix Tutorialのプロジェクトで使っている、
Phoenix-Frameworkのバージョンをv0.13.1からv1.0.0へバージョンアップします。
リリースノート調べてたけど・・・数多いな(笑)
まぁ、バージョンアップを放置していた自分の責任だな。
ここらで一気に解消します。

Index

Rails Tutorial for Phoenix
|> Preparation
|> Version-up
|> Reflection of the version-up content
|> Extra

Preparation

問題があっても、戻せるようにブランチを切っておきます。
>git checkout -b version_up
Switched to a new branch 'version_up'

>git branch
...
* version_up
準備良し!

Version-up

それでは、バージョンアップしましょう。
一気にバージョンアップをします。
そのためプロジェクトを新しく作成し、ソースコードを移行し、
ソースコードに必要なだけの変更をします。
>cd path/to/new/project

>mix phoenix.new sample_app
...

>mix ecto.create
...

>mix phoenix.server

Caution:

sample_appのDBは予め削除しておいて下さい。
同名プロジェクトで作成するとDB名が衝突します。
バージョンアップ自体は、
新しくプロジェクトを作成するだけで完了です。

Reflection of the version-up content

バージョンアップに伴う必要な変更を実施します。

ソースコードの移行を行う

移行対象のソースコード一覧。

マイグレーションファイル

  • priv/repo/migrations/[timestamp]_create_user.exs
  • priv/repo/migrations/[timestamp]_add_password_to_users.exs
  • priv/repo/migrations/[timestamp]_create_micropost.exs
  • priv/repo/migrations/[timestamp]_create_relationship.exs

静的ファイル

  • priv/static/css/custom.css

lib内に作成したファイル / ディレクトリ

  • lib/helpers (ディレクトリ)
  • lib/helpers/pagination_helper.ex
  • lib/helpers/validate_helper.ex
  • lib/helpers/view_helper.ex
  • lib/plugs (ディレクトリ)
  • lib/plugs/check_authentication.ex
  • lib/plugs/signed_in_user.ex
  • lib/authentication.ex
  • lib/encryption.ex
  • lib/gravator.ex
  • lib/sign_in.ex

Webサイト用のソースコード

Caution:

phoenix.newで生成されたソースコードは移行しません。

コントローラ

  • web/controllers/micropost_controller.ex
  • web/controllers/relationship_controller.ex
  • web/controllers/sesion_controller.ex
  • web/controllers/static_pages_controller.ex
  • web/controllers/user_controller.ex

モデル

  • web/models/micropost.ex
  • web/models/relationship.ex
  • web/models/user.ex

ビュー

  • web/views/pagination_view.ex
  • web/views/session_view.ex
  • web/views/shared_view.ex
  • web/views/static_pages_view.ex
  • web/views/user_view.ex

テンプレート

  • web/templates/layout/footer.html.eex
  • web/templates/layout/header.html.eex
  • web/templates/layout/shim.html.eex
  • web/templates/pagination/pagination.html.eex
  • web/templates/session/signin_form.html.eex
  • web/templates/shared/*.eex
  • web/templates/static_pages/*.eex
  • web/templates/user/*.eex

ソースコードへ同様の設定と変更の反映を行う

ファイル: mix.exs

利用していたライブラリを追加します。
defp deps do
  [...
   {:safetybox, "~> 0.1"},
   {:scrivener, "~> 1.0.0"}]
end
依存関係の解決を行います。
>mix deps.get

ファイル: web/router.ex

ルーティングの設定をします。
scope "/", SampleApp do
  pipe_through :browser # Use the default browser stack

  get "/", PageController, :index
  get "/signup", UserController, :new
  get "/home", StaticPagesController, :home
  get "/help", StaticPagesController, :help
  get "/about", StaticPagesController, :about
  get "/contact", StaticPagesController, :contact
  resources "/user", UserController, except: [:new]
  get "user/:id/following", UserController, :following
  get "user/:id/followers", UserController, :followers
  get "/signin", SessionController, :new
  post "/session", SessionController, :create
  get "/signout", SessionController, :delete
  resources "/post", MicropostController, only: [:create, :delete]
  resources "/relationship", RelationshipController, only: [:create, :delete]
end

ファイル: lib/sample_app/repo.ex

Scrivenerのuseを追加します。
defmodule SampleApp.Repo do
  use Ecto.Repo, otp_app: :sample_app
  use Scrivener, page_size: 10
end

ファイル: web/templates/layout/app.html

以下の通り編集しました。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Sample App!!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/custom.css") %>">

    <%= render "shim.html" %>
  </head>

  <body>
    <%= render "header.html", conn: @conn %>

    <div class="container">
      <%= @inner %>
    </div>

    <%= render "footer.html", conn: @conn %>

    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

ファイル: web/models/user.ex

一意性の検証を行う関数が変わったようなので修正します。
def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    ...
    |> unique_constraint(:name)
    |> unique_constraint(:email)
    ...
  end
Description:
一意性の検証をするための関数が変わったようです。
ドキュメント: hexdocs - v1.0.1 Ecto - Changeset - unique_constraint/3
ドキュメントを読んでみると、
インデックスの作成が必要と記述されています。

ファイル: priv/repo/migration/[timestamp]_create_user.exs

インデックスを追加します。
defmodule SampleApp.Repo.Migrations.CreateUser do
  use Ecto.Migration
  @disable_ddl_transaction true

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string

      timestamps
    end

    create index(:users, [:name], unique: true, concurrently: true)
    create index(:users, [:email], unique: true, concurrently: true)
  end
end
マイグレーションを行います。
>mix ecto.migrate
...

コントローラの以下の記述を削除

plug :action

ファイル: web/controllers/page_controller.ex

以下の記述を追加します。
plug SampleApp.Plugs.CheckAuthentication
後は、コンパイルと起動を実行してみて下さい。
(一通りの機能が使えたことは確認しています)
後は、ブランチをマージして削除したら終わりです。
>git checkout master
>git merge version_up
>git branch -d version_up

Extra

ちょっと修正するの忘れていた部分がありました。

ファイル: web/controllers/relationship_controller.ex

リダイレクト先をフォロー / アンフォローしたユーザページに変更します。
修正前:
def create(conn, params) do
  if SampleApp.Relationship.follow!(params["id"], params["follow_id"]) do
    conn = put_flash(conn, :info, "Follow successfully!!")
  else
    conn = put_flash(conn, :error, "Follow failed!!")
  end

  redirect(conn, to: static_pages_path(conn, :home))
end

def delete(conn, params) do
  SampleApp.Relationship.unfollow!(params["id"], params["unfollow_id"])

  conn
  |> put_flash(:info, "Unfollow successfully!!")
  |> redirect(to: static_pages_path(conn, :home))
end
修正後:
def create(conn, params) do
  if SampleApp.Relationship.follow!(params["id"], params["follow_id"]) do
    conn = put_flash(conn, :info, "Follow successfully!!")
  else
    conn = put_flash(conn, :error, "Follow failed!!")
  end

  redirect(conn, to: user_path(conn, :show, params["follow_id"]))
end

def delete(conn, params) do
  SampleApp.Relationship.unfollow!(params["id"], params["unfollow_id"])

  conn
  |> put_flash(:info, "Unfollow successfully!!")
  |> redirect(to: user_path(conn, :show, params["unfollow_id"]))
end

Speaking to oneself

一応、全部の設定は反映しているはずですが、
書き忘れている部分があるかもしれません。
もし、何かお気づきの点、変更が足りないなど、
ご指摘などありましたら、一報頂けると助かります。
本当は、少しずつバージョンアップした方がいいのは分かってるんですけど、
面倒くさかったんです。(後悔はしてない)

Bibliography

2015年8月30日

[Rails Tutorial for Phoenix]Last refactoring

Goal

細かい部分のリファクタリングを実施する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4

Wait a minute

Phoenix Tutorialのリファクタリング第四弾。
最後のリファクタリングです。

Index

Last refactoring
|> Create encrypt module
|> Unification of wording
|> Create sign-in module
|> Error message at the time of user registration
|> Delete button of Micropost
|> Extra

Create encrypt module

Userモデルから暗号化する関数を分離します。

ファイル: lib/encryption.ex

新しく暗号化を担当するモジュールを作成します。
defmodule SampleApp.Encryption do
  # password decrypt
  def decrypt(password) do
    Safetybox.decrypt(password)
  end

  # password encrypt
  def encrypt(password) do
    Safetybox.encrypt(password, :default)
  end
end

ファイル: web/models/user.ex

以下の関数を削除して下さい。
  • decrypt/1
  • encrypt/1
Encryptionモジュールを使用するように修正して下さい。
修正前:
def set_password_digest(changeset) do
  password = Ecto.Changeset.get_field(changeset, :password)
  change(changeset, %{password_digest: encrypt(password)})
end
修正後:
def set_password_digest(changeset) do
  password = Ecto.Changeset.get_field(changeset, :password)
  change(changeset, %{password_digest: SampleApp.Encryption.encrypt(password)})
end

ファイル: web/controllers/user_controller.ex

Encryptionモジュールを使用するように修正して下さい。
修正前:
def edit(conn, %{"id" => id}) do
  user = Repo.get(SampleApp.User, id)
  user = Map.put(user, :password, SampleApp.User.decrypt(user.password_digest))
  changeset = SampleApp.User.changeset(user)

  render(conn, "edit.html", user: user, changeset: changeset)
end
修正後:
def edit(conn, %{"id" => id}) do
  user = Repo.get(SampleApp.User, id)
  user = Map.put(user, :password, SampleApp.Encryption.decrypt(user.password_digest))
  changeset = SampleApp.User.changeset(user)

  render(conn, "edit.html", user: user, changeset: changeset)
end

Unification of wording

ログイン / ログアウト、サインイン / サインアウトと統一していない文言を使用しています。
サインインの方へ統一します。

ファイル: web/templates/session/login_form.html

ファイル名の変更。
修正前: login_form.html.eex
修正後: signin_form.html.eex
パラメータ名の変更。
修正前:
<%= form_for @conn, session_path(@conn, :create), [name: :login_params], fn f -> %>
修正後:
<%= form_for @conn, session_path(@conn, :create), [name: :signin_params], fn f -> %>

ファイル: web/controllers/session_controller.ex

使用するテンプレート名を修正。
修正前:
def new(conn, _params) do
  render conn, login_form.html"
end
修正後:
def new(conn, _params) do
  render conn, "signin_form.html"
end
パラメータ名と文言の修正。
修正前:
def create(conn, %{"login_params" => %{"email" => email, "password" => password}}) do
  case login(email, password) do
    {:ok, user} ->
      conn
      |> put_flash(:info, "User login is success!!")
      |> put_session(:user_id, user.id)
      |> redirect(to: static_pages_path(conn, :home))
    :error ->
      conn
      |> put_flash(:error, "User login is failed!! email or password is incorrect.")
      |> redirect(to: session_path(conn, :new))
  end
end
修正後:
def create(conn, %{"signin_params" => %{"email" => email, "password" => password}}) do
  case sign_in(email, password) do
    {:ok, user} ->
      conn
      |> put_flash(:info, "User sign-in is success!!")
      |> put_session(:user_id, user.id)
      |> redirect(to: static_pages_path(conn, :home))
    :error ->
      conn
      |> put_flash(:error, "User sign-in is failed!! email or password is incorrect.")
      |> redirect(to: session_path(conn, :new))
  end
end
flashのメッセージを修正。
修正前:
def delete(conn, _params) do
  conn
  |> put_flash(:info, "Logout now! See you again!!")
  |> delete_session(:user_id)
  |> redirect(to: static_pages_path(conn, :home))
end
修正後:
def delete(conn, _params) do
  conn
  |> put_flash(:info, "Sign-out now! See you again!!")
  |> delete_session(:user_id)
  |> redirect(to: static_pages_path(conn, :home))
end
関数名を修正。
修正前:
defp login(email, password) do
  ...
end
修正後:
defp sign_in(email, password) do
  ...
end
Safetyboxの部分で、Encryptionを使うように修正。
修正前:
defp authentication(user, password) do
  case user do
    nil -> false
      _ ->
        password == Safetybox.decrypt(user.password_digest)
  end
end
修正後:
defp authentication(user, password) do
  case user do
    nil -> false
      _ ->
        password == SampleApp.Encryption.decrypt(user.password_digest)
  end
end

Create sign-in module

Sessionコントローラのサインインを分離します。

ファイル: lib/sing_in_action.ex

新しくサインインを行うモジュールを作成します。
defmodule SampleApp.Signin do
  import SampleApp.Authentication

  def sign_in(email, password) do
    user = SampleApp.User.find_user_from_email(email)
    case authentication(user, password) do
      true -> {:ok, user}
         _ -> :error
    end
  end
end
Sessionコントローラの認証を分離します。

ファイル: lib/authentication.ex

新しく認証を行うモジュールを作成します。
defmodule SampleApp.Authentication do
  def authentication(user, password) do
    case user do
      nil -> false
        _ ->
          password == SampleApp.Encryption.decrypt(user.password_digest)
    end
  end
end

ファイル: web/controllers/session_controller.ex

以下の関数を削除します。
  • sign_in/2
  • authentication/2
importを追加します。
import SampleApp.Signin

Error message at the time of user registration

新しくユーザを登録する時に、エラーメッセージが発生していますね。
これを修正します。

ファイル: web/models/user.ex

新しく関数を作成。
def new do
  %SampleApp.User{} |> cast(:empty, @required_fields, @optional_fields)
end

ファイル: web/controllers/user_controller.ex

作成した関数を使用するように、newアクションを修正します。
修正前:
def new(conn, _params) do
  changeset = SampleApp.User.changeset(%SampleApp.User{})
  render(conn, "new.html", changeset: changeset)
end
修正後:
def new(conn, _params) do
  render(conn, "new.html", changeset: SampleApp.User.new)
end

Delete button of Micropost

自分自身の投稿以外は削除できないようにします。

ファイル: web/templates/shared/microposts.html.eex

表示中のユーザidとマイクロポストのユーザidの比較を追加しました。
修正前:
<ol class="microposts">
  <li>
  <%= for post <- @posts do %>
    <span class="content"><%= post.content %></span>
    <span class="timestamp">
      Posted <%= post.inserted_at %> ago.
    </span>
    <%= link "Delete", to: micropost_path(@conn, :delete, post), method: :delete, class: "btn btn-danger btn-xs" %>
  <% end %>
  </li>
</ol>
修正後:
<ol class="microposts">
  <li>
  <%= for post <- @posts do %>
    <span class="content"><%= post.content %></span>
    <span class="timestamp">
      Posted <%= post.inserted_at %> ago.
    </span>
    <%= if @user.id == post.user_id do %>
      <%= link "Delete", to: micropost_path(@conn, :delete, post), method: :delete, class: "btn btn-danger btn-xs" %>
    <% end %>
  <% end %>
  </li>
</ol>

Extra

ページネーションのリファクタリングをした時に修正を忘れていました。

ファイル: web/controllers/micropost_controller.ex

Micropostコントローラで、リダイレクト先を指定を修正します。
createアクションの修正。
修正前:
def create(conn, %{"micropost" => micropost_params}) do
  ...

  action = "#{user_path(conn, :show, conn.assigns[:current_user].id)}?select_page=1"
  redirect(conn, to: action)
end
修正後:
def create(conn, %{"micropost" => micropost_params}) do
  ...

  redirect(conn, to: user_path(conn, :show, conn.assigns[:current_user]))
end
deleteアクションの修正。
修正前:
def delete(conn, %{"id" => id}) do
  ...

  action = "#{user_path(conn, :show, conn.assigns[:current_user].id)}?select_page=1"

  conn
  |> put_flash(:info, "Micropost deleted successfully.")
  |> redirect(to: action)
end
修正後:
def delete(conn, %{"id" => id}) do
  ...

  conn
  |> put_flash(:info, "Micropost deleted successfully.")
  |> redirect(to: user_path(conn, :show, conn.assigns[:current_user]))
end

Speaking to oneself

リファクタリングをしようと思えば幾らでもできるのですが、
切りがないので、一旦これで終了します。
修正した方が良い部分などがありましたら、
ご一報頂けると助かります。
この後は、記事の修正をします。
全記事の書き直しだ~
変更点の一覧は以下のリンク先です。
Github: darui00kara/phoenix_tutorial - finish refactoring

Bibliography

特になし

人気の投稿