Tag Archives: rails3

scopeでtwitterっぽいルーティング

「Ruby on Rails 3 アプリケーションプログラミング」のルーティングの項をパラパラとめくっていたら、scopeを使ってtwitterっぽくトップレベル(?)にユーザ名を持ってくるようなルーティングができることを知ったのでメモ。

概要

例えばroutes.rbには次のように記述したとする。

scope ':uid' do
  resources :todos
end

このとき次のようなURLでアクセスすると、USERNAMEの部分をparams[:uid]で受け取ることができる。

http://scope-routing-sample.dev/USERNAME/todos

サンプルアプリを作ってみた

早速サンプルのTODOアプリを作ってgithubに上げてみた:scope-routing-sample

次の手順で動く、はず(bundler, rvm, pow, powderが導入済みの前提で)。

$ git cloen git@github.com:a2ikm/scope-routing-sample.git
$ cd scope-routing-sample
$ rvm use 1.8.7@scope-routing-sample --create
$ bundle
$ powder
$ rake db:migrate
$ open http://scope-routing-sample.dev/

開いたら、

  • 適当にユーザを登録(Uidは半角英数字がベター)
  • 一覧の画面でUidをクリックして開く
  • さぁURLを確認!

ハマったところ

app/views/todos/_form.html.erbにあるform_forがちょっと面倒くさかった。

ただのresourcesなら

form_for(@todo) do |f|

のような記述でよしなにやってもらえるんだけど、今回の場合には、

  • :uidに何が入るのかを指定する必要がある
  • Todoのインスタンスが保存済みか否かで:idを指定するか否かが変わる

ため、一工夫する必要があった(form_for and scopes, rails 3 – stackoverflowを参考にした)。

form_for(@todo, :url => (@todo.persisted? ? todo_path(:id => @todo.id, :uid => @uid) :
                                                 todos_path(:uid => params[:uid]))) do |f|

こうすることで、fには@todoに関するオブジェクトが入り、action属性のURLにはtodo_pathとtodos_pathが適宜切り替えられ、:idと:uidを指定することができる。

まとめ

  • scopeを使ったルーティングはカッコイイかもしれない
  • form_forで一工夫が必要

scopeをネストすればgithubみたいにUSERNAME/PROJECTNAMEみたいなURLも作れるのかな?今度試してみよう。


OmniAuthのサンプル

RailsRumble::Separating Authentication and Identity with OmniAuthに沿ってomniauthのサンプルアプリを作って上げた。

a2ikm / omniauth-sample

ただし作ったのはcallbackを受けるところ(Handling Callbacks)までで、usersにレコードを追加したりする部分は書いていない。

OAuth|OpenIDプロバイダ側とのやりとりはすべてomniauthが吸収してくれてるので、開発者はサービス上でのユーザの遷移に集中できて凄く楽(認証情報をどう持つかとかは考えなきゃならないけども)。

Railscastsにはdeviceとの連携の話が上がっていたので、そちらも参照するといいかも。


Rails3でRSPec2とmachinist2

Rails3でRSpec2machinist2を使うための下準備。

$ alias r=rails
$ r new testapp -T -d mysql
$ cd testapp
$ vi config/application.rb # Test::Unitにさよなら
$ vi Gemfile # RSpec2とmachinist2を追加
$ bundle install
$ r g rspec:install
$ r g machinist:install
$ vi config/application.rb # fixtureの代わりにblueprintを追加するように
$ vi ~/.autotest # DBを毎回クリアする

各種ファイルの編集は下記の通り。

Test::Unitにさよなら

-require 'rails/all'
+require "active_record/railtie"
+require "action_controller/railtie"
+require "action_mailer/railtie"
+require "active_resource/railtie"

RSpec2とmachinist2を追加

RSpec2+Rails3+autotest環境の構築あたりを参考にやってみた。でもautotestが上手く動かない気がする。

source 'http://rubygems.org'

gem 'rails', '3.0.3'
gem 'mysql2'

group :development, :test do
  gem 'rspec-rails'
  gem 'autotest'
  gem 'webrat'
  gem 'faker'
  gem 'machinist', '>= 2.0.0.beta1'
