Rails邪道resource定義

背景

Rails 3 デビューのくせに初版のアジャレイルズ本で勉強した過去があったり、いびつな知識を持っていたので

  • routing は昔懐かしい書き方が頭に残っている
  • URI の path が複数系なのがなんかキモイ

という二つのわがままから、単数形の URI を作りたかった。ので、実際にやってみた。

※ 先に結論だけ言うと Rails 使っといて Rail Way からわざわざ外れようとするのはあんまり嬉しくないです。でもやっぱ URI は RESTful とは違う視点でもこだわりたいじゃないですか。

resourcesの基礎

  • routing で resource を定義する helper があり、用意されているメソッドは resource, resources
  • resource(単数形)は一つのresource用? 用途を思いつかなかったので resources(複数形) を試す

とりあえず scaffold user

改めて例は user で、こんな感じに scaffold してみる。

rails g scaffold user nick:string, name:string

このとき migration はこんな感じ。

db/migrate/xxx_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :nick
      t.string :name
      t.timestamps
    end
  end
end

routing は

config/routes.rb
resources :users

うん、シンプルそのもの。Model は

app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :nick, :name
end

Controller は例の感じで CRUD を生成する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
     ...
  end

  def show
      ...
  end

  ...
end

このときrake routes は以下のようになる。

    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy

この routing がどう変わるかが大事。

「resources 単数形」に書き換える

config/routes.rb
resources :user

すると当然 Controller のファイル名と class名に影響が出る。

app/controllers/user_controller.rb
class UserController < ApplicationController
  ...
end

この段階で rake routes すると

user_index GET    /user(.:format)          user#index
           POST   /user(.:format)          user#create
  new_user GET    /user/new(.:format)      user#new
 edit_user GET    /user/:id/edit(.:format) user#edit
      user GET    /user/:id(.:format)      user#show
           PUT    /user/:id(.:format)      user#update
           DELETE /user/:id(.:format)      user#destroy

users がなくなって user_index になっていることが分かる。このままでは先ほど scaffold したコードは動かない。そこで対処方法は2つ。

  1. users_path を使っている部分を全部 user_index_path に書き換える。具体的には link_to と form_for と redirect_to の中。
  2. resources :user, :as => 'users' で users にマップし直す

2 の方が圧倒的に楽だし、scaffold の form_for は create も update も action が同じになっていないと動かないので 2 にすべき。するとこのときの routes はこんな感じ。

    users GET    /user(.:format)          user#index
          POST   /user(.:format)          user#create
 new_user GET    /user/new(.:format)      user#new
edit_user GET    /user/:id/edit(.:format) user#edit
     user GET    /user/:id(.:format)      user#show
          PUT    /user/:id(.:format)      user#update
          DELETE /user/:id(.:format)      user#destroy

これで URI を単数形にしつつ scaffold の動作を維持できた。

id以外でshowしたい

Rails の resources は id を中心にしており、この id は DB の key になっている。

しかしどうだろう。あるサービスで自分のアカウントの情報が例えば

/users/123456

みたいな URI だったら。

ちょっとガッカリだよね。そこでこれを強引に変更してみる。

get '/user/:nick' => 'user#show', :as => 'user'
resources :user,                  :as => 'users'

これで URI を作る際の user_path というメソッドを上書きできる。これは routing は先に書いたものが優先されることを利用している。

Rails routes are matched in the order they are specified, so if you have a resources :photos above a get 'photos/poll' the show action’s route for the resources line will be matched before the get line. To fix this, move the get line above the resources line so that it is matched first.

Ruby on Rails Guides: Rails Routing from the Outside In

ただし、このままでは全体的にはやはり :id を前提にして動く。したがってもう少しいじらないといけない。

Controller で

@user = User.find(params[:nick])

してた部分はこうなる。

@user = User.where(:nick => params[:nick]).first

あるいは

@user = User.find_by_nick(params[:nick])

ただしこれらの方法は find + id での検索と違って見つからなくても例外が起きないため、このあとに

if @user
  ...
else
  render 'public/404.html', :status => 404
end

みたいにしてちゃんと 404 を返すようにしないといけない。

url_forで省略が効かなくなる

また URI 生成の部分で

link_to 'foo', @user
redirect_to @user

になっているところはそれぞれ

link_to 'foo', user_path(:nick => @user.nick)
redirect_to user_path(:nick => @user.nick)

