結論
TracePoint.trace(:end)
を使う。
例
問題
Rails でモデルのバリデーションが少なくとも 1 つは定義されることを保証するためのモジュールを実装したい。実装は次のイメージ。
module WithSomeValidators
extend ActiveSupport::Concern
class NoValidatorError < StandardError; end
included do
# バリデータが存在しない場合は NoValidatorError を発生させる。
raise(NoValidatorError) unless validators.present?
end
end
class Girl < ApplicationRecord
include WithSomeValidators
validates :name, presence: true
end
しかし、このコードでは Girl クラスでバリデーションを定義しているにもかかわらず NoValidatorError が発生してしまう。これは include WithSomeValidators
を呼んだ時点、つまり validates を呼ぶ前にバリデータの存在を確認してしまうためだ。
class Girl < ApplicationRecord
include WithSomeValidators # この時点ではバリデータ (Girl.validators) は存在しない。
validates :name, presence: true # ここでバリデータが追加される。
end
しかし、だからといって
class Girl < ApplicationRecord
validates :name, presence: true
include WithSomeValidators
end
のように、バリデーションの定義漏れを避けるためのモジュールをバリデーション定義後に include するのは本末転倒だ。
解決策
Girl クラスの定義が終わった後に Girl.validators
を確認できればよい。これを実現するために TracePoint.trace を使い、end
イベントをトレースする。
module WithSomeValidators
extend ActiveSupport::Concern
class NoValidatorError < StandardError; end
included do
TracePoint.trace(:end) do |tp|
break unless tp.self == self
tp.disable # トレースを有効にしたままにするとパフォーマンス低下を招く。
raise(NoValidatorError) unless validators.present?
end
end
end
class Girl < ApplicationRecord
include WithSomeValidators # NoValidatorError は発生しない。
validates :name, presence: true
end
参考
- class TracePoint
-
TracePoint.new
- トレース可能なイベントの一覧が記載されている。