Rails3のビューでブロックを伴なうHelperを使う時の注意

globalize3というgemを評価していて、Rails3のおかしな挙動に気がつきました。

ビュー内で、Globalize.with_locale というメソッド使うと、一部の内容がダブって出力されるというものです。

最初、globalize3の問題かと思いましたが、調べてみると、Rails3の問題のようです。ビュー内でブロックを使うHelperメソッドを使うと、そのブロックまでの内容が二重に出力されます。

以下の手順で再現できます。(Rails 3.0.3で確認)

新規アプリケーション作成

$ rails new just_yield
$ cd just_yield

コントローラを追加

$ rails g controller welcome index
$ rm public/index.html

config/routes.rbに以下を追加

root :to => "welcome#index"


app/helpers/application_helper.rb に、yieldを行なうだけのHelperメソッドを追加

module ApplicationHelper
  def just_yield
    yield
  end
end

ビューを以下のように変更

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% end %>
C

サーバを起動して、http://localhost:3000/にアクセスすると、以下の出力になります。

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>
  
  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビューの先頭から、ブロックの終わりまでが二重に出力されます。

ビューを以下ように修正して、just_yieldの評価結果を変更すると正常に出力されます。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1; end %>
C

出力結果

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>

  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビュー内で使用するHelperメソッドが、yieldの結果をそのままリターンすると、二重の出力になるようです。

さらにこれの原因を調べてみると、do ... end を含むERBソースは、次のようにコンパイルされます。

@output_buffer.append_if_string=  just_yield do
  @output_buffer.safe_concat('B\n');
end

つまり、<%= ... %> ではなくて、<% ... %>を使っているのに、その式の評価結果を出力結果として使用するようです。このメソッドの実装は以下の所です。

gems/actionpack-3.0.3/lib/action_view/template/handlers/erb.rb

    def append_if_string=(value)
      if value.is_a?(String) && !value.is_a?(NonConcattingString)
        ActiveSupport::Deprecation.warn("<% %> style block helpers are deprecated. Please use <%= %>", caller)
        self << value
      end
    end

この Deprecation.warn は、確かに上記の問題が発生する時に、出力されています。

後方互換性のために、ブロックを伴う式の評価結果が文字列だと、それが出力されるようにしてある、ということのようです。

結論として、ブロックを伴う Helperメソッドの実装を変更できる場合には、

module ApplicationHelper
  def just_yield
    yield
    1 # 文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
  end
end

とすればよくて、実装が変更しにくい場合はビューの方を下記のように変更すれば、良いと思います。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1 # ブロックが文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
   end %>
C

世界のナベアツ問題をMoose::Roleで解く

最近、Perlの勉強を始めて Moose というモジュールが面白かったので、これを使って「世界のナベアツ問題」を解いてみました。

世界のナベアツ問題」とはこれです。

1 から 40 までの数をプリントするプログラムを書け。 ただし 3 の倍数または数に 3 が含まれる時はその数に続けて、「〜〜〜」をプリントし、8 の倍数の時はその数に続けて「ぅぅういえぇえあ」をプリントすること。

これを普通にPerlで書くと、こんな感じになると思います。

use strict ;

sub print_number {
    my ($n) = @_ ;

    print $n ;

    print "〜〜〜" if (($n%3 == 0) || ($n =~ /3/)) ;
    print "ぅぅういえぇえあ" if ($n%8 == 0) ;

    print "\n" ;
}

print_number($_) for 1..40 ;

これだと、print_number という関数の中に、以下のような仕様が全部埋め込まれています。

  • まず数字を表示する
  • アホになる条件(3 の倍数または数に 3 が含まれる時)
  • アホになった時に付加する文字列(「〜〜〜」)
  • 快感になる条件(8 の倍数の時)
  • 快感になった時に付加する文字列(「ぅぅういえぇえあ」)

もちろんこんな短いプログラムでは問題になりませんが、実際のプログラムでは、ひとつの関数の中に多くの処理を詰めこんでしまうと、解読、修正、再利用が難しくなります。また、多人数で同時に開発する場合に、コミュニケーションのミスが発生します。

