callbackのthrow :abortの挙動を変える

どうでもいいネタ、やってはいけないネタ。

コールバック内の`throw :abort`の挙動を調べる機会があって、その挙動を変えてみた、という内容。
`before_validation`を例にします。

### ソースを追う

以下のようにコールバックが設定されている。引数を確認。

```activemodel-5.2.2/lib/active_model/validations/callbacks.rb
      included do
        include ActiveSupport::Callbacks
        define_callbacks :validation,
                         skip_after_callbacks_if_terminated: true,
                         scope: [:kind, :name]
```

define_callbacksの定義は以下のようになっている。多分、上記Hashの部分は ```options``` として ```CallbackChain.new``` に渡されている。

```activesupport-5.2.2/lib/active_support/callbacks.rb
        def define_callbacks(*names)
          options = names.extract_options!

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

            set_callbacks name, CallbackChain.new(name, options)
```

CallbackChainクラスは、以下のようになっている。上記の ```options``` は、 ```config``` という名前になっている。 ```:terminator``` というキーは、デフォルトでは ```default_terminator``` が設定されるが、上書き可能になっている。

```activesupport-5.2.2/lib/active_support/callbacks.rb
      class CallbackChain #:nodoc:#
        include Enumerable

        attr_reader :name, :config

        def initialize(name, config)
          @name = name
          @config = {
            scope: [:kind],
            terminator: default_terminator
          }.merge!(config)
          @chain = []
          @callbacks = nil
          @mutex = Mutex.new
        end
```

```default_terminator``` は、以下のようになっている。 ```result_lambda.call``` の結果 ```:abort``` が発生するとその次の行を実行しないので ```terminate``` は ```true``` のままになる。 ```true``` の場合はチェーンが終了するようだ。

```activesupport-5.2.2/lib/active_support/callbacks.rb
          def default_terminator
            Proc.new do |target, result_lambda|
              terminate = true
              catch(:abort) do
                result_lambda.call
                terminate = false
              end
              terminate
            end
          end
```

ということで、この ```:terminator``` の値を ```default_terminator``` から別のものに上書きしてやれば ```throw :abort``` の挙動を変えることができる。（もちろん、それに意味があるとは思わない）

## 現状確認

以下のような ```Grade``` クラスがあった場合、

```app/models/Grade.rb
class Grade < ApplicationRecord
  before_validation do |grade|
    puts "before_validation 1"
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end
```

以下のように２つ実行される。

```sh
> Grade.new.valid?
before_validation 1
before_validation 2
```

以下のように ```throw :abort``` を入れた場合は、

```app/models/Grade.rb
class Grade < ApplicationRecord
  before_validation do |grade|
    puts "before_validation 1"
    throw :abort # これを追加
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end
```

以下のように２つ目が実行されないことがわかる。

```sh
> Grade.new.valid?
before_validation 1
```

## 挙動を変更

では、```:terminator```の値を上書きしてみる。以下のように ```puts "overriden"``` だけ追加したものを設定してみると、

```app/models/Grade.rb
class Grade < ApplicationRecord
  include ActiveSupport::Callbacks
  define_callbacks :validation, {
                   skip_after_callbacks_if_terminated: true,
                   scope: [:kind, :name],
                   terminator: Proc.new do |target, result_lambda|
                      puts "overriden" # ここがdefault_terminatorと異なる
                      terminate = true
                      catch(:abort) do
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                    end
                   }

  before_validation do |grade|
    puts "before_validation 1"
    throw :abort
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end
```

以下のようにオーバーライドされていることを確認できる。

```sh
> Grade.new.valid?
overridden
before_validation 1
```

では、 ```:abort``` で中断しないように変更してみる。

```app/models/Grade.rb
                   terminator: Proc.new do |target, result_lambda|
                      terminate = true
                      catch(:end) do # :abort から :end に変更してみた
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                    end
```

このようにすると、```:abort```では中断しなくなるが、、、catchされなくなってしまうため、以下のように例外が発生してしまう・・・。確かに・・・。

```sh
> Grade.new.valid?
before_validation 1
UncaughtThrowError: uncaught throw :abort
```

そのため、 ```throw :abort``` を ```throw :end``` に変更してみると、

```app/models/Grade.rb
  before_validation do |grade|
    puts "before_validation 1"
    throw :end # 投げるものも :end に変更
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end
```

以下のように正常に中断してくれる。

```sh
> Grade.new.valid?
before_validation 1
```

以下のように常に ```false``` を返すようにすれば、

```app/models/Grade.rb
                   terminator: Proc.new do |target, result_lambda|
                      terminate = true
                      catch(:abort) do
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                      false # 常に false を返す
                    end
```

```throw :abort``` では中断しなくなる。

```sh
> Grade.new.valid?
before_validation 1
before_validation 2
```

## まとめ

- ```:abort```は変更可能。
  - しかし、変更することの影響は大きい。
- 条件を追加したい、という用途はあるかもしれない。
  - 特定の例外のみ追加で補足する、とか。ないかな・・・。