はじめに
実用的なWebアプリを作ると、同じような画面がたくさん必要になってきます。特にバックエンド側の管理画面は、テーブルに対するCRUD機能を少しだけ拡張したような画面がたくさん必要になります。
このような場合にRuby on RailsのScaffold機能はとても有用です。たしかにRuby on Railsが最初から用意しているScaffoldが作るアプリでは、機能が足りなかったり冗長だったりします。しかし、RailsではScaffoldのようなコードジェネレータを簡単に自作できる仕組みを持っています。
今回はこの機能を使って、前回作ったリッチクライアント版のCRUDアプリを作るcurl_scaffoldを作っていきます。これはテーブル定義を与えると、Scaffoldのようにアプリケーションのコードを生成するプログラムです。
これまでの記事
サンプルソースについて
こちらよりサンプルソースをダウンロードできます。なお、サンプルソース実行にあたってはライセンスファイル「curl-license-5.dat」が必要になります。Curl IDEなどに含まれていますので、コピーして使用してください。
curl_scaffoldのイメージ
今回作るcurl_scaffoldを実行した際の流れは、次のようなイメージです。
1
Ruby on RailsのScaffoldでアプリを作るのと同じように、まずrailsコマンドでプロジェクトを作ります。
% rails alih % cd alih
2
script/generateコマンドでcurl_scaffoldジェネレーターを使ってアプリケーションのひな形を生成します。その際、scaffoldと同様にモデル名、RDBテーブルのカラム情報を指定します。
% ruby script/generate curl_scaffold player name:string team:string no:integer goal:integer assist:integer
3
rakeコマンドでDBを作成、サーバを起動します。
% rake db:migrate % ruby script/server
4
ブラウザで http://localhost:3000/players/start をアクセスすると前回と同じようなCurlの画面が表示されます。
もちろん、上の手順で作ったアプリにはデータがありません。前回のplayers.ymlファイルをtest/fixturesディレクトリにコピーし、以下のコマンドを実行することで前回同様のデータが入ります。
% rake db:fixtures:load
Ruby on RailsのScaffoldの仕組み
Scaffoldをはじめ、Ruby on Railsに組み込まれているコードジェネレータについて詳しく書かれた日本語の情報は少ないです。英語のドキュメントとしてはAPIドキュメントのModule Rails::Generatorが参考になりますが、やはりRuby on Railsのソースコードを読むのが一番分かりやすいです。
ScaffoldジェネレータのソースはRubyがC:¥Rrubyにインストールされている場合、C:¥Ruby¥lib¥ruby¥gems¥1.8¥gems¥rails-2.3.2¥lib¥rails_generator¥generators¥components¥scaffold¥scaffold_generator.rbになります。このファイルと同じディレクトリにあるtemplatesディレクトリの下に、生成するコードの元になるファイルが置かれています。