そこで、仕様をなるべく分割し、かつ、ドメイン(適用業務)に近い記述で表現するのが、オブジェクト指向です。Perlにもその機能があるし、Mooseも基本はそれを拡張したものです。

しかし、一般的なオブジェクト指向言語では、クラスという形で分割された仕様を実装し、継承という形でそれらをつなげることが必要です。そして、継承に関連して、プログラミングのレベルでいろいろ難しい問題が発生して、個々のクラスを独立させることが難しくなってしまいます。

Mooseには「ポストモダンオブジェクト指向」というスローガンがあって、Role という記述の単位によって、継承のわずらわしさを無くそうとしています。

たとえば、「ある条件の時だけ、数字の出力後に特定の文字列を付加する」という処理ををMoose::Roleで書くとこうなります。

package Nabeatu::Role::Aho ;
use Moose::Role ;

has 'aho_string' => (
    is => 'rw' ,
    isa => 'Str',
    required => 1,
    ) ;

requires 'aho_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->aho_string if $self->aho_condition($n) ;
};

Roleは単独では動作しません。必ず、一般のクラスに「混ぜて」使います。その使う側のクラスに対して、「〜〜〜」を付加するというような、特定の機能を提供します。

MooseのRoleが優れていると私が考えるのは、その使う側のクラスに対して、「このRoleを使いたかったら、こういう機能を用意しろ」ということを表現できることです。この Role では、以下のことを要求しています。

  • aho_stringという文字列の属性に値を設定すること
  • aho_conditionというメソッドを用意すること
  • print_numberというメソッドを用意すること

そして、print_numberが実行された時に、クラス側の処理の後に、aho_conditionが成立しているかどうかをチェックして、それが真の時にだけ、aho_stringを表示します。

同様に、別の条件で「気持ちいい」みたいな文字列を付加する Role を書くとこうなります。

package Nabeatu::Role::Ecstasy ;
use Moose::Role ;

has 'ecstasy_string' => (
    is => 'rw' ,
    isa => 'Str',
    required => 1,
    ) ;

requires 'ecstasy_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->ecstasy_string if $self->ecstasy_condition($n) ;
};

それで、この二つのRoleされた要求を満たすクラスを書けばいいわけですが、ここでは、さらにもう少し工夫して、この要求を満たすRoleを作成してみます。

package Nabeatu::Role::Conditions ;
use Moose::Role ;

has 'aho_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;

has 'ecstasy_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;


sub dividable_by {
    my ($m, $n) = @_ ;
    return ($m%$n) == 0 ; 
}

sub has_number_character {
    my ($m, $n) = @_ ;

    return $m =~ /$n/ ;
}

sub aho_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->aho_number) || has_number_character($n, $self->aho_number) ;
}

sub ecstasy_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->ecstasy_number) ;
}

この Roleは、aho_conditionとecstasy_conditionという二つのメソッドを提供しますが、その代わりに、aho_numberとecstasy_numberという二つの属性を要求します。つまり、この「○で割りきれる」という基準となる数字を、使うクラスの側で設定できるようにします。

そして、上記の3つのRoleを使って、ナベアツを表現するクラスを書くとこうなります。

package Nabeatu ;
use Moose; 
with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_string} ||= "〜〜〜" ;
    $args{ecstasy_string} ||= "ぅぅういえぇえあ" ;
    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print "\n" ;
};

my $nabeatu = Nabeatu->new ;
$nabeatu->print_number($_) for (1..40) ;

実行結果

1
2
3〜〜〜
4
5
6〜〜〜
7
8ぅぅういえぇえあ
9〜〜〜
10
11
12〜〜〜
13〜〜〜
14
15〜〜〜
16ぅぅういえぇえあ
17
18〜〜〜
19
20
21〜〜〜
22
23〜〜〜
24〜〜〜ぅぅういえぇえあ
25
26
27〜〜〜
28
29
30〜〜〜
31〜〜〜
32〜〜〜ぅぅういえぇえあ
33〜〜〜
34〜〜〜
35〜〜〜
36〜〜〜
37〜〜〜
38〜〜〜
39〜〜〜
40ぅぅういえぇえあ


BUILDARGSは特別なメソッドで、Mooseによるインスタンス生成の途中で呼ばれるメソッドです。ここで適切な初期値を設定します。

