Validators

メール、URL、IPアドレスなど包括的なバリデーター関数のコレクション。ActiveModelと統合可能な実用的なバリデーションライブラリ

validationemailurlipactivemodelrailsruby

Validators

Validatorsは、Rubyアプリケーションで一般的に使用されるバリデーション機能を提供する包括的なライブラリです。メール、URL、IPアドレス、クレジットカード番号など、様々な形式のデータに対する実用的なバリデーターを提供し、ActiveModelと簡単に統合できます。

特徴

  • 包括的なバリデーター: メール、URL、IP、クレジットカードなど多様な形式に対応
  • ActiveModel統合: Railsモデルでシームレスに使用可能
  • カスタマイズ可能: 各バリデーターの設定をカスタマイズ可能
  • パフォーマンス重視: 効率的な正規表現とアルゴリズムを使用
  • 国際化対応: 多言語でのエラーメッセージサポート
  • 軽量: 必要最小限の依存関係
  • 実用性重視: 実際のWebアプリケーションで必要な機能に特化

インストール

Gemfileに追加:

gem 'validators'

またはgemで直接インストール:

gem install validators

基本的な使い方

単体でのバリデーション

require 'validators'

# メールアドレスの検証
Validators::EmailValidator.valid?('[email protected]')  # => true
Validators::EmailValidator.valid?('invalid-email')     # => false

# URLの検証
Validators::UrlValidator.valid?('https://example.com') # => true
Validators::UrlValidator.valid?('not-a-url')           # => false

# IPアドレスの検証
Validators::IpValidator.valid?('192.168.1.1')         # => true
Validators::IpValidator.valid?('999.999.999.999')     # => false

ActiveModelでの使用

class User < ActiveRecord::Base
  validates :email, presence: true, email: true
  validates :website, url: true, allow_blank: true
  validates :ip_address, ip: true, allow_blank: true
  validates :credit_card, credit_card: true, allow_blank: true
end

user = User.new(email: 'invalid-email')
user.valid?  # => false
user.errors.full_messages
# => ["Email is not a valid email"]

利用可能なバリデーター

メールバリデーター

class User < ActiveRecord::Base
  # 基本的なメールバリデーション
  validates :email, email: true
  
  # MXレコードチェック付き(追加設定が必要)
  validates :business_email, email: { mx: true }
  
  # 使い捨てメールアドレスの禁止
  validates :primary_email, email: { disposable: false }
end

# 単体使用
Validators::EmailValidator.valid?('[email protected]')     # => true
Validators::EmailValidator.valid?('[email protected]')     # => true
Validators::EmailValidator.valid?('invalid.email')       # => false

URLバリデーター

class Website < ActiveRecord::Base
  # 基本的なURLバリデーション
  validates :url, url: true
  
  # HTTPSのみ許可
  validates :secure_url, url: { scheme: 'https' }
  
  # 特定のドメインのみ許可
  validates :api_endpoint, url: { host: ['api.example.com', 'staging.example.com'] }
end

# 単体使用
Validators::UrlValidator.valid?('https://example.com')        # => true
Validators::UrlValidator.valid?('http://test.co.jp/path')     # => true
Validators::UrlValidator.valid?('ftp://files.example.com')    # => true
Validators::UrlValidator.valid?('not-a-url')                  # => false

# スキーム指定
Validators::UrlValidator.valid?('https://example.com', scheme: 'https')  # => true
Validators::UrlValidator.valid?('http://example.com', scheme: 'https')   # => false

IPアドレスバリデーター

class ServerConfig < ActiveRecord::Base
  # IPv4とIPv6の両方を許可
  validates :ip_address, ip: true
  
  # IPv4のみ
  validates :ipv4_address, ip: { format: :v4 }
  
  # IPv6のみ
  validates :ipv6_address, ip: { format: :v6 }
  
  # プライベートIPアドレスを禁止
  validates :public_ip, ip: { public: true }
end

# 単体使用
# IPv4
Validators::IpValidator.valid?('192.168.1.1')           # => true
Validators::IpValidator.valid?('10.0.0.1')              # => true
Validators::IpValidator.valid?('255.255.255.255')       # => true

# IPv6
Validators::IpValidator.valid?('2001:db8::1')           # => true
Validators::IpValidator.valid?('::1')                   # => true

# 無効なIP
Validators::IpValidator.valid?('999.999.999.999')       # => false
Validators::IpValidator.valid?('not-an-ip')             # => false