end

fixtureの代わりにblueprintを追加するように

以下をconfig/application.rbのclass TestApp::Application内に記述すると、モデルを追加したときにfixtureではなくblueprintの追加をしてくれる。

config.generators do |g|
  g.fixture_replacement :machinist
end

DBを毎回クリアする

~/.autotestに下記を記述してDBを毎回クリアする。じゃないとコケる。 「Rails3 + rspec2 その他もろもろ環境でのautotest周りの設定とか」を参照。

# オートテスト開始時にテストDBをクリアする
Autotest.add_hook :initialize do |at|
    system("rake db:test:prepare")
    next false
end

# ファイル更新時にテストDBをクリアする
Autotest.add_hook :ran_command do |at|
    system("rake db:test:prepare")
    next false
end

Rails3でTest::Unitを無効化

Rails3でTest::Unitを使わないようにする方法。rspecの導入方法については参考資料を参照。

まずrails newコマンドで新規プロジェクトを作成する際に-Tオプションを付けて、testディレクトリを作成させないようになる。

$ rails new new_project -T

次にconfig/application.rbを次のように編集して、rails/test_unit/railtieを読み込まないようにする。こうすることでTest::Unitのテンプレート自動生成等を行わないようにする。

--- config/application.rb.org  2010-10-05 11:39:47.000000000 +0900
+++ config/application.rb   2010-10-05 11:41:59.000000000 +0900
@@ -1,6 +1,9 @@
 require File.expand_path('../boot', __FILE__)
 
-require 'rails/all'
+require 'active_record/railtie'
+require 'action_controller/railtie'
+require 'action_mailer/railtie'
+require 'active_resource/railtie'
 
 # If you have a Gemfile, require the gems listed there, including any gems
 # you've limited to :test, :development, or :production.

参考資料


ActiveSupport::Concern

以前書いたモジュールの特異メソッドをincludeして使うと同じことが、ActiveSupport::Concernを使うことでもうちょっと綺麗に書ける。

module A
  extend ActiveSupport::Concern

  included do
    # Aがincludeされた際に、includeしたクラスのコンテキストで実行される
  end

  module InstanceMethods
    def instance_method_of_a
      p "instance_method_of_a"
    end
  end

  module ClassMethods
    def class_method_of_a
      p "class_method_of_a"
    end
  end
end
class B
  include A
end

こんなモジュールをとあるクラスBでincludeすることで、

  • InstansMethods以下に定義されたメソッドがincludeによってインスタンスメソッドとして追加
  • ClassMethods以下に定義されたメソッドがextendによってクラスメソッドとして追加

される。

モジュールをincludeすることによって追加されるクラスメソッド、インスタンスメソッドが明確になるので嬉しい。

ただそれだけだとちょっとメリットが弱い気がしなくもない。きっとincludedや、今回触れなかったextendedあたりが活躍するんだろう。

参考資料


ActiveModel::Callbacks

ActiveModel::Callbacksを使えばafter_saveなどのコールバックを手軽に実装できる。

まずActiveRecord::Baseのような抽象クラスを次のように定義する。

class AbstractModel
  extend ActiveModel::Callbacks
  define_model_callbacks :save

  def save
    _run_save_callbacks do
      # 具体的なsaveメソッドの内容
      # この前後にbefore_saveとafter_save、そしてaround_saveが呼ばれる
    end
  end
end

重要なのはsaveメソッドの内容を_run_save_callbacksでラップすること。これはコールバックを利用するメソッドに応じて動的に生成され、createメソッドであれば_run_create_callbacksを利用する。

そしてコールバックを利用したい具象クラスでは次のようにする。

class SomeModel < AbstractModel
  before_save :foo

  protected

  def foo
    # before_saveで実行したい内容
  end
end

これ以外にもbefore_saveにブロックを渡したり、before_saveをインスタンスメソッドとして定義しているオブジェクトを渡したりすることで処理を実装できる。

define_model_callbacksでは複数のコールバック対象のメソッドを指定することができる。

define_model_callbacks :save, :create

:onlyオプションを使うことで例えばafter_*だけに制限することもできる。

define_model_callbacks :save, :only => :after