インスタンス生成(new)の後で初期値を設定しようとすると、Roleから「○○の属性が無い」と言って怒られてしまいます。それで、生成が完了する前に、属性値を設定する必要があります。

無駄に長くなっているだけのように思われるかもしれませんが、本体のクラスとRoleを別の人が作成していると考えてみてください。両者に行き違いがあると、動きだす前に、Mooseがチェックしてくれます。

特に、requiresで要求したメソッドが無い時は、コンパイル時にチェックされます。たとえば、Nabeatuクラスの

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

を次のように変更するとエラーになります。

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy) ;

このエラーは次のように表示されます。

'Nabeatu::Role::Aho|Nabeatu::Role::Ecstasy' requires the methods 'aho_condition' and 'ecstasy_condition' to be implemented by 'Nabeatu' at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application.pm line 59
        Moose::Meta::Role::Application::apply('Moose::Meta::Role::Application::ToClass=HASH(0x8bfda68)', 'Moose::Meta::Role::Composite=HASH(0x8bfda38)', 'Moose::Meta::Class=HASH(0x8bb7158)') called at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application/ToClass.pm line 18
        (中略)
        Moose::with('Nabeatu::Role::Aho', 'Nabeatu::Role::Ecstasy') called at moose_nabeatu.pl line 79

「'aho_condition' と 'ecstasy_condition'という二つのメソッドが実装されてない」と言っていますが、重要なことは、この最後の 「line 79」 が、今、変更した withの行を指していることです。

このエラーは、print_numberが呼ばれる前、Nabeatuのインスタンスが生成される前に検出され、そこでプログラムの実行が止まるのです。

ということは、Nabeatu->new が実行された時点で、Nabeatuが使用している 3つのRoleが要求している条件は(Mooseで記述可能できちんと書かれている限りは)全て満たされていることが保証されるわけです。

多人数で大規模なプログラムを開発したり、オープンソースのモジュールを多く使うような場合は、全部組み合わせた時に、整合性が取れてないという問題が起こりがちです。Perlのように動的な言語では、特にそれが問題になりますが、Mooseを使うと、静的型言語に近いレベルで、そういう問題をチェックできます。

処理系に全ソースを読ませた時点でかなりのことがわかり、必要なインスタンスを生成した時点でほとんどのエラーがチェックされます。

それともう一つ面白いことは、Role を個別のインスタンスに対して動的にアサインできることです。

たとえば、アホになったりしない真面目なナベアツを作ってみます。

package Nabeatu::Dynamic ;
use Moose; 
with qw(Nabeatu::Role::Conditions) ;

has 'aho_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

has 'ecstasy_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

my $nabeatu_d = Nabeatu::Dynamic->new ;
$nabeatu_d->print_number(32) ; # ただの 「32」

これは、「〜〜〜」とか「ぅぅういえぇえあ」とか、ふざけたことを言わずに、数字だけを表示する真面目なナベアツです。

そして、このナベアツに Aho の Roleをアサインします。

$nabeatu_d->aho_string('〜〜〜') ;
Nabeatu::Role::Aho->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜

さらに、EcstasyのRoleをアサインします。

$nabeatu_d->ecstasy_string('ぅぅういえぇえあ') ;
Nabeatu::Role::Ecstasy->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜ぅぅういえぇえあ

つまり、このナベアツは、インスタンスごとに違うふるまいをさせることができるわけです。Aho抜きで「32ぅぅういえぇえあ」と言うナベアツを作ることもできます。

これは、JavaScriptのような Prototypeベースのプログラミングに近いと思いますが、この場合でも、Roleが要求している条件のチェックは行なわれます。

Roleが必要としているメソッドや属性が無いと、applyの時点でエラーになります。JavaScriptで同様のことを行なった場合には、(よほど工夫しないと)実際にprint_numberが実行されるまで、その不整合は検出されないと思います。

Mooseは、普通のクラスベースの使い方もできるし、「ポストモダン」なRoleによって、継承の木構造を浅くする(無くする)ような設計もできるし、プロトタイプベースで動的にふるまいを変えるようなインスタンスもできます。AOPっぽい機能についても他にもいろいろあるようです。