# フォーマット指定
Validators::IpValidator.valid?('192.168.1.1', format: :v4)  # => true
Validators::IpValidator.valid?('2001:db8::1', format: :v4)  # => false

クレジットカードバリデーター

class Payment < ActiveRecord::Base
  # 基本的なクレジットカードバリデーション(Luhnアルゴリズム)
  validates :card_number, credit_card: true
  
  # 特定のカードタイプのみ許可
  validates :visa_card, credit_card: { type: :visa }
  validates :amex_card, credit_card: { type: :american_express }
end

# 単体使用
# 有効なテスト用カード番号
Validators::CreditCardValidator.valid?('4111111111111111')  # Visa
Validators::CreditCardValidator.valid?('5555555555554444')  # MasterCard
Validators::CreditCardValidator.valid?('378282246310005')   # American Express

# 無効なカード番号
Validators::CreditCardValidator.valid?('1234567890123456')  # => false

# カードタイプ指定
Validators::CreditCardValidator.valid?('4111111111111111', type: :visa)  # => true
Validators::CreditCardValidator.valid?('4111111111111111', type: :mastercard)  # => false

電話番号バリデーター

class Contact < ActiveRecord::Base
  # 基本的な電話番号バリデーション
  validates :phone, phone: true
  
  # 国際形式必須
  validates :international_phone, phone: { international: true }
  
  # 特定の国のみ
  validates :us_phone, phone: { country: 'US' }
  validates :jp_phone, phone: { country: 'JP' }
end

# 単体使用
Validators::PhoneValidator.valid?('090-1234-5678')      # => true (日本)
Validators::PhoneValidator.valid?('+81-90-1234-5678')   # => true (国際形式)
Validators::PhoneValidator.valid?('(555) 123-4567')     # => true (米国)
Validators::PhoneValidator.valid?('invalid-phone')      # => false

# 国指定
Validators::PhoneValidator.valid?('090-1234-5678', country: 'JP')  # => true
Validators::PhoneValidator.valid?('090-1234-5678', country: 'US')  # => false

ActiveModelでの詳細設定

カスタムエラーメッセージ

class User < ActiveRecord::Base
  validates :email, email: { 
    message: 'は有効なメールアドレスを入力してください'
  }
  
  validates :website, url: { 
    message: 'は有効なURLを入力してください'
  }
  
  validates :ip_address, ip: {
    message: 'は有効なIPアドレスを入力してください'
  }
end

条件付きバリデーション

class ApiConfig < ActiveRecord::Base
  validates :webhook_url, url: true, if: :webhook_enabled?
  validates :fallback_url, url: true, unless: :primary_url_valid?
  
  validates :server_ip, ip: { format: :v4 }, if: -> { environment == 'production' }
  
  private
  
  def webhook_enabled?
    webhook_enabled == true
  end
  
  def primary_url_valid?
    Validators::UrlValidator.valid?(primary_url)
  end
end

複数条件でのバリデーション

class UserProfile < ActiveRecord::Base
  # 複数のバリデーターを組み合わせ
  validates :contact_email, presence: true, email: true, uniqueness: true
  
  validates :personal_website, url: { scheme: ['http', 'https'] }, allow_blank: true
  
  validates :business_phone, phone: { country: 'US' }, 
            if: -> { country_code == 'US' }
            
  validates :backup_server_ip, ip: { format: :v4, public: true }, 
            allow_blank: true
end

カスタムバリデーターの作成

独自バリデーターの定義

class CustomEmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A[^@\s]+@[^@\s]+\z/
      record.errors[attribute] << (options[:message] || 'は有効なメールアドレスではありません')
    end
    
    # 企業ドメインのみ許可
    if options[:corporate_only] && value =~ /@(gmail|yahoo|hotmail)\.com$/
      record.errors[attribute] << '個人用メールアドレスは使用できません'
    end
  end
end

class Employee < ActiveRecord::Base
  validates :email, custom_email: { corporate_only: true }
end

複雑なバリデーションロジック

