Railsのvalidationの実行過程を調べる

# 環境
Ruby 2.6.5
Rails 6.0.1

# 事の発端
ある時、継承したモデルに対して、 `validation` を書こうとして問題は起きました…

例えば、下記のような `User` モデルが存在したとします。

```ruby:user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
  validates :password, presence: true
  validates :card_num, presence: true
end
```

クレカの番号をDBに生で持つなんて恐ろしいですが、サンプルなので今回は置いておきます。

元々、課金ユーザーのみが対象でしたが、ある時、無課金ユーザーを対象とするために、新しく無課金ユーザーのモデルを作成したとします。無課金ユーザーはクレカの番号を登録しなくても使えるように `:card_num` に対する `validation` を上書きしようと考えて下記のように記述してみました。

```ruby:non_paying_user.rb
class NonPayingUser < User
  validates :card_num, presence: false
end
```

では、実際はどうなるかと言うと、詳しい方はご存知のように `:card_num` に対する `presence: true` は効いてしまう。

```ruby:console
irb(main):001:0> user = NonPayingUser.new(name: "hoge", email: 'hoge@example.com', password: 'passwor
d')
=> #<NonPayingUser id: nil, name: "hoge", email: "hoge@example.com", password: [FILTERED], card_num: nil, created_at: nil, updated_at: nil>

irb(main):002:0> user.valid?
=> false

irb(main):003:0> user.errors.full_messages
=> ["Card num can't be blank"]
```

自分みたいな初心者には、親クラスと同じような定義を子クラスでした場合に、上書きされそうなイメージがありました…しかし実際は違う。

なぜだろうと思って、Railsの `validation` の実行過程を調べてみることにしました。

