Railsのvalidates_withメソッドを読む
前回 validates
メソッドは内部的には validates_with
を呼び出しているだけということがわかったので、当然 validates_with
メソッドも読んでいく
ファイルがrequireされたり、include/extendされる点については前回書いたので省略する
Rails: 7.0.0.alpha
Ruby: 2.7.3
validates :name, presence: true
をBookクラスに書いた時の処理を追っているものとする
validates_withメソッド
メソッドはここにある
def validates_with(*args, &block)
options = args.extract_options!
options[:class] = self
args.each do |klass|
validator = klass.new(options, &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
まず、引数のargsには [ActiveRecord::Validations::PresenceValidator, {:attributes=>[:name]}]
こんな感じの配列が渡ってくる
blockは渡していないのでnil
options = args.extract_options!
extract_options!
メソッドは前回も使われていたが、RailsがRubyのArrayクラスを拡張して追加されているメソッド
def extract_options!
if last.is_a?(Hash) && last.extractable_options?
pop
else
{}
end
end
# 同ファイルでHashクラスに追加されているメソッド
def extractable_options?
instance_of?(Hash)
end
argsのlastは { attributes: [:name] }
というハッシュなので、{ attributes: [:name] }
がoptionsとなる
options[:class] = self
validates
を呼び出したのは自分が用意したモデルのクラスコンテキストなので、当然self(=レシーバ)は自分が作成したモデルクラスとなる
なのでoptionsは { class: Book, attributes: [:name] }
となる
その後、eachでargsを回していくが、argsは破壊的に変更されているため、 [ActiveRecord::Validations::PresenceValidator]
へのeachとなり
ActiveRecord::Validations::PresenceValidator.new({ class: Book, attributes: [:name] }, nil)
をvalidatorとして取得している
validatorの中身は↓
class PresenceValidator < EachValidator
def validate_each(record, attr_name, value)
record.errors.add(attr_name, :blank, **options) if value.blank?
end
end
initialize
メソッドは親クラスのEachValidatorとその親クラスのValidatorに定義されており
それぞれ以下にコメントしたような処理を行なっている
class EachValidator < Validator
attr_reader :attributes
# optionsとして {:attributes=>[:name], :class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)} が渡ってくる
def initialize(options)
@attributes = Array(options.delete(:attributes)) # [:name] が @attributes に入る
raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
super # Validatorクラスのinitializeを呼んでいる
# 初期化時に呼び出されるものの、Railsとしては何もしない(?)メソッド
# 自分でオーバーライドして、独自の検証を行なったりするためのもの?
check_validity!
end
def check_validity!
end
end
class Validator
attr_reader :options
# optionsとして {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)} が渡ってくる
# attributesはこの前の処理でdeleteで消えている
def initialize(options = {})
# exceptメソッドはRailsによって拡張(active_support/core_ext/hash/except.rb)されているメソッド
# optionsから:classキーを除外したものを返す
# よって @options はfreezeされた空のハッシュ({})となる
@options = options.except(:class).freeze
end
end
初期化部分を確認したので、その後の処理を見ていく
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)
validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes
は [:name]
となるので、この部分はtrueとなり、 [:name]
に対してeachが回っていく
ここで利用されている _validators
は active_model/validations.rb:52
で定義されている class_attribute
によるもので
class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
こんな感じで定義されている
class_attributeを読むだけで記事になりそうなので、ここでは内部の細かい処理は省略する
インスタンスからは書き換えられないようになっていて、空のハッシュがセットされているということがわかる
あと、定数やクラス変数との違いを解説しているこの記事が利点などを理解できてよかったので置いておく
忘れそうになるが、ここはBookクラスのコンテキストなので Book._validators
を呼べるということである
_validators[attribute.to_sym] << validator
_validators
は空のハッシュなので、 { name: ActiveRecord::Validations::PresenceValidatorのインスタンス(@attributesに[:name]が、@optionsに{}が入っている) }
となる
最後に
- validator =
ActiveRecord::Validations::PresenceValidatorのインスタンス(@attributesに[:name]が、@optionsに{}が入っているもの)
- options =
{:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}
を引数に validate
メソッドを呼び出している
なんと今度は自身で独自のvalidationを行いたい時に使う validate
メソッドが呼び出されていた!
長くなってきたので今回はここまでにして、次回 validate
メソッドの内部処理を見ていく
最後に
validates
も validates_with
も validate
もそれぞれ独立したメソッドだと思っていたので、再利用具合が凄くて美しく無駄がないな...
こんなコード書けるようになりたい