class SecurityValidator < ActiveModel::Validator
  def validate(record)
    # IPアドレスとポートの組み合わせチェック
    if record.ip_address.present? && record.port.present?
      unless valid_ip_port_combination?(record.ip_address, record.port)
        record.errors.base << 'IPアドレスとポートの組み合わせが無効です'
      end
    end
    
    # URLとAPIキーの関連性チェック
    if record.api_url.present? && record.api_key.present?
      unless url_supports_api_key?(record.api_url, record.api_key)
        record.errors.base << 'APIキーが指定されたURLに対応していません'
      end
    end
  end
  
  private
  
  def valid_ip_port_combination?(ip, port)
    return false if port < 1 || port > 65535
    
    # プライベートIPアドレスでは特定ポートを禁止
    if private_ip?(ip) && [22, 23, 3389].include?(port)
      return false
    end
    
    true
  end
  
  def private_ip?(ip)
    private_ranges = [
      /^10\./,
      /^172\.(1[6-9]|2[0-9]|3[01])\./,
      /^192\.168\./
    ]
    
    private_ranges.any? { |range| ip =~ range }
  end
  
  def url_supports_api_key?(url, api_key)
    # URLとAPIキーの形式をチェック
    return false unless Validators::UrlValidator.valid?(url)
    return false unless api_key.length >= 32
    
    # 特定のドメインパターンをチェック
    uri = URI.parse(url)
    case uri.host
    when /api\.github\.com/
      api_key =~ /^ghp_/
    when /api\.stripe\.com/
      api_key =~ /^sk_/
    else
      true
    end
  end
end

class ApiIntegration < ActiveRecord::Base
  validates_with SecurityValidator
end

エラーハンドリング

詳細なエラー情報の取得

class User < ActiveRecord::Base
  validates :email, email: true
  validates :website, url: true
  validates :phone, phone: true
end

user = User.new(
  email: 'invalid-email',
  website: 'not-a-url',
  phone: 'invalid-phone'
)

if user.valid?
  puts "ユーザー情報は有効です"
else
  puts "バリデーションエラー:"
  user.errors.full_messages.each do |message|
    puts "- #{message}"
  end
  
  # 特定フィールドのエラーを取得
  puts "メールエラー: #{user.errors[:email].join(', ')}"
  puts "URLエラー: #{user.errors[:website].join(', ')}"
  puts "電話番号エラー: #{user.errors[:phone].join(', ')}"
end

バリデーション結果の詳細分析

def analyze_validation_result(record)
  result = {
    valid: record.valid?,
    errors: {},
    warnings: []
  }
  
  record.errors.each do |attribute, message|
    result[:errors][attribute] ||= []
    result[:errors][attribute] << message
    
    # 特定エラーに対する警告
    case attribute
    when :email
      if message.include?('invalid')
        result[:warnings] << "メールアドレスの形式を確認してください"
      end
    when :url
      if message.include?('invalid')
        result[:warnings] << "URLはhttp://またはhttps://で始まる必要があります"
      end
    end
  end
  
  result
end

# 使用例
user = User.new(email: 'test', website: 'example')
analysis = analyze_validation_result(user)

puts "バリデーション結果: #{analysis[:valid] ? '成功' : '失敗'}"
puts "エラー: #{analysis[:errors]}"
puts "警告: #{analysis[:warnings]}"

Railsでの実用例

ユーザー登録フォーム

class User < ApplicationRecord
  validates :email, presence: true, email: true, uniqueness: true
  validates :phone, phone: true, allow_blank: true
  validates :website, url: true, allow_blank: true
  
  before_validation :normalize_phone
  before_validation :normalize_website
  
  private
  
  def normalize_phone
    return unless phone.present?
    
    # 電話番号の正規化
    self.phone = phone.gsub(/[^\d+\-()]/, '')
  end
  
  def normalize_website
    return unless website.present?
    
    # URLの正規化
    unless website.match?(/^https?:\/\//)
      self.website = "https://#{website}"
    end
  end
end

# コントローラーでの使用
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to @user, notice: 'ユーザーが正常に作成されました'
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  private
  
  def user_params
    params.require(:user).permit(:email, :phone, :website)
  end
end

API設定管理

class ApiEndpoint < ApplicationRecord
  validates :name, presence: true
  validates :url, presence: true, url: { scheme: ['http', 'https'] }
  validates :webhook_url, url: true, allow_blank: true
  validates :ip_whitelist, ip: { format: :v4 }, allow_blank: true
  
  validate :check_url_accessibility
  validate :validate_ip_list
  
  private
  
  def check_url_accessibility
    return unless url.present? && Validators::UrlValidator.valid?(url)
    
    begin
      uri = URI.parse(url)
      
      # 本番環境でのみアクセシビリティをチェック
      if Rails.env.production?
        Net::HTTP.get_response(uri)
      end
    rescue => e
      errors.add(:url, "アクセスできないURLです: #{e.message}")
    end
  end
  
  def validate_ip_list
    return unless ip_whitelist.present?
    
    ips = ip_whitelist.split(',').map(&:strip)
    ips.each do |ip|
      unless Validators::IpValidator.valid?(ip)
        errors.add(:ip_whitelist, "無効なIPアドレスが含まれています: #{ip}")
        break
      end
    end
  end
end

フォームヘルパーとの統合

<!-- app/views/users/_form.html.erb -->
<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "個") %>のエラーがあります:</h2>
      <ul>
        <% user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :email, "メールアドレス" %>
    <%= form.email_field :email, class: "form-control" %>
    <% if user.errors[:email].any? %>
      <div class="error-message">
        <%= user.errors[:email].first %>
      </div>
    <% end %>
  </div>

  <div class="field">
    <%= form.label :website, "ウェブサイト" %>
    <%= form.url_field :website, placeholder: "https://example.com", class: "form-control" %>
    <% if user.errors[:website].any? %>
      <div class="error-message">
        <%= user.errors[:website].first %>
      </div>
    <% end %>
  </div>

  <div class="field">
    <%= form.label :phone, "電話番号" %>
    <%= form.telephone_field :phone, placeholder: "090-1234-5678", class: "form-control" %>
    <% if user.errors[:phone].any? %>
      <div class="error-message">
        <%= user.errors[:phone].first %>
      </div>
    <% end %>
  </div>

  <div class="actions">
    <%= form.submit "保存", class: "btn btn-primary" %>
  </div>
