前回の記事では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
を解説した。
前回の記事と合わせて読めば、自由自在にバリデーションを共通化させられるのではないだろうか。