Mooseは、この本の理論的枠組みを元にしているそうです。

The Art of the Metaobject Protocol (The MIT Press)
The Art of the Metaobject Protocol (The MIT Press)

この考え方は、CLOSの基盤にもなっていて、かなり歴史のあるものです。

こういうメタプログラミングで言語を拡張してどんどん便利にしていこうという試みは、他にも多く行なわれていますが、理論的基盤がしっかりしてないと、メタな機能をあれこれ使った時に矛盾が生じて破綻していまいます。Moose はそういう意味でも安心して使える感じがしました。

それと、、Mooseとは関係ないですが、Perlの練習としてついでに、関数的なナベアツも書いてみました。こちらはあんまりうまく行かなかったけど、おまけとして付けておきます。

use strict;
use utf8;

my $3の倍数の時 = の倍数の時(3) ;
my $3がつく数の時 = がつく数の時(3);
my $8の倍数の時 = の倍数の時(8);
my $あほ =  の時だけ(または($3の倍数の時,  $3がつく数の時), "〜〜〜");
my $気持ちいい = の時だけ($8の倍数の時, "ぅぅういえぇえあ");

my $数字をそのまま = sub  { shift };
my $改行 = sub  { "\n" };

print map {
    my $n = $_ ;
    map {
        $_->($n) # 一つの数字に4つの関数を順番に適用して配列を作る
    } (
        $数字をそのまま,
        $あほ,
        $気持ちいい,
        $改行
    ) 
} 1..40; # ...という処理を1から40まで繰り返して配列の配列を作る

sub 倍数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i%$n == 0;
    }
};

sub つく数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i =~ /$n/ ;
    }
};

sub たは {
    my ($a, $b) = @_ ;
    return sub {
        my ($i) = @_ ;
        $a->($i) or $b->($i) ;
    }
};

sub 時だけ {
    my ($condfunc, $text) = @_ ;
    return sub {
        $condfunc->(shift) ? $text : "" ;
    }
}

Amrita2-2.0.2リリース

Amrita2-2.0.2をリリースしました。

プログラムの変更は、GetTextに関する小さなバグ修正二点だけですが、合わせて ザリガニが見ていた...。さんが書いてくださった以下の Amrita2 に関するエントリを英訳して、Amrita2のWikiに掲載させていただきました。

開発者の視点と実際のユーザの視点はズレてしまいがちなので、実際に使用された方の紹介というのは、ユーザにとって非常にわかりやすいものになるのではないかと思います。

id:zariganitosh さんのブログは、以前から私も、Railsについての勉強で参考にさせていただいていましたが、これで少しでもAmrita2を使う人が増えたらいいな、と思っています。ただ、いつもながら私の英語がヒドいので、そこが心配な所ですが。

なお、今回のリリースで修正した、Gettext回りのバグについての指摘は省かせていただきました。

リポジトリに謎の障害

rmindを公開しているリポジトリでおかしな現象が発生しました。

環境、構成

リポジトリのサーバは、Xen + drbd + OpenVPNという仮想だらけの環境ですが、数十日間連続稼動しており、他には特に問題がないので、この件には関係ないと思います。

予兆

しばらく前から、commitの時に以下のようなエラーが出ていました

  • general server errorというようなメッセージで異常終了
  • checksum errorというようなメッセージで異常終了

出るタイミングは不定で、再度(そのまま)、commitをするとそのまま直る場合と、何度やっても同じエラーが出る場合がありました。

この時は次の手順で復旧しました。

  1. ソースを退避
  2. 問題のソースを含むディレクトリを削除
  3. svn update
  4. 退避したソースを上書き
  5. svn commit

本来はここでしっかり調査すべきでしたが、この手順で復旧できていたので、リポジトリでなくワークファイル側の問題だと判断していました。

現象の確認

本日、リポジトリ(内の特定のソース)が完全におかしくなっていることに気がつきました。

  1. svn checkout
  2. ソースを修正 commit
  3. ディレクトリにcheckout
  4. 2の内容が反映されていない

問題となっているソースは、rmind/app/model/user.rb です。

原因

今の所、全く不明です。

対策