# `ActiveModel::Validations#valid?`
まずは `valid?` から追っていきます。
`ActiveModl::Validations#valid?` メソッドは下記のような実装になっています。
参考：[activemodel/validations.rb](https://github.com/rails/rails/blob/6-0-stable/activemodel/lib/active_model/validations.rb#L334)

```ruby:validations.rb
def valid?(context = nil)
  current_context, self.validation_context = validation_context, context
  errors.clear
  run_validations!
ensure
  self.validation_context = current_context
end
```

ここでは、`validation_context` をセットした後に、エラーを `clear` し、その後に、 `run_validations!` を呼んでいます。

その `run_validations!` メソッドの中では、 `_run_validate_callbacks` を呼んだ後に、 `errors.empty?` で `valid?` メソッドの返り値である真偽値を返しています。そのため、実態は、 `_run_validate_callbacks` メソッドになります。

```ruby:validations.rb
def run_validations!
  _run_validate_callbacks
  errors.empty?
end
```

では、 `_run_validate_callbacks`　はどこに定義されているかと言うと、 `ActiveModel` からは離れて、 `ActiveSupport` の方になります。 `define_callbacks` メソッドの中で、動的に定義されていて、内部的には、 `run_callbacks` を呼んでいます。

参考：[activesupport/callbacks.rb](https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/callbacks.rb#L826)

```ruby:callbacks.rb
def _run_#{name}_callbacks(&block)
  run_callbacks #{name.inspect}, &block
end
```

今回は、 `_run_validate_callbacks` メソッドが呼ばれているので、 `name` は `validate` になります。

`run_callbacks` メソッドは同じファイル内にあります。結構長いメソッドで、全部読むのは大変だったので、重要そうなポイントだけコメントすると下記のようになります。

```ruby:callbacks.rb
def run_callbacks(kind)
  # callbacksの内、:validateのcallbacksを取り出す。
  callbacks = __callbacks[kind.to_sym]

  # 今回の場合はcallbacksは存在するのでelseに入る
  if callbacks.empty?
    yield if block_given?
  else
    # 環境情報をセット
    # 中身は、Struct.new(:target, :halted, :value)
    env = Filters::Environment.new(self, false, nil)
    # callbacksは、ActiveSupport::Callbacks::CallbackChainクラスのインスタンスだが、
    # そのインスタンスの@callbacksメソッドに値がなければ詰める（初回のみ呼び出し）
    # 返り値は、ActiveSupport::Callbacks::CallbackSequenceのインスタンス
    next_sequence = callbacks.compile

    # invoke_sequenceにProcインスタンスを代入
    # 場合によってはあとで下記がcallされて、実行される
    invoke_sequence = Proc.new do
      skipped = nil
      while true
        current = next_sequence
        current.invoke_before(env)
        if current.final?
          env.value = !env.halted && (!block_given? || yield)
        elsif current.skip?(env)
          (skipped ||= []) << current
          next_sequence = next_sequence.nested
          next
        else
          next_sequence = next_sequence.nested
          begin
            target, block, method, *arguments = current.expand_call_template(env, invoke_sequence)
            target.send(method, *arguments, &block)
          ensure
            next_sequence = current
          end
        end
        current.invoke_after(env)
        skipped.pop.invoke_after(env) while skipped && skipped.first
        break env.value
      end
    end

    # Common case: no 'around' callbacks defined
    if next_sequence.final?
      # 今回は、next_sequence.final?はtrueだったのでこちらに入る
      # invoke_beforeで、ActiveSupport::Callbacks::CallbackSequenceのインスタンスの、
      # @beforeに含まれるcallbackのProcインスタンスを一つ一つcallして実行する
      # @before.each { |b| b.call(env) }
      # validationのコールバックが、ここで実行されていく。
      next_sequence.invoke_before(env)
      env.value = !env.halted && (!block_given? || yield)
      # afterがあればここで実行される
      next_sequence.invoke_after(env)
      env.value
    else
      invoke_sequence.call
    end
  end
end
```

ざっくりまとめると、当該モデルに設定された `callbacks` の内、 `:validate` のキーに引っかかる `callbacks` を実行していっていると言うことになります。

それでは、今回のサンプルであった、 `NonPayingUser` モデルのインスタンスの `callbacks` はどうなっているのでしょうか？

```ruby:console
[1] pry(main)> user = NonPayingUser.new(name: "hoge", email: 'hoge@example.com', password: 'password')
=> #<NonPayingUser:0x00007ffeb0271df0...省略

[2] pry(main)> user.__callbacks[:validate].instance_variable_get('@chain')
=> [#<ActiveSupport::Callbacks::Callback:0x00007ffeb5f6d298
  @chain_config=
   {:scope=>:name,
    :terminator=>
     #<Proc:0x00007ffeaf24eea0@/Users/ty/rails/private_project/testapp/vendor/bundle/gems/activesupport-6.0.1/lib/active_support/callbacks.rb:604>},
  @filter=
   #<ActiveRecord::Validations::PresenceValidator:0x00007ffeb5f6d590
    @attributes=[:name],
    @options={}>,
  @if=[],
  @key=
   #<ActiveRecord::Validations::PresenceValidator:0x00007ffeb5f6d590
    @attributes=[:name],
    @options={}>,
  @kind=:before,
  @name=:validate,
  @unless=[]>,
 #<ActiveSupport::Callbacks::Callback:0x00007ffeb5f6c9b0
...以下略
```

このように、 `validator` のcallbackが並んでいることがわかります。

ちなみに当該モデルの `validator` 一覧は、 `_validators` メソッドで取得できます。

```ruby:console
[3] pry(main)> user._validators
=> {:name=>
  [#<ActiveRecord::Validations::PresenceValidator:0x00007ffeb04e6978
    @attributes=[:name],
    @options={}>],
 :email=>
  [#<ActiveRecord::Validations::PresenceValidator:0x00007ffeb2e0b448
    @attributes=[:email],
    @options={}>],
 :password=>
  [#<ActiveRecord::Validations::PresenceValidator:0x00007ffeb2e09ad0
    @attributes=[:password],
    @options={}>],
 :card_num=>
  [#<ActiveRecord::Validations::PresenceValidator:0x00007ffeb04fcd18
    @attributes=[:card_num],
    @options={}>]}
```

一番最後の部分で、 `:card_num` に `PresenceValidator` が存在してしまっていますね…

そのため、この `validator` が `callbacks` の一つとして呼ばれて、結果的に、 `:card_num` に対する `presence: true` の `validation` が効いてしまったと言うことになります。

# 新たな疑問
ここで新たな疑問が生まれます。それじゃあ、 `presence: false` って何だったの？と。

それを解き明かすためには、 `presence: false` を設定した時に何が起こるかを見ていく必要があります。それでは `validates` メソッドを見ていきます。こちらも長かったので、重要そうな部分だけコメントしています。

[active_model/validations/validates.rb](https://github.com/rails/rails/blob/6-0-stable/activemodel/lib/active_model/validations/validates.rb#L105)

```ruby:validates.rb
def validates(*attributes)
  # attributesの内、オプションだけ取り出す
  defaults = attributes.extract_options!.dup
　# オプションの内、validator以外のオプションを取り出す
  # _validates_default_keysは、
  # [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
  validations = defaults.slice!(*_validates_default_keys)

  raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
  raise ArgumentError, "You need to supply at least one validation" if validations.empty?

  # attribute名たちをここで詰める
  defaults[:attributes] = attributes

  # validation毎にvalidatorをセットする
  validations.each do |key, options|
    # もしオプションがnil or falseならセットしない
    next unless options
    key = "#{key.to_s.camelize}Validator"

    begin
      # validatorをセット
      validator = key.include?("::") ? key.constantize : const_get(key)
    rescue NameError
      raise ArgumentError, "Unknown validator: '#{key}'"
    end

    # validatorを設定する
    validates_with(validator, defaults.merge(_parse_validates_options(options)))
  end
end
```

少しコメントだけだとわかりにくいと思うので、具体例で見ていきます。

例えば、  `User` モデルに下記のように `validation` をセットしたとします。

```ruby:user.rb
class User < ApplicationRecord
  validates :name, :email, presence: true, length: { maximum: 500 }, allow_nil: true
end
```

この場合、引数の `attributes` は下記のような形になります。

```ruby
[:name, :email, {:presence=>true, :length=>{:maximum=>500}, :allow_nil=>true}]
```

ここで、 `defaults = attributes.extract_options!.dup` すると `option` 部分、すなわち、 `{:presence=>true, :length=>{:maximum=>500}, :allow_nil=>true}` が取り出されます。

次に、 `validations = defaults.slice!(*_validates_default_keys)` で、バリデーションオプション（`[:if, :unless, :on, :allow_blank, :allow_nil, :strict]`）以外が取り出されます。この時点で、各変数の中身は下記のようになります、

```ruby
attributes = [:name, :email]
defaults = {:allow_nil=>true}
validations = {:presence=>true, :length=>{:maximum=>500}}
```

そして、 `attributes` の中身を `defaults[:attributes]` にセットし、各 `validation` 毎に、 `attributes` に対して、 `validator` を作っていくという感じです。

さて、ここで冒頭の `presence: false` は何をしているかということなのですが、 `validations.each` の次の行で、 `next unless options` とあります。 `presence: false` の場合の、 `options` は `false` です。そのため、 `presence: false` がやっていることは、 `validator` を作らないということだけで、既存の `validator` を上書きしたりはしないのです。

だから、子クラスで `presence: false` をしても全く意味がなかったということです。

ようやく腑に落ちました！

# 副次的なtips
ちなみに、同じ `validator` かどうかチェックしていないので、下記のように二重に `validates` すると、二重に `validator` が設定されてしまいます。

```ruby:user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :name, presence: true
end
```

```ruby:console
[2] pry(main)> user._validators
=> {:name=>
  [#<ActiveRecord::Validations::PresenceValidator:0x00007ffeb66c4398
    @attributes=[:name],
    @options={}>,
   #<ActiveRecord::Validations::PresenceValidator:0x00007ffeb66cf9a0
    @attributes=[:name],
    @options={}>]}
```

# 最後に
以上になります！

普段、何気なく使っている `validation` の仕組みの一端を知れて面白かったです。調べながら書いたので、もし間違っている点等ありましたら、ご指摘いただけると幸いです。

最後までお読みいただきありがとうございました！