と書き換える必要がある。これは前者の書き方が最終的に url_for(@user) を呼んでいて、url_for は

rails の named route で Symbol を使うことの利点と欠点 - QA@IT

で moro さんが書いているようにとっても便利にいい具合に仕事をしてくれるんだけど、残念ながら :id を前提にして動くため。

ちなみに

url_for(:nick => @user.nick)

とやってしまうと query string になって美しくない。これを避けるには controller から指定する必要があり、それだったら user_path(:nick => @user.nick) の方が短い。

showだけじゃダメ

new も対応していないので routing は最終的にこうなる。

 get '/user/new'   => 'user#new',  :as => 'new_user'
 get '/user/:nick' => 'user#show', :as => 'user'
 resources :user,                  :as => 'users'

このとき new が上になっていないとせっかく定義しても全部 /user/:nick に「食われてしまう」ので、new の定義の方が上になっていないといけない。

※ 逆にこのままでは :nick には new を使えない。結局 namespace のことを考えてないので、まだ実用的ではない。

まとめ

以上、

  • resource の scaffold の動作を満たしたまま
  • 単数形の Controllerに変更し
  • :id 以外のカラムを利用

する目的の動作にたどりついた。

とは言え、やっぱこれは邪道なんでしょうねぇ…。

なんか他にいい方法あったら教えてください。

Trac の feed を merge する Yapra の YAML を ERB で作る

タイトルなげぇ。

昨日 Yapra で Trac の timeline feed を 1本に を書いてから、Plagger の assets みたいな仕組みがあればベタに書かなくて済むのかなーとか思ってはいたんだけど、うまい方法も思いつかないので今日とりあえず ERB を使って一つのテンプレートから生成するようにしてみた。

想定しているパターン

  • 複数の Trac を運用しているがチェックが面倒なので Timeline の feed を一本化したい
  • それぞれの Trac に対してユーザーが複数いるので、各自の横断的な My Tickets の feed を生成したい

場合に役に立つ Yapra 用 YAML を作ります。そのために ERB を利用します。

基本構成

スクリプトと、それを動かしてできた結果のファイルがこんな風に一つのディレクトリに収まる形を想定しています。

