🔰
カスタムバリデーションはカスタムバリデータクラスで書こう
この記事でわかること
-
ActiveModel::EachValidator
の使い方 - テストの書き方
事の起こり
ある案件で wareki gem を使って入力された日付を和暦に変換して表示しています。
ある日お客様が入力した日付に誤りがあり、和暦が存在しない年がデータベースに登録され、ページを表示する際にエラーが発生し、表示できなくなってしまいました。
その際はデータを修正することで復旧しましたが、次回から同じ誤りが発生しないよう、バリデーションを追加することになりました。
カスタムメソッドで書く
カスタムメソッドで実装すると、例えばこうなります。
app/models/partient.rb
class Patient < ApplicationRecord
validate :validate_birthdate
private
def validate_birthdate
return unless birthdate.present?
Wareki::Date.jd(birthdate)
rescue Wareki::UnsupportedDateRange
errors.add :birthdate, "に対応する和暦が見つかりません(#{birthdate})"
end
end
カスタムメソッドとして実装した後、和暦として正しいか?判定するロジックは、他の属性に対しても使用したいと思いました。でも、属性ごとに同じロジックを書くのはDRYでないですね。そもそも、ある日付をどのようなロジックで判定するかというのは、 Partient クラスの関心ごとなのでしょうか? 誕生日が和暦に対応した日付であることは確認したいですが、具体的に和暦と対応するかどうかは、Patientクラスは知る必要がないと思います。
カスタムバリデータクラス使う
同じバリデーションのロジックを複数の属性や ActiveRecord / ActiveModel で使いまわしたいときは、カスタムバリデータが便利です。
app/models/wareki_validator.rb
# validates :attribute_name, wareki: true
# で使用したい場合、wareki を PascalCase + Validator という命名にする
class WarekiValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
date = value.respond_to?(:to_date) ? value.to_date: value
if date.present?
begin
Wareki::Date.jd(date)
rescue Wareki::UnsupportedDateRange
record.errors.add attribute, "に対応する和暦が見つかりません(#{date.to_s})"
end
end
end
end
app/models/partient.rb
class Patient < ApplicationRecord
validates :birthdate, wareki: true
validates :joined_on, presence: true, wareki: true
end
app/models/document.rb
class Document < ApplicationRecord
validates :valid_until, wareki: true
end
テストを書く
例えば次のようにして、カスタムバリデータのテストを書けます。
test/models/wareki_validator_test.rb
require 'test_helper'
class WarekiVarlidatorTest < ActiveSupport::TestCase
class WarekiModel
include ActiveModel::Validations
attr_accessor :date
validates :date, wareki: true
def initialize(date)
self.date = date
end
end
test "validate wareki within nil" do
record = WarekiModel.new(nil)
assert record.valid?
end
test "validate wareki within valid Date" do
record = WarekiModel.new(Date.new(2024, 1, 1))
assert record.valid?
end
test "validate wareki within invalid Date" do
record = WarekiModel.new(Date.new(1, 1, 1))
assert_not record.valid?
assert_equal ["に対応する和暦が見つかりません(0001-01-01)"], record.errors[:date]
end
test "validate wareki within valid Time" do
record = WarekiModel.new(Time.new(2024, 1, 1))
assert record.valid?
end
test "validate wareki within invalid Time" do
record = WarekiModel.new(Time.new(1, 1, 1))
assert_not record.valid?
assert_equal ["に対応する和暦が見つかりません(0001-01-03)"], record.errors[:date]
end
test "validate wareki within valid TimeWithZone" do
record = WarekiModel.new(Time.zone.local(2024, 1, 1))
assert record.valid?
end
test "validate wareki within invalid TimeWithZone" do
record = WarekiModel.new(Time.zone.local(1, 1, 1))
assert_not record.valid?
assert_equal ["に対応する和暦が見つかりません(0001-01-03)"], record.errors[:date]
end
test "validate wareki within valid String" do
record = WarekiModel.new("2024-01-01")
assert record.valid?
end
test "validate wareki within invalid String" do
record = WarekiModel.new("0001-01-01")
assert_not record.valid?
assert_equal ["に対応する和暦が見つかりません(0001-01-01)"], record.errors[:date]
end
end
まとめ
カスタムバリデータを使うことで、バリデーションのロジックを特定のモデルから抽出することができました。
カスタムバリデータを使うことで、複数のモデルクラスで共有できるほか、 モデルクラスから本質的でないロジックを追い出し、モデルやテストの関心ごとを明確にし、コードやテストを見通し良く保つことができます。
Discussion