AtomPubな何かを作ってみよう その4 〜実装編 Part.03「子リソースの作成」〜
前回は、作成したAtomPubリソースの動作確認をしてみました。今回は、前回作成したリソースの子供の関係にあたるリソースを作ってみたいと思います。
DBのマイグレーションを行う
まずDBに新しくテーブルを作ります。ファイル名を"db/migrate/002_create_childresources.rb"とでもしてマイグレーションファイルを作成しましょう。
class CreateChildresources < ActiveRecord::Migration def self.up create_table :namedwords, :force => true do |t| t.column :meaning_id, :string t.column :name, :string t.column :author, :string t.column :comment, :string t.column :updated, :timestamp end create_table :meaningscomments, :force => true do |t| t.column :meaning_id, :string t.column :author, :string t.column :comment, :string t.column :updated, :timestamp end add_index :namedwords, :name end def self.down [:namedwords, :meaningscomments].each do |t| drop_table t end end end
作成したら、マイグレーションを実行します。
> rake db:migrate
ルーティングの設定を行う
続いて、ルーティング設定を追加します。"config/routes.rb"を書き換えます。
ActionController::Routing::Routes.draw do |map| # '/meanings' or '/meanings/{意味語}' => MeaningsController map.resources :meanings, :path_prefix => '' # '/meanings/{意味語}'のbaseを設定 meaning_base = '/meanings/:meaning' # '/meanings/{意味語}/namedwords' => NamedwordsController map.resources :namedwords, :path_prefix => meaning_base # '/meanings/{意味語}/comments' => MeaningscommentsController map.resources :comments, :controller => 'meaningscomments', :path_prefix => meaning_base, :name_prefix => 'meanings_' end
commentsリソースの設定がちょっとややこしくなっていますが、これは、後々namedwordsの方にもコメントリソースを作った時に名前の重複による動作不正が起きるのを避ける為の伏線です。
Modelを作る
Modelに関しては特別な設定はしないので、ここでは省略。コピペで作ればいいだけなんですが、クラス名を書き換えておくのを忘れずに。
Controllerを作る
Controllerは、基本的には前々回の記事で作成した"meanings_controller.rb"を元に、"meaning"を、"namedword"と"meaningcomment"に書き換えたものを作る形になります。実際のソースコードはこの記事の最後に載せておきたいと思います。
ただ1点ポイントがありまして、namedwordsリソースもcommentsリソースも、URLに含まれる"意味語"をキーとして、指定された"意味語"に属するもののみを、出力するフィード/エントリの対象にする必要があります。その為、URLで与えられた"意味語"をキーとして検索するメソッドを、before_filterとして定義しておきます。
以下のように、"app/controllers/application.rb"にmust_specify_meaningメソッドを追記します。
class ApplicationController < ActionController::Base def must_specify_meaning if params[:meaning] @meaning = Meaning.find_by_name(params[:meaning]) unless @meaning render :text => '見つかりませんでした。', :status => '404 Not Found' return false end end return true end end
これを、"namedwords_controller.rb"と"meaningscomments_controller.rb"にbefore_filterとして設定するようにします。
完成!
という事で、ここまでで作ったものだけでも、単語のCRUD、単語に対する訳語のCRUD、単語に対するコメントのCRUDという機能が使えるWebサービスができた事になります。
とは言え、これだけじゃ面白くも何ともないし、認証機能も作ってないので誰でも好き放題し放題だったりとか、まだ色々と考慮すべき点があるので、それらについても追々作っていきたいと思います。
次回は?
次回は、namedwordsリソースの子リソースにあたる、"/meanings/{meaning}/namedwords/{namedword}/comments"リソースと、"/meanings/{meaning}/namedwords/{namedword}/stars"リソースを作ってみたいと思います。
今回作成したControllerのソースコード
app/controllers/namedwords_controller.rb
class NamedwordsController < ApplicationController # filter before_filter :must_specify_meaning # GET /meanings/{meaning}/namedwords def index @namedwords = Namedword.find(:all, :conditions => ['meaning_id=?', @meaning.id], :order => 'updated desc') headers['Content-Type'] = 'application/atom+xml;type=feed' render :action => 'feed' end # POST /meanings/{meaning}/namedwords def create parse_xml name = @parsed_params[:name] @namedword = Namedword.find_by_meaning_id_and_name(@meaning.id, name) if @namedword headers['Location'] = namedword_url(@meaning.name, @namedword.name) render :nothing => true, :status => '409 Conflict' else @namedword = Namedword.new(@parsed_params) if @namedword.save headers['Location'] = namedword_url(@meaning.name, @parsed_params[:name]) render :action => 'entry', :status => '201 Created' else render :xml => @namedword.errors.to_xml, :status => '400 Bad Request' end end end # GET /meanings/{meaning}/namedwords/{nameword} def show @namedword = Namedword.find_by_meaning_id_and_name(@meaning.id, params[:id]) if @namedword headers['Content-Type'] = 'application/atom+xml;type=entry' render :action => 'entry' else render :status => 404, :text => '指定したエントリは存在しません' end end # PUT /meanings/{meaning}/namedwords/{nameword} def update parse_xml @namedword = Namedword.find_by_meaning_id_and_name(@meaning.id, params[:id]) if @namedword @namedword.update_attributes(@parsed_params) if @namedword.save render :nothing => true, :status => '200 OK' else render :xml => @namedword.errors.to_xml, :status => '400 Bad Request' end end end # DELETE /meanings/{meaning}/namedwords/{nameword} def destroy @namedword = Namedword.find_by_meaning_id_and_name(@meaning.id, params[:id]) if @namedword @namedword.destroy render :nothing => true, :status => '200 OK' end end private # POST/PUTされたXMLを解析し、@parsed_paramsに保存 def parse_xml xml = REXML::Document.new(request.raw_post) @parsed_params = { :name => xml.elements['entry/title'].text, :author => xml.elements['entry/author/name'].text, :comment => xml.elements['entry/content'].text, :meaning_id => @meaning.id, :updated => Time.now, } end end
app/controllers/meaningscomments_controller.rb
class MeaningscommentsController < ApplicationController # filter before_filter :must_specify_meaning # GET /meanings/{meaning}/comments def index @meaningscomments = Meaningscomment.find(:all, :conditions => ['meaning_id=?', @meaning.id], :order => 'updated desc') headers['Content-Type'] = 'application/atom+xml;type=feed' render :action => 'feed' end # POST /meanings/{meaning}/comments def create parse_xml @meaningscomment = Meaningscomment.new(@parsed_params) if @meaningscomment.save headers['Location'] = meanings_comment_url(@meaning.name, @meaningscomment[:id]) render :action => 'entry', :status => '201 Created' else render :xml => @meaningscomment.errors.to_xml, :status => '400 Bad Request' end end # GET /meanings/{meaning}/comments/{commentid} def show @meaningscomment = Meaningscomment.find_by_id(params[:id]) if @meaningscomment headers['Content-Type'] = 'application/atom+xml;type=entry' render :action => 'entry' else render :status => 404, :text => '指定したエントリは存在しません' end end # PUT /meanings/{meaning}/comments/{commentid} def update parse_xml @meaningscomment = Meaningscomment.find_by_id(params[:id]) if @meaningscomment @meaningscomment.update_attributes(@parsed_params) if @meaningscomment.save render :nothing => true, :status => '200 OK' else render :xml => @meaningscomment.errors.to_xml, :status => '400 Bad Request' end end end # DELETE /meanings/{meaning}/comments/{commentid} def destroy @meaningscomment = Meaningscomment.find_by_id(params[:id]) if @meaningscomment @meaningscomment.destroy render :nothing => true, :status => '200 OK' end end private # POST/PUTされたXMLを解析し、@parsed_paramsに保存 def parse_xml xml = REXML::Document.new(request.raw_post) @parsed_params = { :author => xml.elements['entry/author/name'].text, :comment => xml.elements['entry/content'].text, :meaning_id => @meaning.id, :updated => Time.now, } end end
今回作成したViewのソースコード
app/views/namedwords/feed.rxml
xml.instruct!(:xml, :version => 1.0, :encoding => 'UTF-8') xml.feed(:language => 'ja', 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:app' => 'http://www.w3.org/2007/app') do |feed| feed.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/namedwords') feed.title('「'+@meaning.name+'」の訳語候補一覧') feed.author do |author| author.name('nadukete.name') end feed.updated((@namedwords.size > 0) ? @namedwords.map{|namedword| namedword.updated}.max.localtime.iso8601 : Time.now.localtime.iso8601 ) feed.link(:rel => 'self', :href => namedwords_url) @namedwords.each do |namedword| feed.entry do |entry| entry.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/namedwords/'+namedword.id.to_s) entry.updated(namedword.updated.localtime.iso8601) entry.author do |author| author.name(namedword.author) end entry.title(namedword.name) entry.content(namedword.comment, :type => 'text') entry.link(:rel => 'edit', :href => namedword_url(@meaning.name, namedword.name)) end end end
app/views/namedwords/entry.rxml
xml.instruct!(:xml, :version => 1.0, :encoding => 'UTF-8') xml.entry(:language => 'ja', 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:app' => 'http://www.w3.org/2007/app') do |entry| entry.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/namedwords/'+@namedword.id.to_s) entry.updated(@namedword.updated.localtime.iso8601) entry.author do |author| author.name(@namedword.author) end entry.title(@namedword.name) entry.content(@namedword.comment, :type => 'text') entry.link(:rel => 'edit', :href => namedword_url(@meaning.name, @namedword.name)) end
app/views/meaningscomments/feed.rxml
xml.instruct!(:xml, :version => 1.0, :encoding => 'UTF-8') xml.feed(:language => 'ja', 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:app' => 'http://www.w3.org/2007/app') do |feed| feed.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/comments') feed.title('「'+@meaning.name+'」のコメント一覧') feed.author do |author| author.name('nadukete.name') end feed.updated((@meaningscomments.size > 0) ? @meaningscomments.map{|meaningscomment| meaningscomment.updated}.max.localtime.iso8601 : Time.now.localtime.iso8601 ) feed.link(:rel => 'self', :href => meanings_comments_url) @meaningscomments.each do |meaningscomment| feed.entry do |entry| entry.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/comments/'+meaningscomment.id.to_s) entry.updated(meaningscomment.updated.localtime.iso8601) entry.author do |author| author.name(meaningscomment.author) end entry.title('コメントID:'+meaningscomment.id.to_s) entry.content(meaningscomment.comment, :type => 'text') entry.link(:rel => 'edit', :href => meanings_comment_url(@meaning.name, meaningscomment.id.to_s)) end end end
app/views/meaningscomments/entry.rxml
xml.instruct!(:xml, :version => 1.0, :encoding => 'UTF-8') xml.entry(:language => 'ja', 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:app' => 'http://www.w3.org/2007/app') do |entry| entry.id('tag:tom@nadukete.name,2008:meanings/'+@meaning.id.to_s+'/comments/'+@meaningscomment.id.to_s) entry.updated(@meaningscomment.updated.localtime.iso8601) entry.author do |author| author.name(@meaningscomment.author) end entry.title('コメントID:'+@meaningscomment.id.to_s) entry.content(@meaningscomment.comment, :type => 'text') entry.link(:rel => 'edit', :href => meanings_comment_url(@meaning.name, @meaningscomment.id.to_s)) end
*1:書き換えて作ればOKというのは裏を返せば"DRYではない"という事なので、似通っている部分は外出しして、もうちょっとスマートに書けるかもしれないですね…。