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

所感など

RubyRailsもちゃんと触るのは今回が初めてで、とにかく分からない事だらけで(^^;、以下のサイト辺りを参考にさせて頂きました。

なので、ソースコードが似てるところもあるかもしれませんが、ご了承の程を…。

Railsは"RESTfulなフレームワーク"を謳っているだけあって、一つのリソースをポンと作るだけなら、scaffoldした後にちょこっと書き換えるぐらいで作れてしまうのは良いですね。

また、"AtomPubサーバとして実装する"と決めてから作り始めた分、表現形式とかアクセス方法とかは特に考える必要が無くて、そういう意味では、"AtomPubで楽になる"という部分もあるかなとは感じました。(そうは言っても、ちゃんと動くようになるまで、色々と苦労はありましたが…。(^^;ゞ)

次回は?

次回は、今回作ったサービスに実際にアクセスして、動作確認するところをやってみたいと思います。