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 引数 message
が false
か nil
であれば、シンボル :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]
がシンボルであれば、message
と options[: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
変数に Article
や Document
といったモデルクラスをひとつずつ格納しては、ブロックを評価します。
ブロック内では 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
のインスタンスメソッド add
と generate_message
のソースコード解説を終わります。
今回の題材は、Ruby 独特の表現が満載です。
Ruby 初心者には少し難解なところもあったかもしれませんが、楽しんでいただけると幸いです。
--
黒田努