アプリケーションを開発していると、電話番号やURL、カナなど複数のモデルで同じバリデーションを定義していることがある。 上記の例くらいなら、同じバリデーションルールを複数箇所で書いても良さそうだが、変更が生じた時やもう少し複雑なバリデーションを記述する場合、面倒なことになる。 そこで今回は、Railsではどうやってバリデーションルールを共通化させるかについて解説する。

まず、Railsではこのようなルールを定義して利用するための基底クラスとして、ActiveModel::EachValidatorActiveModel::Validatorが用意されている。 本記事ではまずActiveModel::EachValidatorについての解説を行い、次回の記事でActiveModel::Validatorの解説をしたいと思う。

ActiveModel::EachValidatorとは

ある1つの属性のバリデーションルールを定義する時に利用する。例えば電話番号、メールアドレス、郵便番号、URL、カナなどのフォーマットである。

使い方

例えば電話番号のバリデーションルールを定義したい時は、下記のように書く。

# app/validators/tel_format_validator.rb

class TelFormatValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    # 電話番号のフォーマットかどうかを確認したいので、空文字は許可
    return if options[:allow_blank] && value.length.zero?

    # 固定電話と携帯番号(ハイフンなし10桁 or 11桁)を許可
    unless value =~ /\A\d{10}$|^\d{11}\z/
      record.errors[attribute] << (options[:message] || '電話番号の形式に誤りがあります')
    end
  end
end 

ActiveModel::EachValidatorを継承したクラスでは、validate_eachというインスタンスメソッドにバリデーションルールを実装し、その引数であるrecordattributevalueにはそれぞれ、対象のオブジェクト、対象の属性、値が入る。
options[:message]と書くことで、オプションとしてmessageパラメータの文字列が送られてきた時に、バリデーションメッセージを自由に設定することも出来る。

上で実装出来たら、モデルのvalidatesメソッドのオプションとして使える。

# app/models/user.rb

class User < ApplicationRecord
  validates :tel, presence: true, tel_format: true
  validates :tel2, presence: true, tel_format: { message: '電話番号2の形式に誤りがあります' }
end

テストを書く

Vlidatorのテストを書いてみる。実際のユースケースに合わせて、StructクラスにActiveModel::Validationsをincludeする。

spec/validators/tel_format_validator_spec.rb

RSpec.describe TelFormatValidator, type: :validator do
  let(:clazz) do
    Struct.new(:tel) do
      include ActiveModel::Validations
      validates :tel, presence: true, tel_format: true
    end
  end

  describe '#validate_each' do
    subject { clazz.new(tel) }

    context '携帯電話の様式' do
      let(:tel) { '09012345678' }
      it { is_expected.to be_valid }
    end

    context '固定電話の様式' do
      let(:tel) { '0312345678' }
      it { is_expected.to be_valid }
    end
  end

  describe '異常系' do
    subject { clazz.new(tel) }

    context '9桁の電話番号' do
      let(:tel) { '031234567' }
      it { is_expected.to be_invalid }
    end

    context '12桁の電話番号' do
      let(:tel) { '090123456789' }
      it { is_expected.to be_invalid }
    end
  end
end

まとめ

1つの属性に対してバリデーションルールを設定できるActiveModel::EachValidatorを解説した。 「じゃあ、2つ以上の属性が絡み合うバリデーションの場合」はどうするの?と疑問に思うと思うので、次回は複数の属性に関するバリデーションルールを設定出来るActiveModel::Validatorを解説したいと思う。