# app/validators/order_validator.rb
class OrderValidator < ActiveModel::Validator
def validate(record)
validate_order_total(record)
validate_items_availability(record)
validate_shipping_address(record)
end
private
def validate_order_total(record)
calculated_total = record.items.sum(&:price)
if record.total != calculated_total
record.errors.add(:total, 'does not match item prices')
end
end
def validate_items_availability(record)
record.items.each do |item|
unless item.in_stock?
record.errors.add(:base, "Item #{item.name} is out of stock")
end
end
end
def validate_shipping_address(record)
if record.requires_shipping? && record.shipping_address.blank?
record.errors.add(:shipping_address, 'is required for physical items')
end
end
end
class Order < ApplicationRecord
has_many :items
belongs_to :shipping_address, optional: true
validates_with OrderValidator
end
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
def validate_each(record, attribute, value)
return if value.blank?
unless value.match?(REGEX)
record.errors.add(attribute, options[:message] || 'is not a valid email')
end
if options[:check_mx] && !valid_mx_record?(value)
record.errors.add(attribute, 'domain does not exist')
end
end
private
def valid_mx_record?(email)
domain = email.split('@').last
Resolv::DNS.open { |dns| dns.getresources(domain, Resolv::DNS::Resource::IN::MX).any? }
rescue
false
end
end
# Usage in model:
class User < ApplicationRecord
validates :email, email: true
validates :backup_email, email: { check_mx: true, allow_blank: true }
end
class User < ApplicationRecord
# Built-in validations
validates :email, presence: true, uniqueness: { case_sensitive: false }
validates :age, numericality: { greater_than_or_equal_to: 18, less_than: 120 }
validates :username, length: { minimum: 3, maximum: 20 }
validates :website, url: true, allow_blank: true
# Format validation with regex
validates :phone, format: {
with: /\A\+?[1-9]\d{1,14}\z/,
message: 'must be a valid phone number'
}
# Custom inline validation
validate :password_complexity
validate :email_domain_allowed, if: :email_changed?
# Conditional validations
validates :company, presence: true, if: :business_account?
validates :tax_id, presence: true, if: -> { business_account? && country == 'US' }
# Validation on specific actions
validates :terms_accepted, acceptance: true, on: :create
validates :current_password, presence: true, on: :update
# Array/inclusion validations
validates :role, inclusion: { in: %w[admin user guest] }
validates :tags, length: { maximum: 10 }
# Associated record validations
validates_associated :profile
private
def password_complexity
return if password.blank?
errors.add(:password, 'must include at least one uppercase letter') unless password.match?(/[A-Z]/)
errors.add(:password, 'must include at least one number') unless password.match?(/\d/)
errors.add(:password, 'must include at least one special character') unless password.match?(/[!@#$%^&*]/)
end
def email_domain_allowed
domain = email.split('@').last
allowed_domains = ENV['ALLOWED_EMAIL_DOMAINS'].to_s.split(',')
if allowed_domains.any? && !allowed_domains.include?(domain)
errors.add(:email, "domain #{domain} is not allowed")
end
end
def business_account?
account_type == 'business'
end
end