ピクチャ 8.png
Amazon S3のトップページ。ここから申し込み可能。

Amazon S3はAmazonによるストレージサービスです。1バイト〜5GBまでのオブジェクトを自由に作成し、取り出し、更新、削除することができます。データベースに保存しないような、画像やファイルなどを保存するのに最適なソリューションです。

Amazon S3を使う場合、RubyにはAWS::S3という便利なライブラリがあります。これを使うとごく簡単にAmazon S3を利用することができます。Ruby on Railsでの開発の際にももちろん利用します。

ピクチャ 7.png
AWS::S3のトップページ。使い方も書かれています。

問題はローカル環境で開発している時です。ネットワーク接続が必須なのは非常に面倒ですし、何よりAmazon S3は有料のWeb APIなので、開発しているだけで課金されてしまうのは問題です。

そこで使うのがAmazon S3クローンであるParkPlaceです。このソフトウェアはクローンとは言っても置き換えを目指すのではなく、Amazon S3を利用したソフトウェア開発を便利にするために提供されているソフトウェアになります。ここではこのParkPlaceを使った開発方法を書きたいと思います。

ピクチャ 4.png
ParkPlaceのトップページ

(続きを読む…)

結構はまった。結局のところバグのようで。次のリリースでは修正されるはずです。

views/hoge/index.atom.builder内において、