<% end %>

テスト例

RSpecでのテスト

require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'バリデーション' do
    subject { User.new(email: email, website: website, phone: phone) }
    
    let(:email) { '[email protected]' }
    let(:website) { 'https://example.com' }
    let(:phone) { '090-1234-5678' }
    
    context '有効な値が設定されている場合' do
      it { is_expected.to be_valid }
    end
    
    describe 'メールアドレスバリデーション' do
      context '有効なメールアドレスの場合' do
        let(:email) { '[email protected]' }
        it { is_expected.to be_valid }
      end
      
      context '無効なメールアドレスの場合' do
        let(:email) { 'invalid-email' }
        it { is_expected.not_to be_valid }
        it { expect(subject.tap(&:valid?).errors[:email]).to include('は有効なメールアドレスではありません') }
      end
      
      context 'メールアドレスが空の場合' do
        let(:email) { '' }
        it { is_expected.not_to be_valid }
      end
    end
    
    describe 'URLバリデーション' do
      context '有効なURLの場合' do
        let(:website) { 'https://www.example.com/path' }
        it { is_expected.to be_valid }
      end
      
      context '無効なURLの場合' do
        let(:website) { 'not-a-url' }
        it { is_expected.not_to be_valid }
      end
      
      context 'URLが空の場合' do
        let(:website) { '' }
        it { is_expected.to be_valid }  # allow_blank: true
      end
    end
    
    describe '電話番号バリデーション' do
      context '有効な電話番号の場合' do
        let(:phone) { '+81-90-1234-5678' }
        it { is_expected.to be_valid }
      end
      
      context '無効な電話番号の場合' do
        let(:phone) { 'invalid-phone' }
        it { is_expected.not_to be_valid }
      end
    end
  end
  
  describe 'Validators単体テスト' do
    describe 'EmailValidator' do
      it '有効なメールアドレスを正しく判定する' do
        expect(Validators::EmailValidator.valid?('[email protected]')).to be true
        expect(Validators::EmailValidator.valid?('[email protected]')).to be true
        expect(Validators::EmailValidator.valid?('invalid-email')).to be false
      end
    end
    
    describe 'UrlValidator' do
      it '有効なURLを正しく判定する' do
        expect(Validators::UrlValidator.valid?('https://example.com')).to be true
        expect(Validators::UrlValidator.valid?('http://test.local:3000/path')).to be true
        expect(Validators::UrlValidator.valid?('not-a-url')).to be false
      end
    end
    
    describe 'IpValidator' do
      it '有効なIPアドレスを正しく判定する' do
        expect(Validators::IpValidator.valid?('192.168.1.1')).to be true
        expect(Validators::IpValidator.valid?('2001:db8::1')).to be true
        expect(Validators::IpValidator.valid?('999.999.999.999')).to be false
      end
    end
  end
end

ファクトリー設定

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email { Faker::Internet.email }
    website { Faker::Internet.url }
    phone { '+81-90-1234-5678' }
    
    trait :with_invalid_email do
      email { 'invalid-email' }
    end
    
    trait :with_invalid_website do
      website { 'not-a-url' }
    end
    
    trait :with_invalid_phone do
      phone { 'invalid-phone' }
    end
  end