テンプレート
templatesディレクトリにあるview_show.html.erbを見てみましょう
<% for attribute in attributes -%> <p> <b><%= attribute.column.human_name %>:</b> <%%=h @<%= singular_name %>.<%= attribute.name %> %> </p> <% end -%> <%%= link_to 'Edit', edit_<%= singular_name %>_path(@<%= singular_name %>) %> | <%%= link_to 'Back', <%= plural_name %>_path %>
これは詳細表示用のテンプレートで第1回の例ではapp/views/players/show.html.erbファイルがこのテンプレートから作られています。app/views/players/show.html.erbの一部を見てみましょう。
<p> <b>Name:</b> <%=h @player.name %> </p> <p> <b>Team:</b> <%=h @player.team %> </p> ・・・ 省略 ・・・ <b>Assist:</b> <%=h @player.assist %> </p> <%= link_to 'Edit', edit_player_path(@player) %> | <%= link_to 'Back', players_path %>
view_show.html.erb内の<%% ~ %>タグはそのまま<% ~ %>タグに置き換わっています。ただし<% ~ %>の内容が評価されてテンプレートを展開しています。またplural_name、singular_name、attributesなどの変数が使われているのが分かります。
Scaffoldジェネレータ
Scaffoldジェネレータ本体scaffold_generator.rbを上から見てみましょう。ScaffoldGeneratorクラスは、ジェネレータの元になるRails::Generator::NamedBaseクラスを継承しています。また、ジェネレータで使われるコントローラ名、そのファイル名などの属性(インスタンス変数)が定義されています。
class ScaffoldGenerator < Rails::Generator::NamedBase default_options :skip_timestamps => false, :skip_migration => false, :force_plural => false attr_reader :controller_name, :controller_class_path, :controller_file_path, ・・・ 省略 ・・・
初期化メソッドinitializeでは、ジェネレータで使われるコントローラ名、そのファイル名などの設定が行われます。
def initialize(runtime_args, runtime_options = {}) super if @name == @name.pluralize && !options[:force_plural] logger.warning "Plural version of the model detected, using singularized version. Override with --force-plural." @name = @name.singularize end @controller_name = @name.pluralize base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name) ・・・ 省略 ・・・ end
manifestメソッドがジェネレータのメインです。ここでは、m.class_collisions()でこれから作成するファイルがダブらないかチェックし、m.directory()で必要なディレクトリーを作成し、m.template()でテンプレートを元に必要なファイルを作成しています。
また、m.route_resourcesのようにconfig/route.rbを変更する専用のメソッドなどもあります。さらに、Scaffoldではモデルの作成部分はm.dependency 'model'……でmodelジェネレータを呼び出しています。
def manifest record do |m| # Check for class naming collisions. m.class_collisions("#{controller_class_name}Controller", "#{controller_class_name}Helper") m.class_collisions(class_name) # Controller, helper, views, test and stylesheets directories. m.directory(File.join('app/models', class_path)) m.directory(File.join('app/controllers', controller_class_path)) m.directory(File.join('app/helpers', controller_class_path)) ・・・ 省略 ・・・ for action in scaffold_views m.template( "view_#{action}.html.erb", File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.erb") ) end # Layout and stylesheet. m.template('layout.html.erb', File.join('app/views/layouts', controller_class_path, "#{controller_file_name}.html.erb")) m.template('style.css', 'public/stylesheets/scaffold.css') m.template( 'controller.rb', File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") ) ・・・ 省略 ・・・ m.route_resources controller_file_name m.dependency 'model', [name] + @args, :collision => :skip end end
curl_scaffoldジェネレータを作る
Ruby on Railsのジェネレータは、前ページで説明したRailsのコード自身以外に、プロジェクトのlibディレクトリやホームデイレクトリ下の.rails/generatorsデイレクトリにも作れます。
今回はホームデイレクトリ下にcurl_scaffoldデイレクトリを作り、そこにジェネレータを作ることにします。Windowsの場合はC:¥Documents and Settings¥Owner¥.rails¥generators¥curl_scaffoldなどになると思います。
curl_scaffoldジェネレータのファイル構成は以下のようになります。各ファイルについてはこれから説明します。

