12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

条件付きvalidation定義で使うwith_optionsについてちょっと調べてみた

Last updated at Posted at 2019-08-03

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.

そういう目的で使うものだったのですね。なるほど。

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?