現状のリポジトリを保存後、新しいリポジトリを作成し、最新のソースをチェックインします。

「ザリガニが見ていた...。」さんのAmrita2紹介記事

「ザリガニが見ていた...。」さんでAmrita2についての解説記事を書いていただきました。

「ザリガニが見ていた...。」さんのRailsについてのエントリは参考になるものが多くて、私もレビュアブルマインドを開発する時や、現在行なっているRails2.0対応の作業で参考にさせていただいています。

そういうブログでAmrita2について書いていただけたことは、本当にありがたいことです。特に、Rails(ERB)に慣れている方からの視点でAmrita2について書いてもらうと、開発している側にはわからないことがいろいろ見えてきます。

そこで、この3つの記事に対して、補足、感想等を書かせていただきます。

インストールについて

id:zariganitoshさんは、tarボールを vendor/plugin に展開して使用されているようですが、UsingWithRailsに書いてあるように、gemでインストールすることもできます。

$ sudo gem install amrita2
$ cd (アプリケーションルート)
$ mkdir vendor/plugins/amrita2
$ cp /usr/lib/ruby/gems/1.8/gems/amrita2-2.0.1/init.rb vendor/plugins/amrita2/

/usr/lib/ruby/gems/1.8/gemsは、環境によって違いますが、gemsがインストールされているディレクトリです。

ERbからの段階的な移行

最初からAmrita2のすべてを理解する必要はなく、便利だと思う機能から利用していけば良いのだ。

これは、まさに私が意図していたことで、従来のERbテンプレートから少しづつ段階的に移行していけるように工夫しています。

次の例のように(これはanalyticsのjsコードを挿入している所)、<<%< というマークから段を下げた部分は、ERbテンプレートと同じように扱われます。

    <<%<
      <script type="text/javascript">
        var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
        document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
      </script>
      <script type="text/javascript">
        var pageTracker = _gat._getTracker("<%= $rmind_config[:google_analytics_account] %>");
        pageTracker._initData();
        pageTracker._trackPageview();
      </script>

CDATAで囲んだ所も同様にERbとして扱われます。

だから、Amrita2の仕様やエラーメッセージがよくわからない場合は、すぐに、ERbに戻ってそこだけERbで書くことができます。

出力ソースのインデント

ブラウザの描画結果はまったく同じなのだが、やはりHTMLソースも改行されて欲しい...。

Amtita2の出力結果は、はっきり言ってインデントがおかしくて、人間が見るには見づらいものになっています。

後の記事にありますが、これを補うには、tidy等の外部コマンドをフィルターとして組みこんでインデントしてください。

次のようにすると、デバッグ時のみインデントされた出力を行なうことができます。プロダクション環境では、余分なコマンド呼び出しを行ないません。

(config/environment.rb)
if ENV['RAILS_ENV'] == "development"
  IndentByTidyIfDebug = Amrita2::Filters::CommandFilter['tidy -q -xml -indent -utf8']
else
  IndentByTidyIfDebug = nil
end
(layout/application.html.a2)
<<html :| IndentByTidyIfDebug <
  <<head<
    <title>Reviewable Mind</title>

ただし、tidyのインデントは、railsjavascript_tag の出力を壊してしまうようで、javascriptが正しく実行されないことがあります。(他に適当なHTMLのプリティプリンタが無いか物色中です)

表記述の仕様

ここは(ここも)、説明が完全に不足していた所ですが、仕様を読み取ってわかりやすくまとめていただけました。

  • 「|」で区切った範囲が、tdタグの1セルと解釈される。
  • 「||」で区切った左側で属性を指定すると...
    • その行はtdタグの属性値の指定になる。
    • 属性値の指定が無ければ、半角スペースで埋めておけば良い。
  • 上下のハイフンの連続は、無くてもOK。
  • 「|」の間隔も最低1スペースあればOK。(インデントを揃えておかないと、この書式の意味は無いが。)
  • ちなみに、「||」の右側の「|」を「||」に置き換えるとthタグと解釈された。
  • td、th以外の目的で「|」を利用するときは、エスケープ「\|」しておく必要がある。

まったくその通りです。

