Rails 2.2 の ActiveRecord::Validations#add のソースコードを読む

2009/01/17

次に掲載するのは、ActiveRecord 2.2.2 の lib/active_record ディレクトリにある validations.rb からの抜粋です。

def add(attribute, message = nil, options = {})
  message ||= :invalid
  message = generate_message(attribute, message, options) if message.is_a?(Symbol)
  @errors[attribute.to_s] ||= []
  @errors[attribute.to_s] << message
end

今回は、このメソッドについてソースコードを解読していきます。

この add メソッドは、ActiveRecord::Errors クラスのインスタンスメソッドです。

あるレコードオブジェクトについて値の検証 (validation) が失敗すると、それが ActiveRecord::Errors オブジェクトに登録されます。

この add メソッドは、エラーを登録するメソッドです。

Rails 2.1 までは、add メソッドの第 2 引数には文字列でエラーメッセージを指定していました。ただし、第 2 引数を省略すると、"is invalid" というデフォルトのエラーメッセージが登録されます。

Rails 2.2 からは、第 2 引数にシンボルを指定すると、国際化 (i18n) の仕組みが働いてロケールに合わせてエラーメッセージが選択されます。


行単位でコードを読んでいきます。

  message ||= :invalid

第 2 引数 messagefalsenil であれば、シンボル :invalid を代入します。この処理によって、第 2 引数が省略された場合の動きが Rails 2.1 以前と互換性を保っています。

  message = generate_message(attribute, message, options) if message.is_a?(Symbol)

message のクラスが Symbol であれば、generate_message メソッドの戻り値を message 自身に代入します。

このメソッドについては後述しますが、その名の通りエラーメッセージを生成して文字列として返します。

  @errors[attribute.to_s] ||= []
  @errors[attribute.to_s] << message

インスタンス変数 @errors は、フィールド(属性)ごとにエラーメッセージを保持するためのハッシュ(連想配列)です。

ここでも ||= を使って初期化をしています。

このように、オブジェクトが最初に必要になった時点で初期化を行うことを遅延初期化 (lazy initialization) と呼びます。


続いて、generate_message メソッドのソースコードです。

def generate_message(attribute, message = :invalid, options = {})

  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end
  
  defaults << options.delete(:default)
  defaults = defaults.compact.flatten << :"messages.#{message}"

  key = defaults.shift
  value = @base.respond_to?(attribute) ? @base.send(attribute) : nil

  options = { :default => defaults,
    :model => @base.class.human_name,
    :attribute => @base.class.human_attribute_name(attribute.to_s),
    :value => value,
    :scope => [:activerecord, :errors]
  }.merge(options)

  I18n.translate(key, options)
end

教育的配慮に富んだ面白いコードです。

  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

options[:default] がシンボルであれば、messageoptions[:default] の値を入れ替えています。

多くの伝統的な言語では、変数 a と b の値を入れ替える時、次のように書く必要があります。

temp = a
a = b
b = temp

しかし、Ruby では簡潔に次のように記述できます。

a, b = b, a

これを多重代入 (parallel assignment) と呼びます。


  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end

まず、@base.class.self_and_descendents_from_active_record は、対象となるレコードオブジェクトのモデルクラス自身とその祖先クラスのうち ActiveRecord::Base の子孫であるクラスの配列を返します。

対象となるレコードオブジェクトが Article モデルのインスタンスで、Article モデルは Document モデルを継承し、さらに Document モデルは ActiveRecord::Base を継承していると仮定しましょう。

すると、@base.class.self_and_descendents_from_active_record は、[Article, Document] という配列を返すことになります。

Ruby では、クラス自体もオブジェクトであるため、配列の要素として扱うことが可能です。

次に map メソッドです。collect メソッドの別名です。

Enumerable モジュールをインクルードしたクラス(配列など)には、この非常に便利なメソッドが備わっています。

次の例を見てください。

ary = [1, 2, 3, 4, 5]
ary = ary.map do |n|
  n * n
end

最終的に変数 ary にはどのような値が格納されているでしょう。

答えは、[1, 4, 9, 16, 25] です。

この例から分かるように、map メソッドは配列の要素をひとつずつブロックに渡し、そのブロックからの戻り値をひとつずつ集めて (collect) 新しい配列を作って返します。

このことを理解した上で、元のソースコードに戻りましょう。

  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end

map メソッドは klass 変数に ArticleDocument といったモデルクラスをひとつずつ格納しては、ブロックを評価します。

ブロック内では 2 つのシンボルから構成される配列が作られています。

