非ActiveRecord::Baseなクラスからvalidationを使ってみました
Railsの便利機能の中でもかなり注目度の高いActiveRecord::Base#validates_*によるバリデーションですが、これをDBに保存する場面以外でもつかえないか、というのが今回のトピックです。
ユーザからの入力をDBではなく通常のファイルや帳票に落としたり、単に画面に表示させたり、他のWebサービスに送りつけたりする場面で、ARのvalidationが使えると便利だろうなぁ、と。
DBが必須になる*1validates_associatedとvalidates_uniqness_of以外についてはうまく動かせましたのでメモを。
実際のブツはこちらからどうぞ。
まずは動作を
サンプルクラスの動作
SampleAccount = Struct.new(:username, :email, :balance) SampleAccount.class_eval do include NotOnRails::ValidationsAdapter validates_presence_of :username, :email validates_exclusion_of :username, :in => ["admin"] validates_numericality_of :balance, :only_integer => true validates_confirmation_of :email validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i def save_method puts " *SAVE METHOD CALLED!(detail below..)* " puts self.inspect @new_record = false true end end
ここで、save_methodはsaveしたときに呼び出される動作を記述します。名前のまんまですが。冒頭の文脈ですと、このsave_methodをオーバーライドして帳票出力なんかをします。
validateしてみる(validateに失敗する場合)
bad = SampleAccount.new bad.username = "admin" bad.balance = "*not a number*" bad.email = "moronatural_at_gmail.com" bad.email_confirmation = "moro_at_be.to" unless bad.save $stderr.puts "*VALIDATION ERROR*" bad.errors.each{|col,err| puts " - #{col.to_s.humanize} #{err}" } end
実行結果は以下のようになります。
*VALIDATION ERROR* - Username is reserved - Balance is not a number - Email is invalid - Email doesn't match confirmation
validates_presence_ofやvalidates_format_ofなんかが効いているのがわかると思います。ちなみにvalidate_*した結果のエラーはAR::Baseを継承したクラスと同様にerrorsで取り出せるようになります。
validateしてみる(validateに成功する場合)
今度はちゃんとした値をセットしてみます。
good = SampleAccount.new good.username = "morohashi" good.balance = "12345" good.email = "moronatural@gmail.com" good.email_confirmation = "moronatural@gmail.com" good.save!
結果は以下のような感じです。上のsave_methodで定義したな処理が実行されます。
*SAVE METHOD CALLED!(detail below..)* #<struct SampleAccount username="morohashi", email="moronatural@gmail.com", balance="12345">
いい感じですね。
ValidationsAdapterモジュールの仕組み
基本はActiveRecord::Validationsをincludeするだけなんですが、その際にalias_methodでsaveやsave!に別名が付けられます。その時点で同名のメソッドが定義されていないとエラーとなりますので、以下のメソッドを定義しておきます。
- save
- save!
- update_attribute
また、内部的にnew_record?メソッドが呼ばれますのでそれも定義しておく必要があります。
def new_record? ; @new_record ; end def save ; save_method ; end def save! ; raise "override me." unless save_method ; end def update_attribute; update_method ; end
上記のような感じで、saveとsave!の実体はsave_methodを、update_attributeの実体はupdate_methodを、それぞれmix-inしたクラス内で定義します。
さらに、validates_numericality_ofを用いる場合には、内部で呼ばれている*_before_type_castも定義する必要があります。拙作ライブラリではDynamicMethodProxyモジュールにてmethod_missingをオーバーライドし、同メソッドを動的に定義しています。
def method_missing(method, *args) case method.to_s when /(\w+)_before_type_cast/ before_type_cast($1.to_sym) else super end end private def before_type_cast(attr_name); self.__send__(attr_name); end
主な処理はこんな感じで。あとはメインとなるValidationsAdapterモジュールがincludeされるタイミングでごにょごにょとしてActiveRecord::Validationsを定義しています。
また、validationのエラーメッセージを表示させるためにself.human_attribute_nameメソッドが呼ばれます。とりあえずActiveRecord::Base.human_attribute_nameに委譲しています。
module NotOnRails::Validations def self.included(base) base.extend(ActiveRecord::Validations::ClassMethods) base.extend(NotImplementedValidations) base.class_eval do include ActiveRecord::Validations def self.human_attribute_name(attribute_name) ActiveRecord::Base.human_attribute_name(attribute_name) end end end end
プログラム内からの使い方
こちらからアーカイブをダウンロードしてください。Railsプラグインの形式になってはいますが、要はlib/validations_adapter.rbをrequireし、NotOnRails::ValidationsAdapterモジュールを通常どおりインクルードすれば問題なく使えるはずです。
以上、感想やバグ報告などお待ちしています。
*1:と言うかDBに保存する場面でないと無意味だろうと思われる