Railsのvalidationを理解したい

## はじめに
Railsを使っていると、綺麗に隠蔽されているため`validates :name, presence: true`などを書くだけで簡単にvalidationが実装できてしまいます。
裏側にはピュアなRubyで実装されたロジックが存在するわけですが、普段あまり意識することがないです。
最近カスタムvalidationを実装する機会があり、「あれ？僕、validationの挙動を理解していないな？」となったのでまとめます。

## validates
```ruby
class Hoge < ApplicationRecord
  validates :name, presence: true
end
```
↑ のように`()`を省略して書くことが多いのでわかりにくいですが、実際のところはメソッドコールです。
なので、クラスがロードされる時点で`validates`メソッドが呼ばれています。
```ruby
class Hoge < ApplicationRecord
  validates(:name, presence: true)
end
```
この`validates`メソッドが定義されているのは、`ActiveModel::Validations::ClassMethods`です。

```ruby
pry(main)> show-source Hoge.validates

From: /usr/local/bundle/gems/activemodel-7.2.0/lib/active_model/validations/validates.rb:8:
Owner: ActiveModel::Validations::ClassMethods
Visibility: public
Signature: validates(*attributes)
Number of lines: 23

def validates(*attributes)
  defaults = attributes.extract_options!.dup
  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?

  defaults[:attributes] = attributes

  validations.each do |key, options|
    key = "#{key.to_s.camelize}Validator"

    begin
      validator = const_get(key)
    rescue NameError
      raise ArgumentError, "Unknown validator: '#{key}'"
    end

    next unless options

    validates_with(validator, defaults.merge(_parse_validates_options(options)))
  end
end
```

内部の処理では、特定のvalidationに対して責務を持つValidator(class)とattribute情報を`validates_with`メソッドに渡しています。
仮に`validates :name, presence: true`を想定すると、validates_withメソッドは以下のように呼び出されます。
```ruby
validates_with(ActiveRecord::Validations::PresenceValidator, {:attributes=>[:name]})
```

Railsのコードリーディングは迷子になってしまうこともかなりあるので、pryでeditしてprintしながら値を確認していくのが便利です。

<details><summary>例</summary><div>

```pry
pry(main)> edit Hoge.validates
# railsのソースコードに対してputsなどを仕込む
pry(main)> reload!
pry(main)> Hoge.validates(:name, presence: true)
# 出力の確認
```
</div></details>

`validates_with`メソッドは以下のようになっています。

```ruby
def validates_with(*args, &block)
  options = args.extract_options!
  options[:class] = self

  args.each do |klass|
    validator = klass.new(options.dup, &block)

    if validator.respond_to?(:attributes) && !validator.attributes.empty?
      validator.attributes.each do |attribute|
        _validators[attribute.to_sym] << validator
      end
    else
      _validators[nil] << validator
    end

    validate(validator, options)
  end
end
```
validationの対象のクラスやattributesなどを含むoptionsを引数として対象のValidatorクラスのインスタンスを生成して、validateメソッドに渡します。
先ほどと同様に`validates :name, presence: true`を想定するとvalidateメソッドは以下のように呼び出されます。
```ruby
validate(#<ActiveRecord::Validations::PresenceValidator:0x00007fffe27f1f68 @attributes=[:name], @options={}>, {:attributes=>[:name], :class=>Hoge})
```

`validate`メソッドは以下のようになっています。
```ruby
def validate(*args, &block)
  options = args.extract_options!

  if args.all?(Symbol)
    options.each_key do |k|
      unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
        raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
      end
    end
  end

  if options.key?(:on)
    options = options.merge(if: [predicate_for_validation_context(options[:on]), *options[:if]])
  end

  set_callback(:validate, *args, options, &block)
end
```

ここでは、`set_callback`でvalidationを実装する予定のクラスにvalidationを登録しています。
`set_callback`は`ActiveRecord::Transaction`に定義されていて、これはsuper呼び出しによって探索対象の[ActiveSupport::Callbacks::ClassMethodsのset_callbackメソッド](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)を呼び出します。

このset_callbackメソッドはclass_attributeである`__callbacks`に対してvalidation情報を登録します。
実際にvalidatesメソッドの呼び出し前後を確認すると`__callbacks`の`:validate`が変化していることを確認することができます。(filterが変化している)

<details><summary>validates呼び出し前後の__callbacks[:validate]</summary><div>

