カスタム validation

うーむ。フレイムワークのソースを見ざるを得ない訳で。
直前エントリにおいて、AR#save メソド発行時、戻りが false (UPDATE 失敗) だったのですが、その所以はドコなんだ、とゆー事でバックトレイスを。
app/models/comment.rb (の一部再掲)

class Comment < ActiveRecord::Base
  belongs_to :article

  validates_presence_of :author, :body
  validates_against_spamdb :body, :url, :ip
  validates_age_of :article_id

上記の 2 番目の validate でヒッカカっている可能性もあるが、試験 (test_reject_article_age メソド) において、@article3 から @article1 に変更カケて save を発行しているあたりで article_id に違いない、という非常に適当な判断より validates_age_of メソドを確認。以下。

      def validates_age_of(*attr_names)
        configuration = { :message => "points to an item that is no longer avail
able for interaction"}
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
        
        validates_each(attr_names, configuration) do |record, attr_name, value|
          next unless value.to_i > 0
          record.errors.add(attr_name, configuration[:message]) if SpamProtection.new.article_closed?(record)
        end
      end

もう何が何だかワケ分かりませんな。とりあえず以下にポイントを。

  • 仮引数の *attr_names は可変長引数を配列として受けとりまっせ、という合図
  • で configuration という名前の Hash を初期化
  • いっちゃんケツの引数が Hash だったら引数から pop して configuration に追加

(このあたりまでは validate の決まり文句的な手続きみたい)

ちなみに irb (というか script/console) で試してみたのが以下。

> configuration = { :message => "blocked by SpamProtection" }
=> {:message=>"blocked by SpamProtection"}
> configuration.update({:attr => "value"})
=> {:attr=>"value", :message=>"blocked by SpamProtection"}

で、次の validates_each が微妙。内容について別途。ぱっと見では、引数に指定されたシンボルについての検査を、という風に読める。SpamProtection#article_clised? は同じソースで定義されており、以下のようになっている。

  def article_closed?(record)
    if config['sp_article_auto_close'].to_i > 0
      if record.article.created_at.to_i < config['sp_article_auto_close'].to_i.d
ays.ago.to_i
        logger.info("[SP] Blocked interaction with #{record.article.title}") 
        return true
      end
    end
  end

デフォルトでは config['sp_article_auto_close'] は 300 らしい。現時点での試験用 DB も以下のようになっている。

mysql> select * from settings ;
+----+---------+------------------------+-----------+
| id | user_id | name                   | value     |
+----+---------+------------------------+-----------+
|  1 |    NULL | login                  | tobi      | 
|  2 |    NULL | password               | whatever  | 
|  3 |    NULL | blog_name              | test blog | 
|  4 |    NULL | default_allow_pings    | 1         | 
|  5 |    NULL | default_allow_comments | 1         | 
|  6 |    NULL | sp_url_limit           | 3         | 
|  7 |    NULL | sp_article_auto_close  | 300       | 
|  8 |    NULL | sp_global              | 1         | 
|  9 |    NULL | text_filter            | textile   | 
+----+---------+------------------------+-----------+
9 rows in set (0.02 sec)

mysql> 

articles テーブルの created_at が今日から 300 日より前だったら true を返却、みたい。との事で @article1 の created_at を今年の正月とかに修正したら、試験パス。まだもう数件の Failure/Error が残存。メモを纏めたら作業続行予定。

validates_each について

定義は activerecord-1.14.2/lib/active_record/validations.rb との事。中身は以下。(activerecord 1.14.2 です)

      def validates_each(*attrs)
        options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
        attrs = attrs.flatten

        # Declare the validation.
        send(validation_method(options[:on] || :save)) do |record|
          # Don't validate when there is an :if condition and that condition is false
          unless options[:if] && !evaluate_condition(options[:if], record)
            attrs.each do |attr|
              value = record.send(attr)
              next if value.nil? && options[:allow_nil]
              yield record, attr, value
            end
          end
        end
      end

うーん...

これも同様にいっちゃんケツの引数が Hash だったら options に格納している。ちなみに symbolize_keys は ActiveSupport のナニらしい。Rails API ドキュメントによると

Destructively convert all keys to symbols.
Rails API ドキュメントより引用

とある。単に pop するのとどう違うのか、は要調査、という事で。
あと、send とゆーのが何なのかさっぱり分からん。これも調べてみます。(弱

なんとなく record は AR なインスタンスを参照してて attr は列名で value はその値っつー気がするんですが根拠ゼロ。

send

わからん。

        send(validation_method(options[:on] || :save)) do |record|

って何だー。
てか、|record| に AR オブジェクトの参照が渡される根拠は??

とりあえず保留。意味的には大体掴めてると思うし。(とほほほ