.
|-- jane_tickets.yaml        My Tickets 用の YAML
|-- john_tickets.yaml        My Tickets 用の YAML
|-- merge_trac_timeline.erb  timeline 用の YAML のテンプレート
|-- merge_trac_timeline.yaml timeline 用の YAML
|-- prepare.rb               スクリプト
|-- smith_tickets.yaml       My Tickets 用の YAML
`-- user_tickets.erb         各 user の My Tickets 用の YAML のテンプレート

で、yapra からは

ruby YAPRA_PATH/bin/yapra -d /PATH/TO/ABOVE

という具合にファイルを置いたディレクトリを指定してやると

*.yml
*.yaml

以外は無視して動作してくれます。

スクリプト

users, sites などを適宜調整してください。

#! /usr/bin/ruby

require 'erb'
require 'optparse'

class TracMergePreparer
  class FileCannotOpen < Exception; end

  def initialize
    @path = nil
    @name = nil
  end

  def run
    if ( parse_args() )
      erb = ERB.new( File.read( @path ), nil, '-' )
      if ( @name == 'user_tickets' )
        users.each do |user|
          File.open( "#{user}_tickets.yaml", 'w' ) do |f|
            f.write( erb.result( binding ) )
          end
        end
      else
        erb.run( binding )
      end
    end
  end

  def parse_args
    ret = false

    opt = OptionParser.new()
    opt.on( '-f', '--format ERB_NAME' ) { |file|
      path = File.join( File.dirname( __FILE__ ), file )
      if ( File.exist?( path ) )
        @path = path
        @name = File.basename( file, '.erb' )
        ret   = true
      else
        raise FileCannotOpen
      end
    }
    opt.parse!

    return ret
  end

  def users
    return %w(
               john
               smith
               jane
              )
  end

  def sites
    return %w(
               foo
               bar
               baz
             )
  end
end # of class

if ( __FILE__ == $0 )
  app = TracMergePreparer.new()
  app.run()
end

timeline merge 用の YAML を作る

erb はこんな感じ。

<%-
# Usage: ruby prepare.rb -f merge_trac_timeline.erb > merge_trac_timeline.yaml
-%>
- module: RSS::load
  config:
    url:
      <%- sites.each do |site| -%>
      - http://HOST/PATH/<%= site %>/timeline?changeset=on&milestone=on&ticket=on&wiki=on&max=50&daysback=90&format=rss
      <%- end -%>
- module: Filter::sort
  config:
    method: date
- module: reverse
- module: Filter::ApplyTemplate
  config:
    title: '<%%= "[#{item.link.split( /\// )[4]}] #{item.title}" %>'
    description: <%%= item.description.gsub( /<[^>]+>/, '' ) %>
    content_encoded: <%= item.description %>
- module: RSS::save
  config:
    title: "multitrack"
    link: http://HOST/PATH/TO/FEED
    filename: /PATH/TO/FEED

Trac を設置している HOSTNAME や PATH、あと item.link から title に Trac の名前を埋めている部分も PATH の深さによって変わるので注意してください。それぞれ適宜修正のこと。

使い方は上の例で言うと

ruby prepare.rb -f merge_trac_timeline.erb > merge_trac_timeline.yaml

として使う形を考えています。

各ユーザーの My Tickets 用の YAML を作る

<%-
# Usage: ruby prepare.rb -f user_tickets.erb
-%>
- module: RSS::load
  config:
    url:
      <%- sites.each do |site| -%>
      - http://HOST/PATH/<%= site %>/report/7?format=rss&USER=<%= user %>
      <%- end -%>
- module: Filter::sort
  config:
    method: date
- module: reverse
- module: Filter::ApplyTemplate
  config:
    title: '<%%= "[#{item.link.split( /\// )[4]}] #{item.title}" %>'
    description: <%%= item.description.gsub( /<[^>]+>/, '' ) %>
    content_encoded: <%= item.description %>
- module: RSS::save
  config:
    title: "<%= user %>'s tickets"
    link: http://HOST/PATH/<%= user %>_tickets.xml
    filename: /PATH/TO/<%= user %>_tickets.xml

これも必要に応じて修正してください。使い方は

ruby prepare.rb -f user_tickets.erb

を想定しています。この名前はスクリプトの中で決め打っちゃってます。実行すると users に登録してある user の分だけ

#{user}_tickets.yaml

を生成します。

制限

それぞれの Trac に登録されているユーザーが同じでない場合は考えていません。feed が fetch できないとエラーで止まるかも。

また description 内で HTML タグらしき形のものを片っ端から除去しています。読めば分かるように副作用がありますので、注意して使うか、不要ならこの処理は捨てちゃってください。

git の pull request は fork 前提って理解で合ってますか

なんか github の pull-request の説明はすでに fork -> commit -> push 済みなのを前提に説明されているようで、他の方法がよく分からなかったのだけど、考えたら pull request ってのは「ここの repository にあるこの commit をそっちにも merge してくんねーか?」という意味だと思えば fork が公開されてなきゃ意味ないんですよね。

まだ github のサイト上でしかやったことがないけど、git-request-pull でも同じこと、、、なのかな。なんか man 見てもドキュメント読んでもこの辺の記述が見つけられないんだよなぁ。まぁ github 以外では git 使って分散管理する必要ないからこっちは困ってないんだけど。

はてなの新編集機能に(悪い意味で)ビビる

なんかはてなの編集機能が Ajax になったっぽいという記事をどこかで見かけてふらふらとはてな村へ出かけてみた。

どこからそんな編集機能を利用するのか分からない。

なんかセクションタイトルの部分に mouseover したら色が変わったので試しにクリックしたらテキストが textarea になった。あー edit in place ってやつでしたっけ、これ。

でだ。これ、なんで機能が明示されてないの? 普通に表示されてる [ 編集 ] リンクが従来通りなのはいいとして、新しい機能って試しにクリックしてみた人か真面目にはてなダイアリー日記を追いかけてる人か、はてな村の中にはてなの追っかけみたいな親切なお友達がいない限り気づかなくない?

あと。

他人の日記でも textarea になっちゃうのはすごいビビるのでやめてほしいよ? まぁ実際には編集結果を送信するボタンがないのでたぶん編集できないんだけど、それにしたってビビるじゃないか。

まぁ慣れてる人ははてなってこんなもん、と思うのかもしれないけど。

JavaScript 周りをもう一回確認しなきゃな

class ベース OO をマネっこしてるフレームワークは class 名というかコンストラクタというか、それを取得できなくなるよね?と不安になり、確認してみた1。あと、コンストラクタや prototype プロパティへあれこれ追加する記法が変わっちゃうとドキュメンテーションツールが対応できなくなるんじゃね?と思い、それを確認…しなきゃと思っているところ。(つまりまだ確認していない。)

挙げたのは小さめで目に留まっているものだけ。別にフレームワークを網羅するつもりはないし、大きいものは扱いに困りそうで敬遠してるところ。

class名

prototype(1.4)
constructor も instanceof も意図通り動作せず
jQuery(1.0.2)
独自の class 構文はない
mootools(rev.64)
constructor は意図通り取得できないが、instanceof での確認は可能

こうして見ると Web に特化しちゃうなら jQuery もアリかなと思わなくもない。mootools は全体の構造も丁寧に整理されている感じで好感が持てる。

prototype は最初のインパクトはすごかったが、インスパイアされた後出しのものが結構いいし、使うなら上のような問題がないものの方が嬉しいな。まぁ prototype が現状ではいちばん情報が豊富っぽいんだけど。

ドキュメンテーション

class の構造やツリーをドキュメント生成ツールが正しく認識できなくなるとドキュメントの自動生成ができなくなり、結果、ドキュメントのないコードだけが残り、「最初に書いたもん勝ち」というありがちな状況を生みそう。ということでドキュメント生成ツールとの相性を確認しなきゃな、と思っている。

今回、気になって調べたら Natural Docs という、これまた Perl で書かれたツールがあるみたいなので、それの動作も確認できたらいいなと思っている。

jsDoc × prototype
jsDoc × mootools
Natural Docs × prototype
Natural Docs × mootools

jQuery は独自の class 構文がないのでドキュメント生成ツールの対応を考える必要はたぶんないと思う。

Ajaxian >> Natural Docs: Better Javascript Doc によると、DoJo と Natural Docs の相性は悪くないのかな?

  1. そんなもん分かんなくたってメソッドがあったら動けばいいじゃないという考え方はもちろんアリ。 

FreeBSDグループは参加拒否か?

いつまでも承認待ちの状態から進展がない。まぁさすがに参加申し込みの一言に

Otsune, the Jail Master にあこがれて。

と書いたのはまずかったか。というか自分の日記の URL も何も情報がないんだもの、判断しようがないよね。すいません。とりあえずはてなの方に何か書こうかな。

終了ぅ〜

いきなり街のマンキツにきてこんなもん書いてるのもどうかと思うが。しかもさっきドトールでコーヒー飲んだじゃん、おれ。

とりあえず手応えとしてはそこそこいい感じ。正解って発表されないんだよね? このそこそこ感がいちばんドキドキしていやかもしれん。

さーて。早速飲み行っちゃうかぁ?

思わずるびまから日記探訪へ

前田さんは賢いけどドキュメント書いてくんないからなーなどと思ったりしながら時間と体力を消費する午前 3:30. まずいんじゃないだろうか、この状況。一応仮眠は取ったので体力的にはなんとかなるでしょうけど。

ちょっと気になるデジカメ

http://www.zdnet.co.jp/products/0310/15/rj01_f700.html

でも FUJIFILM ってとこが。。。Kodak 派なもので。

VeriSign の野望

http://www.zdnet.co.jp/news/0310/17/nebt_09.html

うーん、正直、site finder は Network Solution 部門の暴走だと思っていたんだけど、そういうことじゃなかったのかぁ。

iTunes 4.1 for Windows

http://www.apple.co.jp/itunes/

日本語版ダウンロードは 10/21 から。英語版 20MB ほどありますなぁ。あと、AAC を利用するには QuickTime 6.2 以降も必要、と。 というかインストーラが QuickTime 6.4 を入れてくれた気がするのですが。Windows 版だからか? も〜ぅ 6.3 入ってたの確認してくれって。

考えたらこのアプリは Music Store への誘導と iPod への誘導がカギなんですよね。

そこで食指が動かない人間にはアプリ単体としてみると他のメディアプレイヤーに勝る部分てないので、あとは「雰囲気を楽しむ」くらいしかないんですよね。うーむむむ。長いこと Winamp で過ごしてきた人間には無用の長物か。

Windows で Rendezvous が使えるのは面白いけど、周りにユーザーがいないと意味ないっすねぇ。

あ。忘れてた。ラジオが聞ける。これはいいかも。

About

例によって個人のなんちゃらです