前回の記事では1つの属性に関するバリデーションルールを共通化出来るActiveModel::EachValidator
について解説した。
そこで今回は、Railsのバリデーションを定義するためのもう一つの基底クラスである、ActiveModel::Validator
について解説したいと思う。
ActiveModel::Validator
は複数の属性を組み合わせたバリデーションルールなど、より複雑なルールを定義する際に利用される。
例えば、イベントを管理するモデルを考えて、そのイベントの開始日時と終了日時の差は24時間以内であるというケースを例に実装からテストまで行ってみる。
使い方
Eventモデルは下記のような構成になっているとする。
ここで、start_atとend_atの差が24時間以内であるというバリデーションを設定したい。
# app/models/event.rb # == Schema Information # # Table name: events # # id :bigint not null, primary key # name :string default(""), not null # content :text default(""), not null # start_at :datetime not null # end_at :datetime not null # created_at :datetime not null # updated_at :datetime not null class Organization < ApplicationRecord validates_presence_of %i( name content start_at end_at ) validate :valide_difference_between_start_at_and_end_at private def valide_difference_between_start_at_and_end_at if (Time.zone.parse(end_at) - Time.zone.parse(start_at)) / 3600 > 24 errors.add(:base, '開始日時と終了日時の差は24時間以内にしてください') end end end
上記のように記述しても問題ないのだが、他のモデルでも同じバリデーションルールを使いたい時や、ファットモデルになってきた時に不都合が出てくる可能性がある。
そこで、ActiveModel::Validator
を使ってバリデーションルールだけ外に切り出してみよう。
# app/validators/event_range_validator.rb class EventRangeValidator < ActiveModel::Validator # マジックナンバーは定数化する MAX_HOUR = 24 SECONDS_OF_AN_HOUR = 3600 def validate(record) if (Time.zone.parse(record.end_at) - Time.zone.parse(record.start_at)) / SECONDS_OF_AN_HOUR > MAX_HOUR # 特定の属性に属さないエラーはbaseに格納する record.errors.add(:base, '開始日時と終了日時の差は24時間以内にしてください') end end end
上記で作成したバリデーションを、下記のように使う。
# app/models/event.rb # == Schema Information # # Table name: events # # id :bigint not null, primary key # name :string default(""), not null # content :text default(""), not null # start_at :datetime not null # end_at :datetime not null # created_at :datetime not null # updated_at :datetime not null class Organization < ApplicationRecord validates_presence_of %i( name content start_at end_at ) validates_with EventRangeValidator, unless: -> { start_at.blank? || end_at.blank? } end
モデルをスッキリさせることが出来た。
テストを書く
前回同様、Vlidatorのテストを書いてみる。
StructクラスにActiveModel::Validations
をincludeする。
spec/validators/event_range_validator_spec.rb RSpec.describe EventRangeValidator, type: :validator do let(:clazz) do Struct.new(:start_at, :end_at) do include ActiveModel::Validations validates_presence_of %i( start_at end_at ) validates_with EventRangeValidator, unless: -> { start_at.blank? || end_at.blank? } end end describe '#validate' do subject { clazz.new(start_at, end_at) } context '差が21時間' do let(:start_at) { '2021-01-01 01:00:00' } let(:end_at) { '2021-01-01 22:00:00' } it { is_expected.to be_valid } end context '差が24時間' do let(:start_at) { '2021-01-01 01:00:00' } let(:end_at) { '2021-01-02 01:00:00' } it { is_expected.to be_valid } end end describe '異常系' do subject { clazz.new(start_at, end_at) } context '差が25時間' do let(:start_at) { '2021-01-01 01:00:00' } let(:end_at) { '2021-01-02 02:00:00' } it { is_expected.to be_valid } end context '差が48時間' do let(:start_at) { '2021-01-01 01:00:00' } let(:end_at) { '2021-01-03 01:00:00' } it { is_expected.to be_valid } end end end
まとめ
複数の属性に対してバリデーションルールを設定出来るActiveModel::Validator
を解説した。
前回の記事と合わせて読めば、自由自在にバリデーションを共通化させられるのではないだろうか。