with_optionsを使ったvalidation
以下のような感じで、with_optionsを使った条件付きvalidationを定義することがあると思います。
class Hoge < ApplicationRecord
with_options on: :some_case do |hoge|
hoge.validates :name, presence: true
hoge.validates :description, length: { minimum: 10 }
end
end
このように書くと、Hoge.new.valid?(:some_case)って書いたときに↑のvalidationを呼ぶことができます。
特に気にせず、そういうイディオムみたいなもの、と思って使っていましたが、このコード、内部の構造読んでみるとなかなかおもしろかったので触りだけまとめてみます。
ブロック変数は何のObject?
上のコードでいうhogeは何のObjectなんでしょうか?
なんとなく、valid?を読んだときのレシーバー、つまりそのclassのインスタンスなのかと思ってましたが、中身を覗いてみるとActiveSupport::OptionMergerというクラスのオブジェクトが出てきます。
こんなclassです
require "active_support/core_ext/hash/deep_merge"
module ActiveSupport
class OptionMerger #:nodoc:
instance_methods.each do |method|
undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
end
def initialize(context, options)
@context, @options = context, options
end
private
def method_missing(method, *arguments, &block)
if arguments.first.is_a?(Proc)
proc = arguments.pop
arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else
arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
end
@context.__send__(method, *arguments, &block)
end
end
end
このclass、黒いな。。。
ど頭でinstance_evalやclassやobject_id以外をundef_methodして、その上でmethod_missingを上書きしてますね。一体何が始まるのか期待感が高まります。
hashのdeep_mergeなんてのも初めて見ました。
with_optionsの定義場所
バリデーションをグルーピングするのに使用しているwith_optionsのほうも気になります。
with_options、validationのときくらいしか見ないので、てっきりActiveModel::Validationとかにあるのかと思いきや
ActiveSupportに定義がありました。
def with_options(options, &block)
option_merger = ActiveSupport::OptionMerger.new(self, options)
block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end
with_optionsに渡されたブロックにブロック引数があればOptionMergerを引数として渡してblockをcallします。
これがバリデーションをグルーピングするときの挙動にあたるわけです。
with_options on: :some_case do |hoge|
hoge.validates :name, presence: true
hoge.validates :description, length: { minimum: 10 }
end
このhogeが前述の通り、OptionMergerのインスタンスです。
OptionMergerにvalidatesはないので、method_missingに拾われ、最終的に以下に行き着きます
@context.__send__(method, *arguments, &block)
@ contextは、with_optionsのself、methodはvalidates、*argumentsは [:name, presence: true] -> [:description, length: { minimum: 10 }]が順番に渡され、&blockはありません。
黒いですね。
with_optionsのコメントを読むと以下の様なことが書いてあります
An elegant way to factor duplication out of options passed to a series of method calls.
そういう目的で使うものだったのですね。なるほど。