curl_scaffold用start.html.erbテンプレート
前回作ったリッチクライアント版のCRUDアプリのメインはapp/view/player/start.html.erbです。このファイルをジェネレータ用のテンプレートに変換することがcurl_scaffold作成の中での大きな仕事です。
それでは、順番にテンプレート化していきましょう。
属性(カラム)の置き換え
start.html.erbの中には、playersテーブルのカラムに対応した記述が多数あります。下のコードではRecordFieldの部分が各カラムに対応したフィールドの定義です。
{curl 7.0 applet} {curl-file-attributes character-encoding = "utf8"} {import * from CURL.IO.JSON} {let players: RecordSet = {RecordSet {RecordFields {RecordField "id", caption = "id", domain = int} , {RecordField "name", caption = "name", domain = String} , {RecordField "team", caption = "team", domain = String} , {RecordField "no", caption = "no", domain = int} , {RecordField "goal", caption = "goal", domain = int} , {RecordField "assist", caption = "assist", domain = int} } } }
下が対応するview_start.html.erbテンプレートです。
{curl 7.0 applet} {curl-file-attributes character-encoding = "utf8"} {import * from CURL.IO.JSON} {let <%= plural_name %>: RecordSet = {RecordSet {RecordFields {RecordField "id", caption = "id", domain = int} , <% for attribute in attributes -%> {RecordField "<%= attribute.name %>", caption = "<%= attribute.name %>", domain = <%= attribute.curl_type %>} <%= attribute.last ? '' : ',' %> <% end -%> } } }
Scaffoldジェネレータではこのカラム情報はattributesという属性(GeneratedAttributeクラスのインスタンス)の配列に入っており、nameには名前、curl_typeには型の情報が入っています。さらにlastはattributes配列の最後の要素のみがtrueになります。そのため","区切りを最後以外に付けています。
また、playersテーブルに対応した複数形の名前はplural_nameに、単数形の名前はsingular_nameに入っているので、テーブル名に対応した変数名や関数名などを<%= ~ %>で置き換えていきます。
汎用化
前回のstart.html.erbではカラムの表示幅などはデータの大きさによりきめ細かく指定していましたが、
{let data_list: RecordGrid = {RecordGrid width = 16cm, height = 6cm, record-source = players, {RecordGridColumn "id", width = 0}, {RecordGridColumn "name", width = 5cm}, {RecordGridColumn "team", width = 2cm}, {RecordGridColumn "no", width = 2cm}, {RecordGridColumn "goal", width = 3cm, halign = "right"}, {RecordGridColumn "assist", width = 3cm, halign = "right"} } }
ジェネレータでは、このような細かい指定は無理なので無難な形で統一してしまいました。
{let data_list: RecordGrid = {RecordGrid width = <%= attributes.size * 3 %>cm, height = 6cm, record-source = <%= plural_name %>, {RecordGridColumn "id", width = 0}, <% for attribute in attributes -%> {RecordGridColumn "<%= attribute.name %>", width = 3cm} <%= attribute.last ? '' : ',' %> <% end -%> } }
その他のcurl_scaffold用テンプレートファイル
curl_scaffoldジェネレータが作成するアプリはHTML用のテンプレートや処理はいりませんので、curl_scaffoldに必要な最低限のファイルやコードのみ作ることにします。
コントローラ
コントローラはHTML・XMLページの表示や、HTMLアプリでしか必要のないnew・editメソッドを省略しました。
また、CSRF対策用のチェックverify_authenticity_token処理をskip_before_filterで指定して行わないようにしました。これでapp/controllers/application_controller.rb内のprotect_from_forgeryを変更しなくてもよくなります。
class <%= controller_class_name %>Controller < ApplicationController skip_before_filter :verify_authenticity_token def start render :layout => false, :content_type => 'text/vnd.curl' end def index @<%= table_name %> = <%= class_name %>.all respond_to do |format| format.json { render :json => @<%= table_name %> } end end def show @<%= file_name %> = <%= class_name %>.find(params[:id]) respond_to do |format| format.json { render :json => @<%= file_name %> } end end def create @<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>]) respond_to do |format| if @<%= file_name %>.save format.json { render :json => @<%= file_name %>, :status => :created, :location => @<%= file_name %> } else format.json { render :json => @<%= file_name %>.errors, :status => :unprocessable_entity } end end end def update @<%= file_name %> = <%= class_name %>.find(params[:id]) respond_to do |format| if @<%= file_name %>.update_attributes(params[:<%= file_name %>]) format.json { head :ok } else format.json { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity } end end end def destroy @<%= file_name %> = <%= class_name %>.find(params[:id]) @<%= file_name %>.destroy respond_to do |format| format.json { head :ok } end end end
JSON出力形式の指定
前回は、JSON出力形式の指定ActiveRecord::Base.include_root_in_json = falseをconfig/environment.rbに書きましたが、今回は専用の初期化ファイルを作ります。Ruby on Railsではconfig/initializersディレクトリにあるファイルはサーバ起動時に実行されるので、ここにsetup_json.rbというファイルで指定します。
ActiveRecord::Base.include_root_in_json = false
なお、config/initializersディレクトリ下のファイルはアルファベット順に実行されるので、複数のファイルで同じ項目の設定を行う場合はファイル名が重要になります。今回のJSON出力形式の指定はnew_rails_defaults.rbでも指定されているので"n"より後ろのファイル名にしました。
ライセンスファイル
Curlのライセンスファイルの設置もcurl_scaffoldジェネレータで行うことにします。
curl_scaffoldジェネレータのコード
いよいよcurl_scaffoldジェネレータのコードcurl_scaffold_generator.rbです。コードはscaffold_generator.rbをコピーして作りました。コードを順に見ていきましょう。
manifestメソッド
curl_scaffoldでは通常のscaffoldに比べ生成するファイルを減らしたので、manifestメソッドはだいぶシンプルになっています。
ファイル名の衝突チェック、ディレクトリ作成に次いで行われる、テンプレートからのアプリケーションファイル作成はview_start.html.erbとコントローラのみです。後は初期化ファイルsetup_json.rb、ライセンスファイルのコピーを行っています。
最後にconfig/route.rbの作成とモデルの作成を行います。
class CurlScaffoldGenerator < Rails::Generator::NamedBase ・・・scaffold_generator.rbと同じ ・・・ def def initialize(runtime_args, runtime_options = {}) ・・・scaffold_generator.rbと同じ ・・・ end def manifest record do |m| # Check for class naming collisions. m.class_collisions"#{controller_class_name}Controller" m.class_collisions class_name # Controller, helper, views, test and stylesheets directories. m.directory File.join('app/models', class_path) m.directory File.join('app/controllers', controller_class_path) m.directory File.join('app/views', controller_class_path, controller_file_name) m.directory File.join('test/unit', class_path) m.template "view_start.html.erb", File.join('app/views', controller_class_path, controller_file_name, "start.html.erb") m.template 'controller.rb', File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") m.file 'setup_json.rb', 'config/initializers/setup_json.rb' m.file 'curl-access.txt', 'public/curl-access.txt' m.file 'curl-license-5.dat', 'public/curl-license-5.dat' m.route_resource ":#{controller_file_name}, :collection => { :start => :get }" m.dependency 'model', [name] + @args, :collision => :skip end
Curl用属性クラスの作成
コマンドで指定されたテーブルのカラム情報を保持する属性(GeneratedAttribute)クラスですが、型やデフォルト値の情報はRuby on Rails用なのでCurl用の型やデフォルト値を戻してくれるGeneratedCurlAttributeクラスを定義しました。またview_start.html.erbテンプレートの処理で属性配列の最後が分かると便利なので、最終要素を表すlast属性も追加しました。
class CurlScaffoldGenerator < Rails::Generator::NamedBase ・・・省略・・・ protected def attributes unless (@attributes) @attributes = @args.collect do |attribute| GeneratedCurlAttribute.new(*attribute.split(":")) end @attributes[-1].last = true end @attributes end end class GeneratedCurlAttribute attr_accessor :name, :type, :column, :last def initialize(name, type) @name, @type = name, type.to_sym @column = ActiveRecord::ConnectionAdapters::Column.new(name, nil, @type) @last = false end def curl_type @curl_type ||= case type when :integer then "int" when :string then "String" else raise "Not supported type in curl_scaffold." end end def default @default ||= case type when :integer then 0 when :string then "" else raise "Not supported type in curl_scaffold." end end end
config/route.rb設定メソッドの作成
config/route.rbに書かれるルーティング情報の追加メソッドroute_resourcesですが、Scaffold用に作られたものは:collection => { :start => :get }等の情報を付加できないので、やはり専用のメソッドroute_resourceに置き換えています。
module Rails module Generator module Commands class Create < Base def route_resource(resource) sentinel = 'ActionController::Routing::Routes.draw do |map|' logger.route "map.resources #{resource}" unless options[:pretend] gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match| "#{match}\n map.resources #{resource}\n" end end end end end end end
まとめ
今回のようにRuby on Railsに独自のコードジェネレータを作る事で、サーバ側のRuby on Railsのプログラムだけではなく、Curlコードの作成も飛躍的な生産性の向上がはかれる事を感じていただけたら幸いです。