この記法は激しく好みが分かれる所のような気がしますが、次のようにフォームと組み合わせた場合等は、有効ではないかと思います。

次の例は、レビュアブルマインドのサインアップフォームです。

  <%
      signup_form = amrita_define_form(:user, :action=>:login) do |f|
        f.text_field :login
        f.password_field :password
        f.add_field_element :password_confirm, password_field_tag(:password_confirm, "")
        f.text_field :email, :size=>50
        f.add_field_element :terms_of_use, check_box_tag(:terms_of_use, false)
        f.add_field_element :link_to_terms_of_use, link_to('terms of use', '/term.html')
      end
   %>

  << :signup_form | AcceptData[:hook] <
    <<two_columns_form<
      <<<---------------------------------------------------------------
        ||| Login ID:            | <<:login>>                          |
      <<<---------------------------------------------------------------
        ||| Password:            | <<:password>>                       |
      <<<---------------------------------------------------------------
        ||| Password(confirm):   | <<:password_confirm>>               |
      <<<---------------------------------------------------------------
        ||| E-Mail:              | <<:email>>                          |
      <<<---------------------------------------------------------------
        ||| Terms of use:        | <<:terms_of_use>> &#160;            |
        |||                      | I have read and agree to the &#160; |
        |||                      | <<:link_to_terms_of_use>>           |
      <<button_bar  <---------------------------------------------------
        <<:| Join[:nbsp]<
          %= submit_tag _('Signup')
          %= link_to _('Login'), :action => 'login'

two_columns_formは、マクロという機能を使用して、tableタグに置き代わりますが、ここに直接 table タグを指定してもかまいません。

ちなみに、この例では、lableタグを後から埋めこむためにマクロを使用しています。

Rubyブロック内のルール

# ブロック内にRuby式以外が存在するとエラー。
# NG
% 3.times do
  <p>
    index.html.a2
  </p>
% end

# ブロック内がすべてRuby式なら大丈夫。
# OK
% 3.times do
  %= "<p>"
    %= "index.html.a2"
  %= "</p>"
% end


行頭の%の記述ですが、ここは、ちょっと特殊な処理を行なっています。

  • 行頭に%が続く限り、(インデントを無視して)一つのCDATAブロックとしてまとめる(後にERbとして一括処理)
  • 行頭が%=だったら、その1行分の結果を出力する処理に変換

この記法は単発の短い処理を書くのには適していますが、ブロックを使う処理や、複数行にまたがる処理をERbで書く時は、次の書き方の方がおすすめです。

<<%<
  <% 3.times do %>
    <p>
      index.html.a2
    </p>
  <% end %>

GetTextへの自動対応

これは、Amrita2の最も重要な機能と言えるかもしれません。Amrita2では、ほとんど自動的にテンプレートを国際化することができます。

Gettext用のフィルターがあってこれを組み込むと、テンプレート内の文字列に対して、自動的に_()を呼び出す処理を行います。

require "amrita2/template"
require "amrita2/gettext"
include Amrita2

tmpl_text =  <<-END
<<html<
  <<body<
    <<h1 :title |Attr[:title, :body]>>
    <<p <
      Amrita2 is a html template libraly for Ruby.
END

tmpl = Amrita2::Template.new(tmpl_text) do |e, src, filters|
  filters << Amrita2::Filters::GetTextFilter.new
end

tmpl.set_trace(STDOUT)
tmpl.text_domain = 'test'
puts tmpl.render_with(:title=>{:title=>"hello world", :body=>"hello world" })

このサンプルから出力されたコンパイル済みソースを抜粋すると、次のようになっています。

__stream__.concat("<html><body>")
__stream__.concat(XXTitleInstance.render_with($_.amrita_value("title"), __binding__))
__stream__.concat("<p>")
__stream__.concat(_("Amrita2 is a html template libraly for Ruby.") % $_)
__stream__.concat("</p></body></html>")

"Amrita2 is a ..."という文字列が、_()呼び出し後に出力されています。全ての文字列に対して自動的にこの処理が組み込まれます。

Rails用のブリッジでは、自動的にこのフィルターを組み込んでいますので、Rails上では、何もしないで国際化対応になります。