ここでも興味深い書き方が行われています。

:"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}"

Ruby では #{ ... } という書き方を使って文字列中に式の値を埋め込む (interpolate) ことができます。

そして、文字列の前にコロン (:) を付ければ、その文字列をシンボルに変換できます。

仮に attribute の値を 'title' とし、message の値を 'invalid' とすると、上記のコードは次のような配列を変数 defaults に代入することになるわけです。

[
  [ :"models.article.attributes.title.invalid", :"models.article.invalid" ],
  [ :"models.document.attributes.title.invalid", :"models.document.invalid" ]
]

では、続けましょう。

  defaults << options.delete(:default)

options はハッシュ(連想配列)であったことを思い出しましょう。

ハッシュのインスタンスメソッド delete は引数に指定されたキーに対応する値を削除してその値を返します。

そして、その値を配列 defaults の末尾に追加しています。


  defaults = defaults.compact.flatten << :"messages.#{message}"

defaults の値は、シンボルを含む配列の配列です。compact メソッドで配列中に含まれる nil をすべて除去します。そして、flatten メソッドで、入れ子になった配列を“平坦”な配列に変換します。最後に :"messages.invalid" を追加して、defaults 自身に代入し直します。

flatten メソッドの働きを理解するには、次のコードをご覧ください。

ary = [1, 2, 3, [4, 5, 6], 7]
ary = ary.flatten

最終的に変数 ary の値は、[1, 2, 3, 4, 5, 6, 7] のような入れ子構造のない配列となります。

  key = defaults.shift

配列 defaults から最初の要素を除去して、変数 key に代入しています。


  value = @base.respond_to?(attribute) ? @base.send(attribute) : nil

インスタンス変数 @base には、この Errors オブジェクトの対象となっているレコードオブジェクトが格納されています。

respond_to? メソッドは、そのレコードオブジェクトがあるメソッドを持っているかどうかを調べるメソッドです。

今、変数 attribute には :title のようなシンボルが格納されているはずですね。

レコードオブジェクトが title メソッドを持っていれば、@base.respond_to?(attribute)true と評価されます。

さて、この行では三項演算子 (ternary operator) が使われています。次の例をご覧ください。

y = x ? 1 : 0

変数 y の値は、x が「真」の時 1 となり、そうでないとき 0 になります。Ruby では、false でも nil でもない値はすべて「真」と判定される点に注意してください。

元のコードでは変数 value@base.send(attribute)nil が格納されます。

send メソッドはメソッド名を指定して、あるオブジェクトのメソッドを呼び出します。

つまり、@base.title のようなメソッドの戻り値が変数 value に代入されるわけです。

三項演算子は Ruby 特有の演算子ではなく、C の影響を受けた多くのプログラミング言語が採用しています。

if による条件分岐を使っても表現できるため、三項演算子の使用を避ける人もいます。

私自身は、このソースコードのように条件式が短い場合、どうしても三項演算子を使いたくなります。


  options = { :default => defaults,
    :model => @base.class.human_name,
    :attribute => @base.class.human_attribute_name(attribute.to_s),
    :value => value,
    :scope => [:activerecord, :errors]
  }.merge(options)

ここでのポイントは、ハッシュのインスタンスメソッド merge です。

変数 options もハッシュであることを思い出してください。

メソッド merge は、文字通りハッシュとハッシュをひとつにまとめます。

次の例を見てください。

a = { :foo => 1, :bar => 2 }
b = { :foo => 3, :baz => 4 }
c = a.merge(b)

変数 c に格納されるのは、{ :foo => 3, :bar => 2, :baz => 4 } というハッシュです。

この merge メソッドは、オプションにデフォルト値を与える時によく使われます。一種のイディオムとして覚えておくとよいでしょう。


  I18n.translate(key, options)

I18n モジュールの translate メソッドは、第 1 引数に指定されたキーに対応する文字列を翻訳ファイルから探し出して返します。

翻訳ファイルとは config/locales ディレクトリに置かれた YAML ファイルです。

詳しくは、連載「基礎 Ruby on Railsの asagao を Rails 2.2 に対応させる」の国際化(i18n)の第一歩を参照してください。

また、translate メソッドの詳しい使い方については、The Ruby on Rails I18n core api を参照してください。

以上で、ActiveRecord::Validations のインスタンスメソッド addgenerate_message のソースコード解説を終わります。


今回の題材は、Ruby 独特の表現が満載です。

Ruby 初心者には少し難解なところもあったかもしれませんが、楽しんでいただけると幸いです。
--
黒田努