Sexy Validationで独自のValidationを

いつのまにか、Rails3+1.9.2というモテコンビで仕事をやっております。で、Rails3から入った新機能として、SexyValidationというのができたので、それを使ってみました。

Sexy Validationとは、いままでのRailsのvalidationの書き方とは違い、Modelに対してのvalidationがスッキリ書けるようになりました。例えば、titleカラムを持つPostというModelがあるときに、title要素は必須なんだというvalidationを書こうとすると、以下のようになります。

class Post < ActiveRecord::Base
   validates :title, :presence => true
end

今迄の

validates_presence_of :name

よりわかりやすいのではないでしょうか。

独自のvalidationをしたくなったらどうすればいいのでしょうか?

その場合、ActiveModel::EachValidatorを継承したクラスを作り、each_validatorメソッドをオーバーライドします。文字列にあらかじめ設定されたwordが含まれていないか確認するvalidationを定義してみましょう。

class NgWordValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
      record.errors[attribute] << 'include ng word' if value =~ /NG WORD/
   end
end

引数valueには、validateメソッドの第一引数のカラムの値が渡ってきます。validationを失敗とするときは、record.errors[]にエラーメッセージをいれます。attributeはvlidationの名前が入ってきます。

validationを利用する側では、以下のように設定できます。

class Post < ActiveRecord::Base
   validates :title, :presence => true, :ng_word => true
end

簡単ですね。実際にどうなるか確認してみましょう。

> post = Post.new
=> #<Post id: nil, title: nil, created_at: nil, updated_at: nil> 
> post.valid?
=> false 
> post.errors
=> {:title=>["can't be blank"]} 
> post.title = "NG WORD TITLE"
=> "NG WORD TITLE" 
> post.valid?
=> false 
> post.errors
=> {:title=>["include ng word"]} 
> post.title = "TITLE"
=> "TITLE" 
> post.valid?
=> true

正しくvalidationできていることがわかります。

今回は、内部でエラーとする文字列を保持していましたが、利用する側から渡したい場合は、trueとしているとこにハッシュを渡します。

class Post < ActiveRecord::Base
   validates :title, :presence => true, :ng_word => {ng:"NG WORD"}
end

validate_each側では、渡されたハッシュはoptionsに入ってわたってきます。

class NgWordValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
      record.errors[attribute] << 'include ng word' if value =~ /#{options[:ng]}/
   end
end

作成したValidatorは全てのActiveModelで使用することができます。例えば、bodyカラムを持つCommentモデルを作成して、以下のようにvalidationを設定すれば、先程のng_word validatorが使用できます。

class Comment < ActiveRecord::Base
   validates :body, :ng_word => {ng:"XXXX"}
end
> comment = Comment.new
=> #<Comment id: nil, body: nil, created_at: nil, updated_at: nil> 
> comment.body = "NG WORD"
=> "NG WORD" 
> comment.valid?
=> true 
> comment.body = "XXXX"
=> "XXXX" 
> comment.valid?
=> false

とても、簡単なのでどんどん利用していきたいです。目下の悩みどころは、このValidatorのクラスをどこに配置するべきかがわかっていないので、有識者の方に教えてもらいたいです。(libにいれたのですが、勝手にrequireされなくて。。)