次のRakeタスクを実行すると、テンプレート上の全ての静的な文字列を国際化対象の文字列としてpoファイルを生成します。

task :updatepo do
  $: << 'vendor/plugins/amrita2/lib'
  require 'gettext/utils'
  require 'amrita2/gettext'
  require 'amrita2/macro'

  GetText.update_pofiles("rmind", 
                         Dir.glob("{app,config,components,lib}/**/*.{rb,rhtml,a2html,a2}"),
                         "rmind 1.0.0" 
                         )
end

レビュアブルマインドは、この機能を利用して国際化しています。

form_forブロックの中のrender :partialの悩みを解決

この悩みとは、次のようなことではないかと思います。

  • newとeditは似たページであるので、似た部分を一つの記述ですませたい
  • 基本的には共通部分をパーシャルテンプレートにすればよい
  • ボタンやテキストフィールド等の要素の配置は共通だが、要素の生成方法が微妙に違う
  • 生成方法の違いを、テンプレート間で受け渡す記述が必要になるが、:localsによる受け渡しがもう一つしっくり来ない

Amrita2を使えば、この問題は解決します。

実際のアプリケーションでは、これと同じパターンでもう少し複雑な問題になると思うのですが、それに柔軟に対応しつつDRYに書けると思います。

というのは、上の例ではnewとeditの違いがボタン一つだったので、受け渡すパラメータも一つだけで済みます。パーシャルテンプレートが複数のページから使用されて、それぞれに微妙に違う要求があったような場合に、Amrita2は有効です。

Amrita2のsample/depotのadminコントローラから引用します。

(view/admin/new.html.a2)

<h1>New product</h1>

  <%
    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:create }
           ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Create")
    end
  %>

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

(view/admin/edit.html.a2)

<h1>Editing product</h1>

  <%
    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end
  %>

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

(view/admin/_form.html.a2)

%= error_messages_for 'product'

<< :form <
  <<p<
    <label for="product_title">Title</label><br/>
    <<:title>>

  <<p<
    <label for="product_description">Description</label><br/>
    <<:description>>

  <<p<
    <label for="product_image_url">Image url</label><br/>
    <<:image_url>>

  <<p<
    <label for="product_price">Price</label><br/>
    <<:price>>

  <<:submit>>

まずパーシャルテンプレートの _form.html.a2 から説明すると、ここでは、title description 等の要素をどういう形で、どこに配置するかだけ指定しています。個々の要素の生成は一切行なっていません。

それを生成しているのが、newとeditの中にある次の部分です。

    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end

amrita_define_formは、form_forのラッパーで、同じパラメータを同じ順番で受け取ります。そして、こちらでは、逆に「どこに配置するか(WHERE)」ということは後回しにして、「何を配置するか(WHAT)」についての記述だけを行います。

fのメソッドも、form_forの時とほぼ同じですが、

  • add_field_elementというメソッドが追加されている
  • それ以外は、FormHelperと同じだが、一つ目のパラメータがフィールドのIDとなり、後程、そのIDに対応した場所に配置される

ということになります。

例えば、修正時には、titleが変更できなくて、テキストフィールドの代わりに単なる文字列で表示するとしたら、次のようにします。

    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.add_field_element :title, @product.title # ← ココ
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end

こうすると、テキストフィールドが表示されていた位置に、その内容の文字列がそのまま表示されます。

それで、親子で受け渡しされる変数は常に一個で同じ記述ですみます。

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

$_ は、Amrita2のコンテキストバリューと言って、その時点で評価に使われている値です。この場合は、フィールドと要素の対応を示すハッシュになっています。:object を使っているので、このハッシュがパーシャルテンプレートに(テンプレート名と同じ名前の変数として)渡されます。

ここは、Amrita2の中でも最もトリッキーな所なので、かえってわかりにくいと感じるかもしれませんが、

  • ベーステンプレートでWHATを指定する
  • パーシャルテンプレートでWHEREを指定する

という機能の分離はきれいにできていると思います。WHATの部分(amrita_define_form)はヘルパーメソッドにすることもできるので、

  • 一つのWHATを複数のWHEREで共有する
  • 一つのWHEREを複数のWHATで共有する

という両方のDRYが実現できます。