CodeZine(コードジン)

特集ページ一覧

Ruby on Rails + Curl
Curl用Scaffoldを自作する

第3回

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2009/07/21 15:00

ダウンロード サンプルソース (5.4 KB)

 RailsにはScaffoldという簡単なアプリを自動生成するジェネレータがありますが、Scaffoldはデフォルトで用意されているものだけでなく自作することも可能です。今回は前回作成したCRUDアプリを自動生成するScaffoldを自作します。

はじめに

 実用的なWebアプリを作ると、同じような画面がたくさん必要になってきます。特にバックエンド側の管理画面は、テーブルに対するCRUD機能を少しだけ拡張したような画面がたくさん必要になります。

 このような場合にRuby on RailsのScaffold機能はとても有用です。たしかにRuby on Railsが最初から用意しているScaffoldが作るアプリでは、機能が足りなかったり冗長だったりします。しかし、RailsではScaffoldのようなコードジェネレータを簡単に自作できる仕組みを持っています。

 今回はこの機能を使って、前回作ったリッチクライアント版のCRUDアプリを作るcurl_scaffoldを作っていきます。これはテーブル定義を与えると、Scaffoldのようにアプリケーションのコードを生成するプログラムです。

これまでの記事

  1. Railsの表示にCurlを使ってRIAを構築する
  2. リッチクライアントCRUDアプリを作成する

サンプルソースについて

 こちらよりサンプルソースをダウンロードできます。なお、サンプルソース実行にあたってはライセンスファイル「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コードの作成も飛躍的な生産性の向上がはかれる事を感じていただけたら幸いです。

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 吉田裕美(ヨシダユウミ)

    有限会社 EY-Office 取締役 CADのベンチャー企業でCADのコア部分や図面管理システムなどの開発に従事した後、独立しJava,Ruby,PerlでWebアプリを中心に開発してきた。現在は殆どの開発はRuby on Rails。 ここ数年はソフトウェアエンジニアの教育に興味をもち、従来の...

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5