Custom Validator Klasse

Der Großteil aller Modelvalidierungen wird in Ruby On Rails mit ActiveModel::Validations abgedeckt.
Allerdings gibt es mitunter die Notwendigkeit von Custom Validierung.
Dann kann es Sinn machen, sie in eine eigene Validation Klasse zu überführen.

Validierung Beispiel

Als Ausgangsbeispiel dient ein vereinfachtes User Modell:

rails g model User email:string password:string

Das Passwort soll vorhanden sein und eine Mindestlänge von 6 Zeichen haben. Zusätzlich soll die Passwortkomplexität validiert werden. In diesem Beispiel soll es entweder aus Groß-/ Kleinbuchstaben, Großbuchstaben/ Zahlen oder Kleinbuchstaben/ Zahlen bestehen:

class User < ApplicationRecord
  validates :password, presence: true
  validates :password, length: { minimum: 6 }, allow_nil: true
  validate :password_complexity

  private

  REQUIRED_PASSWORD_COMPLEXITY = 2
  PASSWORD_COMPLEXITIES = [
    /[A-Z]/,
    /[a-z]/,
    /[0-9]/
  ]

  def password_complexity
    return if password.nil?
    return if implied_complexity(password) >= REQUIRED_PASSWORD_COMPLEXITY
    errors.add(:password, I18n.t('errors.messages.complexity'))
  end

  def implied_complexity(value)
    COMPLEXITIES.select { |complexity| complexity =~ value }
                .size
  end
end

Es gibt verschiedene Gründe, warum die Validierung in eine eigene Validator Klasse überführt werden sollte:

  • bessere Testbarkeit der Validierungslogik
  • Wiederverwendbarkeit des Validators in anderen Models oder Form Objects

Custom Validatoren einführen

Zunächst muss noch der Pfad zu den Custom Validatoren für den Autoloader konfiguriert werden:

config.autoload_paths += Dir["#{config.root}/lib/custom_validations/**/"]

Custom Validator Klasse

Es gilt die Konvention für den Klassennamen: Name des Validators + Validator:

# lib/custom_validations/complexity_validator.rb
class ComplexityValidator < ActiveModel::EachValidator
  MAX_LEVEL = 3
  COMPLEXITIES = [
    /[A-Z]/,
    /[a-z]/,
    /[0-9]/
  ]

  def validate_each(subject, attribute, value)
    return if options[:allow_nil] && value.nil?
    return if implied_complexity(value.to_s) >= required_complexity
    subject.errors.add(attribute, error_message)
  end

  private

  def required_complexity
    options[:with] || MAX_LEVEL
  end

  def implied_complexity(value)
    COMPLEXITIES.select { |complexity| complexity =~ value }
                .size
  end

  def error_message
    options[:message] || I18n.t('errors.messages.complexity')
  end
end

Die Validierungslogik muss sich in der Methode validate_each befinden. Die Übergabeparameter sind das Objekt (user), das Attribut (password) und dem Wert (das Passwort). Außerdem besteht noch die Möglichkeit auf options zuzugreifen. Das sind die bekannten Standardoptionen (message, allow_nil, allow_blank, on, if, unless, strict). Mit der with Option können weitere Parameter an den Validator übergeben werden, wie man es zum Beispiel von dem LengthValidator kennt.

Custom Validator verwenden

Durch die Auslagerung der Validierungslogik in den Custom Validator, gewinnt die Lesbarkeit des Modells:

class User < ApplicationRecord
  validates :password, presence: true
  validates :password, length: { minimum: 6 },
                       complexity: 2,
                       allow_nil: true

Der ComplexityValidator wird mit der Komplexität 2 gesetzt.
Der Validator kann auch mit anderen Validatoren kombiniert werden (in diesem Fall mit dem LengthValidator).
Angenommen für Administratoren soll eine höhere Passwortkomplexität gelten, ist das sehr einfach umsetzbar. Zum Beispiel in einem Form Object für das Erstellen von Administratoren:

class AdminForm < ApplicationForm
  validates :password, presence: true
  validates :password, length: { minimum: 8 },
                       complexity: 3,
                       allow_nil: true