```pry
pry(main)> Hoge.__callbacks[:validate]
=> #<ActiveSupport::Callbacks::CallbackChain:0x00007fffe1f4a298
 @all_callbacks=nil,
 @chain=
  [#<ActiveSupport::Callbacks::Callback:0x00007fffe1f4a338
    @chain_config=
     {:scope=>:name,
      :terminator=>
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>},
    @compiled=
     #<ActiveSupport::Callbacks::Filters::Before:0x00007fffe1f4a2e8
      @filter=:cant_modify_encrypted_attributes_when_frozen,
      @halted_lambda=
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>,
      @name=:validate,
      @user_callback=
       #<Proc:0x00007fffe1f34830 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:361 (lambda)>,
      @user_conditions=
       [#<Proc:0x00007fffe1f34970 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:406 (lambda)>]>,
    @filter=:cant_modify_encrypted_attributes_when_frozen,
    @if=
     [#<Proc:0x00007fffe1f34fb0 /usr/local/bundle/gems/activerecord-7.2.0/lib/active_record/encryption/encryptable_record.rb:13 (lambda)>],
    @kind=:before,
    @name=:validate,
    @unless=[]>],
 @config=
  {:scope=>:name,
   :terminator=>
    #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>},
 @mutex=#<Thread::Mutex:0x00007fffe1f345d8>,
 @name=:validate,
 @single_callbacks={}>

pry(main)> Hoge.validates(:name, presence: true)

pry(main)> Hoge.__callbacks[:validate]
=> #<ActiveSupport::Callbacks::CallbackChain:0x00007fffe21df878
 @all_callbacks=nil,
 @chain=
  [#<ActiveSupport::Callbacks::Callback:0x00007fffe1f4a338
    @chain_config=
     {:scope=>:name,
      :terminator=>
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>},
    @compiled=
     #<ActiveSupport::Callbacks::Filters::Before:0x00007fffe1f4a2e8
      @filter=:cant_modify_encrypted_attributes_when_frozen,
      @halted_lambda=
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>,
      @name=:validate,
      @user_callback=
       #<Proc:0x00007fffe1f34830 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:361 (lambda)>,
      @user_conditions=
       [#<Proc:0x00007fffe1f34970 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:406 (lambda)>]>,
    @filter=:cant_modify_encrypted_attributes_when_frozen,
    @if=
     [#<Proc:0x00007fffe1f34fb0 /usr/local/bundle/gems/activerecord-7.2.0/lib/active_record/encryption/encryptable_record.rb:13 (lambda)>],
    @kind=:before,
    @name=:validate,
    @unless=[]>,
   #<ActiveSupport::Callbacks::Callback:0x00007fffe21df918
    @chain_config=
     {:scope=>:name,
      :terminator=>
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>},
    @compiled=
     #<ActiveSupport::Callbacks::Filters::Before:0x00007fffe21df8c8
      @filter=#<ActiveRecord::Validations::PresenceValidator:0x00007fffe28673d0 @attributes=[:name], @options={}>,
      @halted_lambda=
       #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>,
      @name=:validate,
      @user_callback=
       #<Proc:0x00007fffe2865dc8 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:384 (lambda)>,
      @user_conditions=[]>,
    @filter=#<ActiveRecord::Validations::PresenceValidator:0x00007fffe28673d0 @attributes=[:name], @options={}>,
    @if=[],
    @kind=:before,
    @name=:validate,
    @unless=[]>],
 @config=
  {:scope=>:name,
   :terminator=>
    #<Proc:0x00007fffe2028b60 /usr/local/bundle/gems/activesupport-7.2.0/lib/active_support/callbacks.rb:665>},
 @mutex=#<Thread::Mutex:0x00007fffe2865698>,
 @name=:validate,
 @single_callbacks={}>
```
</div></details>

## valid?
データの正当性を確かめる際には`valid?`メソッドをコールすると思いますが、`valid?`は`ActiveRecord::Validations`で以下のように定義されています。
```ruby
def valid?(context = nil)
  context ||= default_validation_context
  output = super(context)
  errors.empty? && output
end
```
`super`によって以下の`ActiveModel::Validations`の`valid?`がコールされますが、そこでは`run_validations`メソッドが呼ばれています。

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

# ...
def run_validations!
  _run_validate_callbacks
  errors.empty?
