AtomPubな何かを作ってみよう その2 〜実装編 Part.01「単体のAtomPubリソースの作成」〜
前回の記事でリソース設計をやってみた訳ですが、今回はその中から、意味語のリソース(/meanings)をRailsで実際に作ってみたいと思います。
やたらとソースコードが多くて長い記事になってしまったので、今回は実装するところまでで、実際に使ってみるのは次回に回そうと思います。
まずは準備
今回使った環境は以下の通り。
- WindowsXP
- Ruby1.8.6
- Rails2.1.2
- PostgreSQL8.2
それぞれのインストール作業についてはここでは省略します。
仮でもいいからプロジェクト名を決めてから始めた方が何かと便利なので、まず始めにプロジェクト名を決めておきます。今回は"naduketename(名付けてねーむ)"というプロジェクト名にしました。ネーミングセンスについてのツッコミは受け付けません。(笑) 一応、"名付けてね♪"っていう軽いノリな感じにしているのと、今時点で"nadukete.name"ドメインが空いていたようなので、これにしています。(ドメイン取得までは、まだしていませんが…。)
という訳で、早速Railsアプリケーションを作成します。
> cd path_to_projects > rails -d postgresql naduketename
続いて、接続先のDBを準備します。
> createdb -E utf8 naduketename_development > createdb -E utf8 naduketename_production > createdb -E utf8 naduketename_test
この後はscaffoldで各ファイルを作成してから書き換えていってもいいんですが、今回はベタで全部書いていく事にします。
まず、"config/database.yml"を書き換えます。
development: adapter: postgresql encoding: unicode database: naduketename_development username: user password: pass
"production"と"test"についても同様に記述しておきます。
次に、"db/migrate/001_initial_schema.rb"というファイルを以下の内容で作成します。
class InitialSchema < ActiveRecord::Migration def self.up create_table :meanings, :force => true do |t| t.column :name, :string t.column :author, :string t.column :comment, :string t.column :updated, :timestamp end add_index :meanings, :name end def self.down [:meanings].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 => '' end
Modelを作る
"app/models/meaning.rb"を以下の内容で作成します。
class Meaning < ActiveRecord::Base end
ホントは色々と設定すべきなのかもしれませんが、面倒なので省略…。
Controllerを作る
"app/controllers/meanings_controller.rb"を以下の内容で作成します。
class MeaningsController < ApplicationController # GET /meanings def index @meanings = Meaning.find(:all, :order => 'updated desc') headers['Content-Type'] = 'application/atom+xml;type=feed' render :action => 'feed' end # POST /meanings def create parse_xml name = @parsed_params[:name] @meaning = Meaning.find_by_name(name) if @meaning headers['Location'] = meaning_url(@meaning.name) render :nothing => true, :status => '409 Conflict' else @meaning = Meaning.new(@parsed_params) if @meaning.save headers['Content-Type'] = 'application/atom+xml;type=entry' headers['Location'] = meaning_url(@parsed_params[:name]) render :action => 'entry', :status => '201 Created' else render :xml => meaning.errors.to_xml, :status => '400 Bad Request' end end end # GET /meanings/{meaning} def show @meaning = Meaning.find_by_name(params[:id]) if @meaning headers['Content-Type'] = 'application/atom+xml;type=entry' render :action => 'entry' else render :status => 404, :text => '指定したエントリは存在しません' end end # PUT /meanings/{meaning} def update parse_xml @meaning = Meaning.find_by_name(params[:id]) if @meaning @meaning.update_attributes(@parsed_params) if @meaning.save headers['Content-Type'] = 'application/atom+xml;type=entry' headers['Location'] = meaning_url(params[:id]) render :action => 'entry', :status => '200 OK' else render :xml => @meaning.errors.to_xml, :status => '400 Bad Request' end end end # DELETE /meanings/{meaning} def destroy @meaning = Meaning.find_by_name(params[:id]) if @meaning @meaning.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, :updated => Time.now, } end end
この辺りまで来るとあんまり自信が無いので、色々と足りてなかったりおかしかったりする箇所があるかもしれませんが(^^;ゞ、そこら辺はツッコミ待ちです…。
(追記 2008/12/19 16:30)動作確認していて、POST/PUT後のレスポンスヘッダのContent-Typeがおかしかった事に気が付いて、一部コード修正しました。
Viewを作る
"app/views/meanings/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') feed.title('単語一覧') feed.author do |author| author.name('nadukete.name') end feed.updated((@meanings.size > 0) ? @meanings.map{|meaning| meaning.updated}.max.localtime.iso8601 : Time.now.localtime.iso8601 ) feed.link(:rel => 'self', :href => meanings_url) @meanings.each do |meaning| feed.entry do |entry| entry.id('tag:tom@nadukete.name,2008:meanings/'+meaning.id.to_s) entry.updated(meaning.updated.localtime.iso8601) entry.author do |author| author.name(meaning.author) end entry.title(meaning.name) entry.content(meaning.comment, :type => 'text') entry.link(:rel => 'edit', :href => meaning_url(meaning.name)) end end end
"app/views/meanings/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) entry.updated(@meaning.updated.localtime.iso8601) entry.author do |author| author.name(@meaning.author) end entry.title(@meaning.name) entry.content(@meaning.comment, :type => 'text') entry.link(:rel => 'edit', :href => meaning_url(@meaning.name)) end
元々、AtomPubで扱うのが適当かどうか微妙なリソースというのもあって、一部、要素の当てはめ方が若干無理矢理なところがあるかもしれないですが…まぁ、気にしない事にします。
日本語出力が文字参照になる問題の回避対応を行う
ここまででとりあえず動くようにはなるんですが、実際に動かしてみると、日本語部分がなぜか文字参照に変換されて出力されてしまうという問題がありました。
理由はきちんと追い切れてはいないんですが、ググりまくってみたところ、以下のような対応でいけるようなので、その対処を行います。
"app/controllers/application.rb"に以下の記述をする事で、変換が掛からずに出力されて、日本語もきちんと読めるようになります。
# rxmlで生成されたxmlの日本語が文字参照になるのを回避する class String def to_xs self end end
所感など
RubyもRailsもちゃんと触るのは今回が初めてで、とにかく分からない事だらけで(^^;、以下のサイト辺りを参考にさせて頂きました。
なので、ソースコードが似てるところもあるかもしれませんが、ご了承の程を…。
Railsは"RESTfulなフレームワーク"を謳っているだけあって、一つのリソースをポンと作るだけなら、scaffoldした後にちょこっと書き換えるぐらいで作れてしまうのは良いですね。
また、"AtomPubサーバとして実装する"と決めてから作り始めた分、表現形式とかアクセス方法とかは特に考える必要が無くて、そういう意味では、"AtomPubで楽になる"という部分もあるかなとは感じました。(そうは言っても、ちゃんと動くようになるまで、色々と苦労はありましたが…。(^^;ゞ)
次回は?
次回は、今回作ったサービスに実際にアクセスして、動作確認するところをやってみたいと思います。