atom_feed({’xmlns:openSearch’ => ‘http://a9.com/-/spec/opensearch/1.1/’}) do |feed|

等と定義しても無視されます。ソースは「actionpack-2.0.2/lib/action_view/helpers/atom_feed_helper.rb」で、ネームスペースっぽい定義が見つからない。

Module: ActionView::Helpers::AtomFeedHelper
 http://caboo.se/doc/classes/ActionView/Helpers/AtomFeedHelper.html#M006099

ここを見る限り、

Other namespaces can be added to the root element:

  app/views/posts/index.atom.builder:
    atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
        'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|


と書かれてはいるのだが、ソースを見る限りできそうにない。で、探してみたらパッチがあった。

#10304 ([PATCH] Atom feed helper should allow to add new namespaces to the feed root element) – Rails Trac – Trac
 http://dev.rubyonrails.org/ticket/10304

86 feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
87 feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
88
89 xml.feed(feed_opts) do

この辺りが修正範囲。xml〜ではじまっていればネームスペースとして見なす模様。これで無事、Atomフィードに任意のネームスペースが追加できるようになる。

AtomPubっぽいものを出力する方法です。

  • http://localhost:3000/example/index HTML
  • http://localhost:3000/example/index.rss RSS2.0
  • http://localhost:3000/example/index.atom Atom

という形で定義します。

Rails2.0の場合は、フォーマットによって出力を簡単に変更できますので、それを使います。

    respond_to do |format|
      format.html
      format.atom {
        render(:layout => false)
      }
      format.rss {
        render(:layout => false, :action => "index.rxml")
      }
    end

これで、Atomの場合は/views/example/index.atom.builderが、RSSの場合は/views/example/index.rxmlが読み込まれます。

Atomのテンプレートは以下のようになります。

atom_feed do |feed|
  feed.title("#{@title} » Example Feed")
  feed.updated(@memos.first.created_at)

  @memos.each do |m|
    feed.entry(m, :url => create_link(m)) do |entry|
      entry.title(m.title)
      entry.content(trans(m), :type => "html")
      entry.author do |a|
        a.name(@user.name)
      end
    end
  end
end

これはベースのようなものです。@memosというリストごとにフィードのエントリーを定義して、タイトルやcontentを作成しています。次にRSS側も同様に処理できます。


xml.instruct! :x ml, :version => "1.0", :encoding => "UTF-8"
xml.rss('version' => '2.0', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearchrss/1.0/') do
  xml.channel do
    xml.title @title
    url = url_for(:controller => :memo, :action => :index, :id => @user.name, :o nly_path => false)
    xml.link url
    xml.tag!("atom:id", url)
    xml.description(@title)
    xml.language "en-us"
    xml.ttl "40"
    xml.pubDate(Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z"))
    xml.managingEditor(@user.name)
    xml.generator("Jot a Memo")
    xml.tag!("openSearch:totalResults", @memos.paginate.total_entries)
    xml.tag!("openSearch:startIndex", @startIndex)
    xml.tag!("openSearch:itemsPerPage", @perpage)
    xml.guid(url, "isPermaLink" => 'false')
    xml.tag!("atom:updated", Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z"))
    @memos.each do |m|
      xml.item do
        xml.title(m.title)
        xml.tag!('atom:id', create_link(m))
        xml.guid(create_link(m), "isPermaLink" => 'false')
        xml.pubDate(m.created_at)
        xml.tag!("atom:updated", m.updated_at)
        xml.tag!("atom:summary", trans(m))
        xml.link(create_link(m))
        xml.description(trans(m))
        xml.guid(create_link(m))
        xml.author(m.user.name)
      end
    end
  end
end

やたらと長いのですが、あまり気にしないでください。大事なのは、xml.tag!を使うと、<atom:id>…</atom:id>のような形式が定義できるということと、xml.guid(aa, bb)のように二つ目の引数を定義すると、それがattributesになるということです。

後は<rss>の中に拡張書式を定義する場合があると思いますが、それもHashで指定可能です。

このように定義しておくと、次に以下のようなURLを考えた場合も容易に対応できます。

  • http://localhost:3000/example/view/id HTML
  • http://localhost:3000/example/view/id.rss RSS2.0
  • http://localhost:3000/example/view/id.atom Atom

でidで指定されたデータの詳細表示を行う場合です。この場合は以下のように対応します。


    @memos = [@memo]
    respond_to do |format|
      format.html
      format.atom {
        render(:layout => false, :action => "index.atom.builder")
      }
      format.rss {
        render(:layout => false, :action => "index.rxml")
      }
    end

@memoという1件のデータを@memosに配列で入れた以外は、indexと同じ処理になります。実際、フィードで1件を表示する場合も複数件表示する場合も処理は変わらないので、これで対応できます。もちろん、HTMLの場合は異なると思いますので、view.rhtmlは内容が異なるかと思います。

とは言ってもこれではGETだけの対応なので、これからPOST/PUT/DELETEの実装を完了してはじめてAtomPubへの対応が完了したと言えるのですが。

ピクチャ 10.png

まだ基礎だけですが。とりあえず使うのはGDataライブラリです。

$ sudo gem install gdataSuccessfully installed GData-0.0.4Installing ri documentation for GData-0.0.4…Installing RDoc documentation for GData-0.0.4…

スクリプトは以下のように。
/opt/local/lib/ruby/gems/1.8/gems/GData-0.0.4/lib/gdata/google_contact.rb

require 'gdata/base'
require 'builder'
module GData
class GoogleContact < GData::Base
def initialize
super 'cp', 'gdata-ruby', 'www.google.com'
end

 def list
request "/m8/feeds/contacts/#{@email}/base"
end

 def authenticate(email, password)
@email = email
super email, password
end
end

使い方は簡単で、

require ‘gdata/google_contact’
>> gc = GData::GoogleContact.new
=> #<GData::GoogleContact:0×157cd34 @url=”www.google.com”, @source=”gdata-ruby”, @service=”cp”>
>> gc.authenticate “example@gmail.com”, “[your password]”
=> {”Authorization”=>”GoogleLogin auth=…\n”, “Content-Type”=>”application/atom+xml”}
>> gc.list

でリストがXMLで返ってきます。本当はこれをさらに解析してvCard形式とかでもとれるようになっていると便利そう。実際に返ってくるデータは次のような感じ(整形しています)。
=> ”
<?xml version=’1.0′ encoding=’UTF-8′?>
<feed xmlns=’http://www.w3.org/2005/Atom’ xmlns:openSearch=’http://a9.com/-/spec/opensearchrss/1.0/’ xmlns:gContact=’http://schemas.google.com/contact/2008′ xmlns:gd=’http://schemas.google.com/g/2005′>
<id>example@gmail.com</id>
<updated>2008-03-10T05:05:34.160Z</updated>
<category scheme=’http://schemas.google.com/g/2005#kind’ term=’http://schemas.google.com/contact/2008#contact’/>
<title type=’text’>Atsushi Nakatsugawa’s Contacts</title>
<link rel=’alternate’ type=’text/html’ href=’http://www.google.com/’/>
<link rel=’http://schemas.google.com/g/2005#feed’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base’/>
<link rel=’http://schemas.google.com/g/2005#post’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base’/>
<link rel=’self’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base?max-results=25′/>
<link rel=’next’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base?start-index=26&max-results=25′/>
<author>
<name>Atsushi Nakatsugawa</name>
<email>example@gmail.com</email>
</author>
<generator version=’1.0′ uri=’http://www.google.com/m8/feeds’>Contacts</generator>
<openSearch:totalResults>376</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>25</openSearch:itemsPerPage>
<entry>
<id>http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0</id>
<updated>2007-11-26T02:27:10.592Z</updated>
<category scheme=’http://schemas.google.com/g/2005#kind’ term=’http://schemas.google.com/contact/2008#contact’/>
<title type=’text’>Atsushi Nakatsugawa</title>
<link rel=’self’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0′/>
<link rel=’edit’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0/1196044030592000′/>
<gd:email rel=’http://schemas.google.com/g/2005#other’ address=’example@moongift.jp’ primary=’true’/>
</entry>

タイトルにユーザ名が入って、gd:emailのaddressにメールアドレスが入る形ですね。個別で取得する場合はidを利用するようです。その他、住所などのデータも入っている場合は取得できます。とは言え、http://schemas.google.com/g/2005が見られないので、項目の詳細は不明です。

が、メールアドレスのリストだけであればこれで取得できるので便利です。

Rails2.0になった際に、ページネーションとAction Web Serviceがそれぞれプラグインで提供されるようになりました。Action Web Serviceは

script/plugin install http://biorails.org/svn/biorails/plugins/action_web_services

にてインストールできます。で、この二つを組み合わせた場合に、なぜかモデル内に定義した関数が使えなくなりました。

NoMethodError (undefined method `authenticate' for #<Class:0x2360a04>):
/opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1532:in `method_missing_without_paginate'
/vendor/plugins/will_paginate/lib/will_paginate/finder.rb:101:in `method_missing'

このような感じです。なぜかClassに関数がないといってきます。実際に処理を呼び出しているのはUserモデルのUser. authenticateで、

@user=User.authenticate(h[:username], h[:password])

のように呼び出しています。が、メソッドがないと言われます。実際、コンソールでやっても失敗します。
原因は不明なのですが、
vendor/plugins/will_paginate/lib/will_paginate/finder.rb

def method_missing_with_paginate(method, *args, &block)
# did somebody tried to paginate? if not, let them be
unless method.to_s.index('paginate') == 0
return method_missing_without_paginate(method, *args, &block)
end

ここに処理が飛んで、当然paginateを使っていないのでmethod_missing_without_paginateに飛ばされ、

/opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb

def method_missing(method_id, *arguments)

内の

send(method_id, *arguments)
else
super
end

にてsuperが呼び出されてエラーになっているようです。回避策としては、

def authenticate(name, passwd) (処理内容) end

def self.authenticate(name, passwd) (処理内容) end

のようにself.を付けるとエラーが出なくなります。

Railsの開発に限った訳ではないのですが、筆者はテキストエディタはEmacsを使っています。なので、Railsの開発もEmacsで行っています。EmacsにはRails開発向けにrails.elが作られており、それを使うと効率的な開発ができるようになります。

ピクチャ 1.png

ダウンロード&使い方についてはrails.el まとめ – ひげぽん OSとか作っちゃうかMona- を参考にしていただくとして、コマンドだけでも以下のように各種揃っています。

rails-browse-api            rails-browse-api-at-point
rails-controller-layout:menu       rails-controller-layout:switch-to-controller
rails-controller-layout:switch-to-functional-test
rails-controller-layout:switch-to-helper
rails-controller-layout:switch-to-migration
rails-controller-layout:switch-to-model
rails-controller-layout:switch-to-unit-test
rails-controller-layout:toggle-action-view
rails-controller-minor-mode        rails-create-tags
rails-find:config                  rails-find:controller
rails-find:db                      rails-find:fixtures
rails-find:helpers                 rails-find:javascripts
rails-find:layout                  rails-find:lib
rails-find:migrate                 rails-find:models
rails-find:public                  rails-find:stylesheets
rails-find:tasks                   rails-find:view
rails-functional-test-minor-mode   rails-goto-file-on-current-line
rails-lib:run-primary-switch       rails-lib:run-secondary-switch
rails-log:open                     rails-log:open-development
rails-log:open-production          rails-log:open-test
rails-migration-minor-mode         rails-minor-mode
rails-model-layout:menu            rails-model-layout:switch-to-controller
rails-model-layout:switch-to-fixture
rails-model-layout:switch-to-mailer
rails-model-layout:switch-to-migration
rails-model-layout:switch-to-model
rails-model-layout:switch-to-unit-test
rails-model-minor-mode             rails-nav:goto-controllers
rails-nav:goto-fixtures            rails-nav:goto-functional-tests
rails-nav:goto-helpers             rails-nav:goto-javascripts
rails-nav:goto-layouts             rails-nav:goto-mailers
rails-nav:goto-migrate             rails-nav:goto-models
rails-nav:goto-observers           rails-nav:goto-plugins
rails-nav:goto-stylesheets         rails-nav:goto-unit-tests
rails-plugin-minor-mode            rails-plugin-minor-mode:switch-to-init
rails-plugin-minor-mode:switch-with-menu
rails-rake:migrate                 rails-rake:migrate-to-prev-version
rails-rake:migrate-to-version      rails-rake:task
rails-run-sql                      rails-script:breakpointer
rails-script:console               rails-script:create-project
rails-script:destroy               rails-script:destroy-controller
rails-script:destroy-mailer        rails-script:destroy-migration
rails-script:destroy-model         rails-script:destroy-observer
rails-script:destroy-plugin        rails-script:destroy-resource
rails-script:destroy-scaffold      rails-script:generate
rails-script:generate-controller   rails-script:generate-mailer
rails-script:generate-migration    rails-script:generate-model
rails-script:generate-observer     rails-script:generate-plugin
rails-script:generate-resource     rails-script:generate-scaffold
rails-script:output-mode           rails-script:toggle-output-window
rails-search-doc                   rails-speedbar:get-focus
rails-svn-status-into-root         rails-test:compilation-mode
rails-test:run                     rails-test:run-current
rails-test:run-current-method      rails-view-minor-mode
rails-view-minor-mode:create-helper-from-block
rails-view-minor-mode:create-partial-from-selection
rails-ws:auto-open-browser         rails-ws:open-browser
rails-ws:open-browser-on-controller
rails-ws:print-status              rails-ws:start
rails-ws:start-default             rails-ws:start-development
rails-ws:start-production          rails-ws:start-test
rails-ws:stop                      rails-ws:switch-default-server-type
rails-ws:toggle-start-stop

ピクチャ 2.png
個人的によく使うのは、ビューとコントローラの切り替えと、ログ表示機能、ファイル表示機能などでしょうか。なお、個人的によくやる

return render(:action => :index)

のような記述の場合にC-Enterを押すと、”index)”までの認識になってしまいます。

render :action => :index
return

の場合は問題ありません。Java開発を通じてEclipseの素晴らしさに触れている方はRadRailsを使われるケースも多いようですが、個人的には重たいように感じられてなりません。Emacsがごく軽いかと言われるとそうでもありませんが…。

Rails2.0からページネーションがプラグインベースになりました。普通のページネーションであれば、

@memos = @user.memos.paginate(:page => params[:page], :per_page => 10, :o rder=>”memos.id DESC”)

だけで使えるようになって便利ですが、任意の情報に対してページネーションする方法がいまいち分かっていませんでした。そして、acts_as_searchableを使って、HyperEstraierで全文検索を行う際に、ページネーションをどのように行えば良いのか調べつつ分かったところを書いてみたいと思います。

実際にページネーション処理を行っているのは、

vendor/plugins/will_paginate/lib/will_paginate/finder.rb

になります。ここの

def paginate_by_sql(sql, options)
  WillPaginate::Collection.create(*wp_parse_options!(options)) do |pager|
    query = sanitize_sql(sql)
    options.update :o ffset => pager.offset, :limit => pager.per_page

    original_query = query.dup
    add_limit! query, options
    # perfom the find
    pager.replace find_by_sql(query)

    unless pager.total_entries
      count_query = original_query.sub /bORDERs+BYs+[w`,s]+$/mi, ''
      count_query = "SELECT COUNT(*) FROM (#{count_query}) AS count_table"
      # perform the count query
      pager.total_entries = count_by_sql(count_query)
    end
  end
end

が参考になります。

pager.replaceに、リストを入れて、pager.total_entriesに行数を入れればOKのようです。この処理をまねて、

app/controllers/application.rb

に次のように定義します。

module WillPaginate
  module Finder
    module ClassMethods
      def paginate_by_fulltext_search(query, options)
        WillPaginate::Collection.create(*wp_parse_options!(options)) do |pager|
          pager.replace fulltext_search(query, options)

          count_options = Hash.new
          count_options[:count] = true
          count_options[:attributes] = options[:attributes] if options[:attributes]
          pager.total_entries = fulltext_search(query, count_options)
        end
      end
    end
  end
end

です。そして、呼び出し側では次のようにします。

@memos = Memo.paginate_by_fulltext_search(params[:q], :attributes => “user_id NUMEQ %d” % @user.id, :limit => perpage, :o ffset => offset, :page => params[:page], :per_page => perpage)

ここで、:limitはacts_as_searchable用、:per_pageはwill_paginate用になります。:pageや:offsetは共通で利用できるようです。:attributesはあれば設定します。これさえ設定しておけば、ビュー側は

<%= will_paginate @memos, :prev_label=> _(’« Prev’), :next_label=> _(’Next »’) %>

が利用できて便利です。ちなみにpaginate_by_sqlではoptionsがアップデートされるので、デフォルトの30件になってしまうようですね。これはなぜなんだろう…。

MOONGIFTネットワーク。こちらもぜひご覧ください。
MOONGIFT
Open Service
Rails 2.0
Resident on Net
iPhone最適化
リーンソフトウェア
MarketPedia
Producing Web
Cool Coding