end
```
`run_validations!`メソッドは`_run_validate_callbacks`を呼び出し、errorsに値が入っているかどうかの判定をしています。
これが実質`valid?`メソッドの戻り値になります。
RailsのbuiltinのPresenceValidatorを見ると、データが正しくなかったときにインスタンスのerrorsにオブジェクトを入れているので納得です。

```ruby
module ActiveModel
  module Validations
    class PresenceValidator < EachValidator # :nodoc:
      def validate_each(record, attr_name, value)
        record.errors.add(attr_name, :blank, **options) if value.blank?
      end
    end
# ...
```

`_run_validate_callbacks`はメタプロされているので探しても見つかりませんが、`ActiveSupport::Callbacks::ClassMethods`の`define_callbacks`で定義されるようになっています。

```ruby
def define_callbacks(*names)
  options = names.extract_options!

  names.each do |name|
    name = name.to_sym

    ([self] + self.descendants).each do |target|
      target.set_callbacks name, CallbackChain.new(name, options)
    end

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def _run_#{name}_callbacks(&block)
        run_callbacks #{name.inspect}, &block
      end

      def self._#{name}_callbacks
        get_callbacks(#{name.inspect})
      end

      def self._#{name}_callbacks=(value)
        set_callbacks(#{name.inspect}, value)
      end

      def _#{name}_callbacks
        __callbacks[#{name.inspect}]
      end
    RUBY
  end
end
```

なので`_run_validate_callbacks`の実態は、`run_callbacks :validate`になります。
```ruby
def run_callbacks(kind, type = nil)
  callbacks = __callbacks[kind.to_sym]

  if callbacks.empty?
    yield if block_given?
  else
    env = Filters::Environment.new(self, false, nil)

    next_sequence = callbacks.compile(type)

    # Common case: no 'around' callbacks defined
    if next_sequence.final?
      next_sequence.invoke_before(env)
      env.value = !env.halted && (!block_given? || yield)
      next_sequence.invoke_after(env)
      env.value
    else
      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&.first
          break env.value
        end
      end

      invoke_sequence.call
    end
  end
end
```
この処理で、class_attributeの`__callback`に登録されたcallback処理たちが呼び出されます。
`validates`で登録された`__callback[:validate]`も同様なのでvalidationが実行されます。

ここから先ももう少し追いたかったのですが、力尽きたのでここまでにしてValidatorの`validate_each`のcallstackを貼っておきます。

<details><summary>presence: trueを設定し、valid?時に呼び出されるcallstack</summary><div>


```sh
activerecord-7.2.0/lib/active_record/validations/presence.rb:10:in `validate_each'
activemodel-7.2.0/lib/active_model/validator.rb:155:in `block in validate'
activemodel-7.2.0/lib/active_model/validator.rb:151:in `each'
activemodel-7.2.0/lib/active_model/validator.rb:151:in `validate'
activesupport-7.2.0/lib/active_support/callbacks.rb:385:in `block in make_lambda'
activesupport-7.2.0/lib/active_support/callbacks.rb:179:in `block in call'
activesupport-7.2.0/lib/active_support/callbacks.rb:670:in `block (2 levels) in default_terminator'
activesupport-7.2.0/lib/active_support/callbacks.rb:669:in `catch'
activesupport-7.2.0/lib/active_support/callbacks.rb:669:in `block in default_terminator'
activesupport-7.2.0/lib/active_support/callbacks.rb:180:in `call'
activesupport-7.2.0/lib/active_support/callbacks.rb:559:in `block in invoke_before'
activesupport-7.2.0/lib/active_support/callbacks.rb:559:in `each'
activesupport-7.2.0/lib/active_support/callbacks.rb:559:in `invoke_before'
activesupport-7.2.0/lib/active_support/callbacks.rb:109:in `run_callbacks'
activesupport-7.2.0/lib/active_support/callbacks.rb:915:in `_run_validate_callbacks'
activemodel-7.2.0/lib/active_model/validations.rb:441:in `run_validations!'
activemodel-7.2.0/lib/active_model/validations/callbacks.rb:115:in `block in run_validations!'
activesupport-7.2.0/lib/active_support/callbacks.rb:110:in `run_callbacks'
activesupport-7.2.0/lib/active_support/callbacks.rb:915:in `_run_validation_callbacks'
activemodel-7.2.0/lib/active_model/validations/callbacks.rb:115:in `run_validations!'
activemodel-7.2.0/lib/active_model/validations.rb:366:in `valid?'
activerecord-7.2.0/lib/active_record/validations.rb:71:in `valid?'
```
</div></details>

## 参考

https://guides.rubyonrails.org/active_record_validations.html

https://www.oreilly.co.jp/books/9784873117430/