はじめに
Ruby言語とRuby on Rails(略してRails)Webアプリケーションフレームワークは、ソフトウェアプログラミング業界に一大旋風を巻き起こしました。Rubyが登場してから10年以上が経ちますが、Railsフレームワーク自体は比較的新しいものです。しかし、非常に複雑なWebフレームワークの世界で悪戦苦闘している多くのプログラマに大変人気があります。
この記事では、RubyとRailsを一緒に使うと、単純なWikiアプリケーションが簡単に作成できることを実証します。Railsは、データベースアプリケーションの作成に利用されるのが一般的ですが、この記事では、ファイルベースの永続化を説明します。というのは、私は個人的にファイルベースのWikiシステムの方が好きで、かつ、こちらの方がセットアップも簡単だからです。また、Railsを使ったデータベース以外のアプリケーション開発に関する情報はWeb上であまり公開されていないから、という理由もあります。なお、データベースへの切り替えは、Railsのデータベースサポートを使って手順どおりに行えば比較的簡単にできます。
必要なもの
この記事の内容を理解するには、Ruby言語とRailsフレームワークに関する実用的な知識が必要です。そのどちらも初めての方は、既に公開されている次のDevXの記事をお読みください。
さらに興味のある方は、本稿の最後の「参考資料」で紹介しているリンクも参照してください。
サンプルのwikiを作成して実行するには、中核となる次の3つのソフトウェアをインストールする必要があります。
- Rubyインタプリタ
- RubyGemsパッケージングシステム
- Railsフレームワーク
また、テキストマークアップエンジンとしてRedClothというRuby gemが必要です。これらのソフトウェアはすべて以下の場所にあります。バージョンはこの記事で使用したバージョンです(注: これらのソフトウェアはリスト順どおりにインストールしてください)。
- Rubyインタプリタ(バージョン1.8.5)
- RubyGemsパッケージングシステム(バージョン0.9.2)
- Gems:
以上すべてのソフトウェアをインストールしたら、次のコマンドを実行して構成をテストしてください。
- ruby -help
- gem --help(あるいはgem list --local)
- rails -help
本稿のWikiシステムの機能
コードを確認する前に、本稿で作成するWikiシステム(名前は「RailsWiki」とします)の実装に必要な機能を検討してみましょう。一般的なWikiシステムでは、Webを介してコンテンツを簡単に追加、編集、削除できます。そこでRailsWikiでも、最低、次の操作ができるようにします。
- Wikiドキュメントを新規作成する
- 既存のWikiドキュメントを編集する(テキストマークアップ言語によるコンテンツの書式設定を可能にする)
- 既存のWikiドキュメントを表示する(開く)
- 既存のWikiドキュメントを印刷する
- 既存のWikiドキュメントを削除する
- 既に作成されたWikiドキュメントをすべてリストする(表示または削除目的)
次に、これら高いレベルの機能要求(いわゆるユーザーストーリー)を実装するUIサンプル(モックアップ)を検討してみましょう。図1、図2、図3、図4はそれぞれ、ウェルカムページ、表示機能、編集機能、印刷機能のスクリーンショットです。RailsWikiの実装に必要な機能は、この4つの図にほぼ反映されています。
システムの設計
UIのレイアウトが決定したところで、次は、より技術的な面(アプリケーションの基本設計など)を検討してみましょう。図5に、RailsWikiアプリケーションの設計図を示します。RailsWikiでは一般的なモデルビューコントローラ(MVC)アーキテクチャを使用しますが、永続化の機能にファイルシステムを使用するという点がやや特殊です。ファイルベースの永続化スキームを採用したのは、前述の通り、できる限り単純なWikiシステムを作成するためです。Wikiの開発者のWard Cunningham氏も、Wikiのコンセプトは「必要最低限で動くシンプルさを追求すること」と言っています。
アプリケーションのコーディング
ソフトウェアを適切にインストールしたら、RubyとRailsを使ってコーディングを開始します。
Railsのすばらしさの1つは、新しくWebアプリケーションを開発するための叩き台となる数多くのコード、ディレクトリ、ファイルを生成してくれることです。RailsWikiのファイルを生成するには、まずこのアプリケーションの親ディレクトリとサブディレクトリを作成しようと考えているトップレベルディレクトリ(例: /users/anil/dev/、c:anildev)から、次のようなrailsコマンドを実行します。
railsコマンドが適切に実行されると、多数のディレクトリとファイルが作成されます。この記事で使用するディレクトリの一部を以下に示します。
- app/controllers/
- app/models/
- app/views/
- app/helpers/
- public/stylesheets/
- test/unit/
- config/
- script/
コマンドとディレクトリの相対パス名について注記
以降のコマンドとディレクトリには、RailsWikiアプリケーションのトップレベルのディレクトリからの相対パスを示します。例えば、この場合のapp/views/は、c:anildev ailswikiappviewsなどを指していると考えてください。
コントローラとビューのスタブファイルの生成
最初に作成(正確には「生成」)するのは、コントローラやそれぞれのビューに関するファイルです。Railsアプリケーションのトップレベルのディレクトリから、次のコマンドを入力して実行します。
ruby script/generate controller Wiki index view edit print help
generateコマンドを実行すると、いくつかのファイルが作成されます。その一部を以下に示します。
- app/controllers/wiki_controller.rb
- app/views/layouts/application.rhtml
- app/views/wiki/edit.rhtml
- app/views/wiki/help.rhtml
- app/views/wiki/index.rhtml
- app/views/wiki/print.rhtml
- app/views/wiki/view.rhtml
この時点で、動作するWebアプリケーションが既にできています。このスタブアプリケーションをテストするには、次のコマンドを実行して、組み込みのWEBrick HTTPサーバーを起動します。
Webサーバーが起動したら、Webブラウザで「http://localhost:3000/wiki/」を開きます。図6のような画面が表示されるはずです。
ここまでの作業で生成したコントロールとビューのスタブファイルにコードを追加する前に、まず、Wikiエンジンとして機能するモデルクラスを作成します。
"モデル"クラスを開発する
Railsは、通常はバックエンドにデータベースを配置したWebアプリケーションの開発に使われるため、モデルとその関連ファイルを生成するときには、script/generateコマンドを使用するのが一般的です。しかし、今回のサンプルではファイルベースの永続化を使用するので、できる限りRailsの規則に従ってモデルクラスをゼロから作成します(リスト1を参照)。ここでは、app/modelsディレクトリにWikiモデルクラスを配置します(app/models/wiki.rb)。Railsでは、通常、生成されたモデルファイルはこの場所に配置されます。なお、メソッドの命名規則は、Railsから提供される
ActiveRecord::Baseクラスに倣っています。リスト1に、完全なWikiモデルクラスを示します。
リスト1 Wikiモデルクラス
require ’find’
# This is the core/manager class for the Railswiki application
# Author: Anil Hemrajani
# Date: Feb, 2007
class Wiki
@@basedir = "."
@@extension = "wiki"
def self.basedir(basedir)
@@basedir = basedir
end
# check if file exists
def self.exists?(basefilename)
File.file?(getfullpath(basefilename))
end
# get file stat
def self.attributes(basefilename)
File.stat(getfullpath(basefilename))
end
# delete existing wiki file
def self.delete(basefilename)
File.delete(getfullpath(basefilename))
end
def self.file_extension
@@extension
end
# save raw wiki file contents
def self.save(basefilename, content)
mkdir
file = File.open(getfullpath(basefilename), ’w’)
begin
file.print content
ensure
file.close
end
end
# return array of wiki file names in basedir
# get raw wiki file contents
def self.find(basefilename)
#unless (FileTest.file?(basefilename)) return "File not found."
getline = ""
File.open(getfullpath(basefilename), ’r’) do |f1|
while line = f1.gets
getline = getline + line
end
end
getline
end
# Get list of file names ending with @@extension
def self.find_wikis()
files = []
Find.find(@@basedir) do |path|
if File.file?(path) && path =~ /.#{@@extension}$/
files << File.basename(path, ".#{@@extension}")
end
end
files
end
private
def self.mkdir()
Dir.mkdir(@@basedir) if !File.exist?(@@basedir)
end
def self.getfullpath(basefilename)
@@basedir + "/" + getfullname(basefilename)
end
def self.getfullname(basefilename)
basefilename + "." + @@extension
end
end
このクラスで注目すべきメソッドは、
basedir、
find、
find_wikis、
saveです。これらのメソッドについて詳しく見てみましょう。
- basedirメソッド
すべてのWikiファイルを格納する静的なディレクトリ(ベースディレクトリ)の名前を設定できます。このメソッドを呼び出さない場合は、クラス変数@@basedirに指定されているように、デフォルトでカレントディレクトリが使用されます。
- findメソッド
Wikiファイル全体を読み込み、呼び出し元に文字列として返します(ファイルを読み込むのにfindというメソッド名は多少違和感があるかもしれませんが、Railsの命名規則に従ってこの名前を付けました。Railsの命名規則に従うことで、データベースの永続化モデルに切り替えるときも、Railsモデルのサポートを使って簡単に対応できます)。
- find_wikisメソッド
ベースディレクトリ内でクラス変数@@extensionの値で終わるファイル名(この場合はデフォルトで".Wiki"に設定)を検索し、見つかったファイル名の配列を返します。
- saveメソッド
ファイルを開き、メソッドパラメータcontentのすべての内容をそのファイルに書き込みます。
その他の注目すべきメソッドは、
delete、
exists?、
attributesです。
deleteメソッドはWikiファイルを削除します。
exists?メソッドはWikiファイルが存在するかどうかを示します。
attributesメソッドはファイルの属性(更新日やサイズなど)を取得します。
モデルのコードの単体テスト
モデルのコードをコントローラクラスに挿入する前に、すべてのメソッドが期待どおりに動作するかを確認するために単体テストを実施します。通常、単体テストはメソッドを個別にテストする小規模なものが望ましいのですが、ここでは、複数のテストを単体テストファイル「test/unit/wiki_test.rb」内の
test_wikiという1つのメソッドにまとめて実施します。
test_wikiメソッドでは、Wikiモデルクラスの
save、
find、
delete、
exists?の各メソッドをテストします。以下に「wiki_test.rb」ファイルから抜粋したコードを示します。
# Ensure file doesn’t exist (yet)
assert !File.exists?(getfullpath(@filename))
# Test save and find
Wiki.save @filename, DEFAULT_TEXT
assert File.exists?(getfullpath(@filename))
text = Wiki.find(@filename)
assert_equal DEFAULT_TEXT, text
# Test delete
Wiki.delete @filename
# Test exists?
assert !Wiki.exists?(@filename)
ここで、データベースを使用しないRailsアプリケーションをテストするときに発生するいくつかの問題と、その解決方法について考える必要があります。通常、モデルクラスの単体テストは、Railsアプリケーションのトップレベルのディレクトリからrake test:unitsコマンド(以前のバージョンではrake test_units)を実行して開始します。このときRailsはデフォルトでデータベースに接続しようとしますが、RailsWikiアプリケーションはデータベースを使用しないので、この部分をうまく機能させるために2つの変更が必要です(詳細は、Jay Fieldsのブログ記事
「Ruby on Rails Unit Tests」を参照)。
まず、lib/tasksの下に拡張子.rakeで終わるファイルを配置する必要があります。これには、本稿のダウンロードファイルに含まれている「testing.rake」というファイルを使用します。このファイルには、次のRubyコードが含まれています。
Rake::Task[:’test:units’].prerequisites.clear
次に、「test/test_helper.rb」ファイルのコードを次のように変更します。
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__)
+ "/../config/environment")
require ’application’
require ’test/unit’
require ’action_controller/test_process’
require ’breakpoint’
ここでrake test:unitsコマンドを実行すると、次のような出力が生成されます。
Started
.
Finished in 0.094 seconds.
1 tests, 5 assertions, 0 failures, 0 errors
モデル、ビュー、コントローラのコードを統合する
Wikiモデルクラスのコーディングとテストが完了したら、モデル、ビュー、コントローラのコードを統合します。リスト2に、
WikiControllerクラスの完全なコードを示します(このクラスは「app/controllers/wiki_controller.rb」にあります)。
リスト2 WikiControllerクラス
# This is the front/main controller for the Wiki application
# Author: Anil Hemrajani
# Date: Feb, 2007
class WikiController < ApplicationController
layout "wiki" , :except => [ :index, :print ]
def initialize
Wiki.basedir WIKI_DIR
end
def index
@filelist = Wiki.find_wikis
end
def edit
get_content
end
def view
get_content
end
def print
get_content
end
def help
@filename = get_filename params[:f] # need this for other actions
end
# create new wiki file
def create
@filename = get_filename params[:f]
if Wiki.exists?(@filename)
flash[:error] = 
"File ’#{@filename}’ already exists; use Open instead of Create."
redirect_to :action => :index
else
Wiki.save @filename, ""
redirect_to :action => :view, :f => @filename
end
end
# delete existing wiki file
def delete
if params[:f].nil? || params[:f].empty?
flash[:error] = "File parameter not specified."
redirect_to :action => :index
else
@filename = get_filename params[:f]
Wiki.delete @filename
redirect_to :action => :index
end
end
# save existing wiki file
def save
@filename = get_filename params[:f]
content = params[:c]
p @filename
p content
Wiki.save @filename, content
redirect_to :action => :view, :f => params[:f]
end
private
def get_filename(f)
f = "untitled" if (f.nil? || f.empty?)
f
end
# load file into @content using param :f; for errors go to index page
def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
end
先ほど、Railsのscript/generate controllerコマンドを実行したとき、いくつかのスタブファイルが生成されたことを思い出してください。ただし今回の例では、生成されたメソッドにコードを追加し、さらにいくつかのメソッドを追加しました。新たに追加したメソッドは、ビューに関連付けられるものではなく、他のビューから利用される特定のアクションとして機能するものです。WikiControllerには、
initialize、
index、
edit、
view、
print、
help、
create、
delete、
saveという
publicメソッドが含まれています。これらのメソッドの一部を詳しく見てみましょう。
- initializeメソッド
Wikiファイルを格納するディレクトリ名を設定します。この名前は、「config/environment.rb」ファイルでWIKI_DIRプロパティを介して構成されます。Wikiファイルを必要に応じて別の適切なディレクトリの下に保存したくなった場合は、この設定だけを変更すれば済みます。
WIKI_DIR = "/tmp/railswiki"
- indexメソッド
Wiki.find_wikisを呼び出して既存のWikiファイル名のリストを生成し、そのリストをインスタンス変数@filelistを介して「index.rhtml」ビューに渡します。このビューは、配列をループ処理し、処理結果をHTMLの<SELECT>ドロップダウンリストに設定します。
<select name="f">
<% @filelist.each do |file| %>
<option><%= file %></option>
<% end %>
</select>
- edit、view、printの各メソッド
単にget_contentという内部(private)メソッドを呼び出すだけです。get_contentは、リクエストされたファイルを読み込み、そのファイルの属性と内容をそれぞれインスタンス変数@filestatと@contentを介して対応するビューに渡します。以下に使用例を示します。
def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
get_contentメソッドは、リクエストされたファイルの内容と属性を読み込む他に、ページをインデックスページにリダイレクトし、例外が発生した場合にはエラーメッセージを表示します。エラーメッセージは、Railsのflash機能(実体はアプリケーションエラーを格納するハッシュ)を使用して設定します。
コントローラがビューにコントロールを渡すと、ビューは単にインスタンス変数
@contentの内容を表示します。例えば、「view.rhtml」テンプレートに含まれているファイル全体として1行しかないコードでは、Wikiファイルの内容を表示するだけでなく、表示する前に内容を
to_htmlというヘルパーメソッドを使用して変換します。
to_htmlヘルパーメソッドは、Railsが生成するヘルパーモジュール「app/helpers/wiki_helper.rb」ファイル内にあります(ヘルパーモジュールはビューで使用するメソッドを作成するのに便利です)。また、このファイルは、未加工のWikiファイルをHTMLに変換する手順を含み、前に説明したRedClothというgemの動作が開始する場所でもあります。以下に、「wiki_helper.rb」に含まれる、このコードの内容を示します。
require ’redcloth’
module WikiHelper
# parse and return data as HTML
def to_html(rawtext)
return "" if rawtext.nil?
r = RedCloth.new rawtext
r.to_html
end
end
その他の注目すべきファイル
生成された「app/views/layouts/application.rhtml」ファイルは、アプリケーションレベルの(つまり全コントローラで使用可能な)一貫性のあるルックアンドフィールを提供し、ヘッダやフッタなどの要素を含んでいます。しかし、コントローラ固有のレイアウトを作成したい場合もあるはずです。そこでこのファイルの名前を「wiki.rhtml」に変更し、特定のWikiControllerにのみ適用されるようにしました。さらに、この「wiki.rhtml」ファイルをカスケーディングスタイルシート(CSS)ファイル「public/stylesheets/wiki.css」にもリンクさせました。これにより、色やフォントなどのアプリケーションの外観をCSSで制御できるようになります。
さらに私が少々カスタマイズしたもう1つの生成ファイルは「config/routes.rb」です。マッピングの1つを変更し、生成された「public/index.html」ファイルを削除することで、行末のURI(/wiki/)を指定せずにインデックスページにアクセスできるようにしました。
map.connect ’’, :controller => "wiki"
これで、http://localhost:3000/のURLは、http://localhost:3000/wiki/と同じものとして処理されます。
また、「config/routes.rb」ファイルに次の行を追加して、WebサービスのURLのように簡潔なREST(Representational State Transfer)を使用できるようにしました。
map.connect ’wiki/:action/:f’, :controller => "wiki"
例えば上の行を追加すると、http://localhost:3000/wiki/view?f=untitledのようなURLは、http://localhost:3000/wiki/view/untitledと表示されます。この種のURLの書式設定は、Railsのヘルパーメソッド(
link_to、
redirect_to、
url_forなど)により自動処理されます。
次のステップ
これで正常に機能するWikiシステムが完成しました。しかし、この基本システムはまだ改良の余地があります。Railsをさらに学習したい方や、本稿のサンプルをとにかく改良してみようと思う方は、以下に挑戦してみてください。
- Railsの機能を活かして、アプリケーションのWikiページをデータベースに永続化できるようにする(実際のWikiテキストにはtextデータ型を使用する)。
- インデックス作成機能と検索機能を提供する既存のRuby gemを使用して検索機能を追加する(例えば、ferret.davebalmain.com/tracで、このような機能を持つFerretというgemを入手可能)。
- WikiWordや変更履歴などの一般的なWiki機能のサポートを追加する。
以上を参考にしながら、RubyとRailsのコーディングをぜひ楽しんでください。
参考資料
20年に及ぶプログラミング歴を持つ。多くの記事や一般読者向けの本を執筆し、業界関連の賞を多数受賞。世界の4大陸で講演を行い、ある有名な開発者コミュニティを創設した経験を持ち、成功企業の経営にも携わる。現在は独立コンサルタントとして活動中。