end

# 使用例
describe 'バリデーションテスト' do
  it '無効なメールアドレスではバリデーションエラーになる' do
    user = build(:user, :with_invalid_email)
    expect(user).not_to be_valid
    expect(user.errors[:email]).to be_present
  end
end

国際化(i18n)

設定ファイル

# config/locales/ja.yml
ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            email:
              email: "は有効なメールアドレスではありません"
            website:
              url: "は有効なURLではありません"
            phone:
              phone: "は有効な電話番号ではありません"
            ip_address:
              ip: "は有効なIPアドレスではありません"
              
  validators:
    email:
      invalid: "は有効なメールアドレスではありません"
      mx_invalid: "のメールサーバーが見つかりません"
      disposable: "では使い捨てメールアドレスは使用できません"
    url:
      invalid: "は有効なURLではありません"
      scheme_invalid: "は%{scheme}で始まる必要があります"
    ip:
      invalid: "は有効なIPアドレスではありません"
      v4_invalid: "は有効なIPv4アドレスではありません"
      v6_invalid: "は有効なIPv6アドレスではありません"
      private_not_allowed: "ではプライベートIPアドレスは使用できません"
    phone:
      invalid: "は有効な電話番号ではありません"
      country_invalid: "は%{country}の電話番号形式ではありません"
    credit_card:
      invalid: "は有効なクレジットカード番号ではありません"
      type_invalid: "は%{type}カードの番号ではありません"

パフォーマンス最適化

バリデーションのキャッシュ

class ValidatorCache
  class << self
    def email_valid?(email)
      Rails.cache.fetch("email_valid:#{email}", expires_in: 1.hour) do
        Validators::EmailValidator.valid?(email)
      end
    end
    
    def url_accessible?(url)
      Rails.cache.fetch("url_accessible:#{url}", expires_in: 5.minutes) do
        begin
          uri = URI.parse(url)
          response = Net::HTTP.get_response(uri)
          response.code.to_i < 400
        rescue
          false
        end
      end
    end
  end
end

class User < ApplicationRecord
  validate :check_email_with_cache
  
  private
  
  def check_email_with_cache
    return unless email.present?
    
    unless ValidatorCache.email_valid?(email)
      errors.add(:email, 'は有効なメールアドレスではありません')
    end
  end
end

バックグラウンドでのバリデーション

class ExpensiveValidationJob < ApplicationJob
  queue_as :default
  
  def perform(record_class, record_id, attribute, value)
    record = record_class.constantize.find(record_id)
    
    case attribute
    when 'email'
      # MXレコードチェック
      if mx_record_exists?(value)
        record.update_column(:email_verified, true)
      else
        record.update_column(:email_verified, false)
        AdminMailer.invalid_email_notification(record).deliver_now
      end
    when 'url'
      # URLアクセシビリティチェック
      if url_accessible?(value)
        record.update_column(:url_verified, true)
      else
        record.update_column(:url_verified, false)
      end
    end
  end
  
  private
  
  def mx_record_exists?(email)
    domain = email.split('@').last
    Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).any?
  rescue
    false
  end
  
  def url_accessible?(url)
    uri = URI.parse(url)
    Net::HTTP.get_response(uri).code.to_i < 400
  rescue
    false
  end
end

# モデルでの使用
class User < ApplicationRecord
  validates :email, email: true
  validates :website, url: true
  
  after_create :schedule_expensive_validations
  
  private
  
  def schedule_expensive_validations
    ExpensiveValidationJob.perform_later(self.class.name, id, 'email', email) if email.present?
    ExpensiveValidationJob.perform_later(self.class.name, id, 'url', website) if website.present?
  end
end

まとめ

Validatorsライブラリは、Rubyアプリケーションにおいて実用的で包括的なバリデーション機能を提供します。主な利点:

  • 実用性: 実際のWebアプリケーションで必要な形式のバリデーションを網羅
  • ActiveModel統合: Railsアプリケーションでの使用が簡単
  • パフォーマンス: 効率的なアルゴリズムと最適化されたパターンマッチング
  • カスタマイズ性: 各バリデーターの詳細設定が可能
  • 拡張性: 独自のバリデーターを簡単に追加可能
  • 国際化対応: 多言語でのエラーメッセージサポート

特に、メール、URL、IP、電話番号、クレジットカードなどの一般的な形式のバリデーションが必要なWebアプリケーションにおいて、開発効率と品質の向上に大きく貢献するライブラリです。