Engineering from Scratch

エンジニア目指してます

Railsの Custom Validation

RailsのCustom Validationを初めて使ったから,まとめる。参考資料はRailsガイド。

railsguides.jp

Record ごとのValidation

以下のHuman Classを考える。ここでいうrecordとは,Human のオブジェクトをを指す。

iclass Human
  include ActiveModel::Validations

  attr_reader :first_name, :last_name
  
  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end
end

Human classに対して,以下のCustom Validation クラスを定義する。

class HumanValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any? { |field| record.send(field) == "Taro" }
      record.errors.add(:base, "これは太郎だ")
    end

    if options[:first_name] == 'Hanako'
      record.errors.add(:base, 'これは花子だ')
    end

    if options[:initial_value] == 'Jiro'
      record.errors.add(:base, 'これは次郎だ')
    end
  end
end

そして,Humanクラスでは,以下のように,Custom Validation を呼び出す。

class Human
  略

  validates_with HumanValidator, fields: [:first_name, :last_name], first_name: @first_name, initial_value: 'Jiro'
end

validates_with メソッドで,呼び出したいValidation Class を書き,options で,好きなhashを定義でき,Custom Validation のクラスで,options[:initial_value]のように呼び出せる。

バリデーションを一つずつ確認していく。まずは一つ目。

    if options[:fields].any? { |field| record.send(field) == "Taro" }
      record.errors.add(:base, "これは太郎だ")
    end
irb(main):008:0> human = Human.new(first_name: 'Taro', last_name: 'Yamada')
=> #<Human:0x00007fe5b6031d28 @first_name="Taro", @last_name="Yamada">
irb(main):009:0> human.valid?
=> false
irb(main):010:0> human.errors.full_messages
=> ["これは太郎だ", "これは次郎だ"]

record.send(field) == "Taro"で,first_name,last_name'Taro'という文字列が存在するかのバリデーションを行い,今回は存在するため,エラーメッセージがHuman オブジェクトに格納されている。

二つ目のバリデーション。

    if options[:first_name] == 'Hanako'
      record.errors.add(:base, 'これは花子だ')
    end

validates_withにoptionsとして,first_name: :first_nameを指定している。この時のエラーメッセージは以下となる。

irb(main):011:0> human = Human.new(first_name: 'Hanako', last_name: 'Yamada')
=> #<Human:0x00007fe5b23bddb0 @first_name="Hanako", @last_name="Yamada">
irb(main):012:0> human.valid?
=> false
irb(main):013:0> human.errors.full_messages
=> ["これは次郎だ"]

これは花子だのエラーが格納されそうだが,されていない。Railsガイドには以下のように書いてある。

このバリデータは、アプリケーションのライフサイクル内で一度しか初期化されない点にご注意ください。バリデーションが実行されるたびに初期化されることはありません。インスタンス変数の扱いには十分ご注意ください。

つまり,validates_withのoptionsとして加えたfirst_name: @first_nameの@first_nameは初期化時点ではnilであるため,Humanクラスのinitialize時の@first_name = Hanakoは使われることはない。以下のバリデーションをHumanValidatorに追加してみる。

    if options[:first_name] == nil
      record.errors.add(:base, 'nilが初期値です')
    end

そして,もう一度Humanインスタンスを作成すると,

irb(main):001:0> human = Human.new(first_name: 'Hanako', last_name: 'Yamada')
=> #<Human:0x00007ff03a085b10 @first_name="Hanako", @last_name="Yamada">
irb(main):002:0> human.valid?
=> false
irb(main):003:0> human.errors.full_messages
=> ["これは次郎だ", "nilが初期値です"]

のように,nilが初期値ですのエラーが格納されており,アプリケーションの初期化時点では@first_namenilとなっていることがわかる。

最後のValidationは,

    if options[:initial_value] == 'Jiro'
      record.errors.add(:base, 'これは次郎だ')
    end

initial_valueJiroを渡しているため,想定通りのエラーが常に格納されている。

AttributeごとのValidation

HumanクラスのAttribute,今回の場合は,first_name, last_nameに対してValidationを追加する。

最初にfirst_nameattributeに対してValidationを追加する。Custom Validationは以下となる。

class HumanFirstNameValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, 'が次郎です') if value == 'Jiro'
  end
end

Humanクラスで以下のように呼び出す。

class Human
  略

  validates :first_name, human_first_name: true
end
irb(main):009:0> human = Human.new(first_name: 'Jiro', last_name: 'Yamada')
=> #<Human:0x00007fc5036e0328 @first_name="Jiro", @last_name="Yamada">
irb(main):010:0> human.valid?
=> false
irb(main):011:0> human.errors.full_messages
=> ["First name が次郎です"]

first_nameに対して,Custom Validationがかけられていることが確認できた。

last_nameに対しても,以下のように同様にValidationを追加することができる。

class HumanLastNameValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, 'が山田です') if value == 'Yamada'
  end
end
class Human
  略

  validates :first_name, human_first_name: true
  validates :last_name, human_last_name: true
end
irb(main):016:0> human = Human.new(first_name: 'Hanako', last_name: 'Yamada')
=> #<Human:0x00007fc507b880c0 @first_name="Hanako", @last_name="Yamada">
irb(main):017:0> human.valid?
=> false
irb(main):018:0> human.errors.full_messages
=> ["Last name が山田です"]

Attributeに対するCustom Validationは,emailやURLのフォーマットチェックといった少し複雑な処理だが,汎用的なAttributeに対して使用するのが主な目的となる。