Rails uygulamalarınızda model doğrulamasını bir üst seviyeye taşımak mı istiyorsunuz? Bu kapsamlı rehber, özel validatörleri gem olarak paketlemeyi adım adım açıklıyor, kod kalitenizi artırıyor ve veri bütünlüğünü güçlendiriyor.
Bir web uygulamasının temel taşlarından biri, veri bütünlüğüdür. Kullanıcıdan alınan bilgilerin doğru, tutarlı ve beklenen formatta olması, uygulamanın güvenilirliği ve işlevselliği için hayati öneme sahiptir. Ruby on Rails, bu ihtiyacı karşılamak üzere Active Record modelleri için güçlü bir doğrulama (validation) mekanizması sunar. validates yardımcı metodu ile presence, uniqueness, length, format gibi birçok standart doğrulayıcıyı kolayca kullanabiliriz. Örneğin, bir kullanıcının e-posta adresinin boş olmamasını veya belirli bir uzunlukta olmasını sağlamak için aşağıdaki gibi basit kontroller yeterlidir:
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 6 }
end
Ancak, her zaman bu kadar şanslı olmayız. Gerçek dünya uygulamaları genellikle çok daha karmaşık iş kuralları ve veri ilişkileri barındırır. Standart Rails validatörleri, bu tür senaryolarda yetersiz kalmaya başlayabilir. Örneğin, bir parolanın sadece minimum uzunlukta olmasını değil, aynı zamanda en az bir büyük harf, bir sayı ve bir özel karakter içermesini gerektiren bir güvenlik politikası düşünün. Ya da bir e-ticaret uygulamasında, ürün SKU'sunun hem benzersiz olmasını hem de belirli bir üçüncü taraf envanter sisteminde gerçekten var olup olmadığını kontrol etmeniz gerekebilir. Bu gibi durumlarda, modeli karmaşık ve okunmaz hale getiren uzun ve tekrarlayan özel metodlar yazmak yerine, özel validatörler (custom validators) devreye girer. Bu yaklaşımlar, kod tekrarını önler, modelin sorumluluklarını daha iyi ayırır ve geliştirici deneyimini önemli ölçüde iyileştirir. Veri bütünlüğünü sağlamak sadece uygulamanın çökmesini engellemekle kalmaz, aynı zamanda iş mantığının doğru bir şekilde işlemesini ve kullanıcıların sisteme güven duymasını da garantiler.
Özel Validatör Nedir ve Ne Zaman Kullanmalıyız?
Ruby on Rails ekosisteminde özel validatörler, mevcut doğrulama mekanizmalarının yetersiz kaldığı durumlarda kendi iş mantığımızı uygulamamıza olanak tanıyan sınıflardır. Temelde, ActiveModel::Validator veya ActiveModel::EachValidator soyut sınıflarından türeyen bu yapılar, bir modelin belirli bir özelliğini veya genel durumunu belirli kurallara göre denetler ve geçerli değilse hata mesajları ekler. Bu sayede, karmaşık doğrulama mantığını model dosyasından ayırarak daha temiz, test edilebilir ve yeniden kullanılabilir bir kod tabanı oluştururuz.
Peki, özel bir validatöre ne zaman ihtiyaç duyarız? İşte bazı yaygın senaryolar:
- Karmaşık İş Kuralları: Bir özelliğin değeri, uygulamanın birden fazla başka durumu veya ayarı ile birlikte değerlendirilmelidir. Örneğin, bir kullanıcının yaşının, belirli bir bölgedeki yasal yaş sınırına uygun olup olmadığını kontrol etmek.
- Üçüncü Taraf API Entegrasyonları: Doğrulama, harici bir servise yapılan bir API çağrısının sonucuna bağlıdır. Yukarıda bahsedilen SKU örneği veya bir kullanıcının kredi notunu harici bir finansal servisten çekerek doğrulama.
- Tekrarlayan Mantık: Birden fazla modelde veya farklı özellikler için aynı doğrulama mantığını kullanmanız gerektiğinde. Örneğin, tüm dosya yüklemelerinin belirli bir mime türünde olmasını gerektiren bir
FileExtensionValidator. - Okunabilirlik ve Sorumluluk Ayrımı: Modelin kendisi zaten çok fazla iş mantığı içeriyorsa, doğrulama kurallarını ayrı bir sınıfa taşımak, model dosyasını daha okunabilir hale getirir ve tek sorumluluk prensibine (Single Responsibility Principle) daha uygun hale getirir.
- Parametrik Doğrulama: Validatörün davranışını, modele farklı seçenekler (parametreler) göndererek değiştirmek istediğinizde.
Özel validatörler, sadece mevcut Rails validates yöntemleriyle çözülemeyen problemleri ele almakla kalmaz, aynı zamanda kod kalitenizi de önemli ölçüde artırır. Modeliniz, sadece iş verilerini temsil etme ve ilişkileri yönetme sorumluluğunu taşırken, doğrulama mantığı ise kendi özel sınıfında yaşar. Bu, özellikle büyük ve uzun ömürlü uygulamalarda bakımı ve hata ayıklamayı çok daha kolay hale getirir.
Kendi Özel Model Validatörümüzü Nasıl Geliştiririz? Adım Adım Rehber
Özel bir Rails validatörü oluşturmak, aslında oldukça basit bir süreçtir. Rails, bu tür yapıları oluşturmak için iki temel soyut sınıf sunar: ActiveModel::Validator ve ActiveModel::EachValidator. İlk olarak, bir ActiveModel::Validator kullanarak nasıl genel bir validatör oluşturacağımızı inceleyelim. Bu tip validatörler, modelin genel durumunu doğrulamak için kullanılır ve validate adında bir metod içermelidir.
Genel Bir ActiveModel::Validator Yazmak
Bir e-posta formatını kontrol eden basit bir validatör ile başlayalım. Diyelim ki URI::MailTo::EMAIL_REGEXP yeterli değil ve biz kendi özel, daha katı e-posta doğrulama kurallarımızı uygulamak istiyoruz.
# app/validators/custom_email_validator.rb
class CustomEmailValidator < ActiveModel::Validator
def validate(record)
unless record.email =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
record.errors.add :email, "geçersiz bir e-posta formatına sahip"
end
# Ek kural: Belirli bir domainden gelmemeli
if record.email.present? && record.email.ends_with?("@baddomain.com")
record.errors.add :email, "bu e-posta adresi ile kayıt olamazsınız"
end
end
end
Bu kod bloğunda, CustomEmailValidator sınıfı ActiveModel::Validator'dan miras alır. Bu, Rails'in bu sınıfı bir doğrulayıcı olarak tanımasını sağlar. validate metodu, doğrulanan modelin bir örneğini (record parametresi olarak) alır. Bu metod içinde, modelin email özelliğini kendi düzenli ifademizle kontrol ediyoruz. Eğer e-posta formatı eşleşmezse veya yasaklı bir domainden geliyorsa, record.errors.add metodunu kullanarak :email özelliğine bir hata mesajı ekliyoruz. Bu hata, modelin valid? metodunun false dönmesine neden olacaktır.
Modelde CustomEmailValidator'ı Kullanma
Validatör sınıfımızı yazdıktan sonra, onu herhangi bir Active Record modelinde kullanmak oldukça kolaydır. validates_with metodunu kullanırız:
# app/models/user.rb
class User < ApplicationRecord
validates_with CustomEmailValidator
# Diğer doğrulamalarınız...
validates :name, presence: true
end
Artık bir User nesnesi oluşturup e-posta formatı hatalıysa, valid? çağrısı false dönecek ve errors.full_messages içinde ilgili hata mesajını görebileceğiz:
u = User.new(name: "Ali", email: "geçersiz-email")
u.valid? # => false
u.errors.full_messages # => ["Email geçersiz bir e-posta formatına sahip"]
u2 = User.new(name: "Ayşe", email: "ayse@baddomain.com")
u2.valid? # => false
u2.errors.full_messages # => ["Email bu e-posta adresi ile kayıt olamazsınız"]
Bu yaklaşım, doğrulama mantığını modelden ayırarak hem modelin daha temiz kalmasını sağlar hem de aynı doğrulayıcıyı farklı modellerde veya farklı senaryolarda yeniden kullanma esnekliği sunar. Kodun okunabilirliği ve bakımı açısından büyük bir avantajdır. Her bölüm min. 300 kelime kuralına uymak adına bu kısımları daha detaylı ele almak, özellikle ActiveModel::EachValidator kullanımına geçişi de içermek, makaleyi zenginleştirecektir.
Daha Karmaşık Validatörler: Seçenekler ve Bağımlılıklar (ActiveModel::EachValidator)
Yukarıdaki örnek, modelin genel durumunu doğrulamak için yeterliydi. Ancak genellikle, bir modelin belirli bir özelliğinin her bir öğesini (örneğin bir koleksiyondaki her bir elemanı) veya belirli bir özelliğini belirli kurallara göre doğrulamak isteriz. İşte bu noktada ActiveModel::EachValidator devreye girer. Bu validatör tipi, adından da anlaşılacağı gibi, belirtilen her bir özellik için ayrı ayrı çalışır ve validate_each adında bir metod içermelidir.
ActiveModel::EachValidator, ActiveModel::Validator'a göre iki önemli avantaja sahiptir: birincisi, doğrudan bir özelliğe bağlanabilir ve ikincisi, validatöre seçenekler (options) geçirmemize olanak tanır. Bu sayede validatörlerimizi daha esnek ve yapılandırılabilir hale getirebiliriz. Örneğin, bir parolanın minimum uzunluğunu veya hangi karakter türlerini içermesi gerektiğini belirten bir PasswordStrengthValidator oluşturalım:
# app/validators/password_strength_validator.rb
class PasswordStrengthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Varsayılan seçenekleri ayarlayalım
options = { min_length: 8, requires: [:lowercase, :uppercase, :number, :special] }.merge(self.options)
if value.blank?
record.errors.add(attribute, :blank)
return
end
# Minimum uzunluk kontrolü
if value.length < options[:min_length]
record.errors.add(attribute, "en az #{options[:min_length]} karakter içermeli")
end
# Gerekli karakter türleri kontrolü
options[:requires].each do |requirement|
case requirement
when :lowercase
record.errors.add(attribute, "en az bir küçük harf içermeli") unless value =~ /[a-z]/
when :uppercase
record.errors.add(attribute, "en az bir büyük harf içermeli") unless value =~ /[A-Z]/
when :number
record.errors.add(attribute, "en az bir rakam içermeli") unless value =~ /[0-9]/
when :special
record.errors.add(attribute, "en az bir özel karakter (örn: !@#$%) içermeli") unless value =~ /[^a-zA-Z0-9\s]/
end
end
end
end
Bu validatörde, validate_each metodu üç argüman alır: record (model örneği), attribute (doğrulanan özelliğin adı, örn: :password) ve value (doğrulanan özelliğin değeri). Bu, doğrudan hedef özelliğe odaklanmamızı sağlar. Validatöre, minimum uzunluk ve hangi karakter türlerinin gerekli olduğunu belirten seçenekler geçirebiliriz. Bu seçenekler self.options üzerinden erişilebilir.
Modelde ActiveModel::EachValidator'ı Kullanma
ActiveModel::EachValidator tabanlı validatörleri kullanmak için validates metodunu, normal bir doğrulayıcı gibi kullanabiliriz, ancak özel validatörümüzün ismini bir hash anahtarı olarak geçeriz:
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :password, password_strength: { min_length: 10, requires: [:uppercase, :number, :special] }
validates :username, presence: true, uniqueness: true
end
Şimdi bir User objesi oluştururken, parola için belirlediğimiz karmaşık kuralların geçerli olup olmadığını kontrol edebiliriz:
u = User.new(username: "deneme", email: "test@example.com", password: "gecici")
u.valid? # => false
u.errors.full_messages
# => ["Password en az 10 karakter içermeli", "Password en az bir büyük harf içermeli", "Password en az bir rakam içermeli", "Password en az bir özel karakter (!@#$%) içermeli"]
u2 = User.new(username: "deneme2", email: "test2@example.com", password: "GuclU_Parola1!")
u2.valid? # => true
ActiveModel::EachValidator, modelinizdeki belirli bir alanın değerine odaklanmanız gerektiğinde mükemmel bir çözümdür. Ayrıca, validatörün davranışını dinamik olarak ayarlamak için seçenekler geçirebilme yeteneği, kodun yeniden kullanılabilirliğini ve esnekliğini önemli ölçüde artırır. Bu, özellikle farklı modellerde veya aynı modelin farklı bağlamlarında hafifçe farklı doğrulama kuralları uygulamanız gerektiğinde büyük kolaylık sağlar. Bu yapılar, veri bütünlüğünü korumak ve uygulama mantığını temiz tutmak için kilit araçlardır.
Özel Validatörü Gem Olarak Nasıl Paketliyoruz?
Özel validatörlerinizi gem olarak paketlemek, onları farklı Rails projelerinizde veya açık kaynak topluluğuyla paylaşmak için en etkili yoldur. Bir validatör gem'i oluşturmak, kodunuzun yeniden kullanılabilirliğini artırır, dağıtımını kolaylaştırır ve projeler arasında tutarlı doğrulama kuralları uygulamanıza yardımcı olur. Bu bölümde, basit bir validatör gem'ini nasıl oluşturacağınızı ve Rails uygulamalarınıza nasıl entegre edeceğinizi adım adım göreceğiz.
Neden Gem Olarak Paketlemeliyiz?
- Yeniden Kullanılabilirlik: Aynı doğrulama mantığını birden fazla projede kullanmanızı sağlar.
- Sorumluluk Ayrımı: Doğrulama mantığını uygulamanızın çekirdek kodundan ayırır.
- Kolay Dağıtım: Gem'ler, RubyGems.org üzerinden veya doğrudan proje bağımlılığı olarak kolayca dağıtılabilir ve kurulabilir.
- Versiyonlama ve Bakım: Gem'ler, bağımsız olarak versiyonlanabilir ve bakımı yapılabilir, bu da değişiklikleri yönetmeyi kolaylaştırır.
Gem Yapısı Oluşturma
Yeni bir gem oluşturmanın en kolay yolu Bundler kullanmaktır. Terminalinizi açın ve aşağıdaki komutu çalıştırın:
bundle gem my_custom_validators
Bu komut, my_custom_validators adında yeni bir dizin oluşturacak ve içinde bir gem'in temel dosya yapısını otomatik olarak hazırlayacaktır. Oluşan ana dosyalar şunlardır:
my_custom_validators.gemspec: Gem'in metadata bilgilerini (adı, versiyonu, bağımlılıkları vb.) içerir.lib/my_custom_validators.rb: Gem'in ana giriş noktası.lib/my_custom_validators/version.rb: Gem'in versiyon numarasını tanımlar.Rakefile,Gemfile,LICENSE.txt,README.mdvb.
Validatör Kodunu Gem'e Taşıma
Özel validatör sınıfımızı (örneğin PasswordStrengthValidator'ı) şimdi bu gem yapısına taşıyabiliriz. lib/my_custom_validators/validators adında bir dizin oluşturmak ve validatör dosyalarınızı buraya yerleştirmek iyi bir pratik olacaktır:
# lib/my_custom_validators/validators/password_strength_validator.rb
module MyCustomValidators
class PasswordStrengthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
options = { min_length: 8, requires: [:lowercase, :uppercase, :number, :special] }.merge(self.options)
if value.blank?
record.errors.add(attribute, :blank)
return
end
if value.length < options[:min_length]
record.errors.add(attribute, "en az #{options[:min_length]} karakter içermeli")
end
options[:requires].each do |requirement|
case requirement
when :lowercase
record.errors.add(attribute, "en az bir küçük harf içermeli") unless value =~ /[a-z]/
when :uppercase
record.errors.add(attribute, "en az bir büyük harf içermeli") unless value =~ /[A-Z]/
when :number
record.errors.add(attribute, "en az bir rakam içermeli") unless value =~ /[0-9]/
when :special
record.errors.add(attribute, "en az bir özel karakter (örn: !@#$%) içermeli") unless value =~ /[^a-zA-Z0-9\s]/
end
end
end
end
end
Gem'in ana dosyası olan lib/my_custom_validators.rb'yi bu validatörleri otomatik olarak yükleyecek şekilde düzenlememiz gerekiyor. Ayrıca Rails'in bu validatörleri validates :field, password_strength: {} gibi bir syntax ile tanıması için ActiveModel'e kendi validatörlerimizi kaydetmeliyiz.
# lib/my_custom_validators.rb
require "active_model"
require "my_custom_validators/version"
require "my_custom_validators/validators/password_strength_validator"
# Diğer validatörlerinizi de buraya ekleyebilirsiniz.
module MyCustomValidators
# ActiveModel'e kendi validatörümüzü kaydetmek için bir modül genişletmesi
# Bu kısım genellikle bir railtie içinde yapılır, ancak basit bir gem için doğrudan da eklenebilir.
# Ancak Rails projeleri için en doğru yöntem Railtie kullanmaktır.
end
Validatörün Active Model tarafından tanınması için ActiveModel::Validations modülüne dahil edilmesi gerekir. Bu işlemi bir Rails uygulamasında otomatik olarak yapmak için bir railtie kullanmak en iyi yoldur. Bu, Rails'in gem'inizi yüklediğinde belirli kodları çalıştırmasını sağlar.
# lib/my_custom_validators/railtie.rb
require 'rails/railtie'
module MyCustomValidators
class Railtie < Rails::Railtie
initializer "my_custom_validators.active_record" do |app|
ActiveSupport.on_load(:active_record) do
# Tüm Active Record modellerine bu validatörleri eklemek isterseniz buraya yazabilirsiniz.
# Örneğin, PasswordStrengthValidator'ı ActiveModel'e kaydetmek için
# ActiveModel::Validations.const_set(:PasswordStrengthValidator, MyCustomValidators::PasswordStrengthValidator)
# Ancak EachValidator tabanlı olduğu için Rails bunu otomatik olarak algılar.
# Sadece lib/my_custom_validators/validators altındaki dosyaları yüklediğimizden emin olmalıyız.
# Bu zaten lib/my_custom_validators.rb içinde yapılıyor.
end
end
end
end
lib/my_custom_validators.rb dosyasında, railtie dosyasını da dahil ettiğinizden emin olun:
# lib/my_custom_validators.rb
require "active_model"
require "my_custom_validators/version"
require "my_custom_validators/railtie" if defined?(Rails) # Sadece Rails ortamında yüklensin
require "my_custom_validators/validators/password_strength_validator"
module MyCustomValidators
# Modül içeriği
end
Son olarak, my_custom_validators.gemspec dosyasını açın ve gem'inizin adını, özetini, açıklamasını ve bağımlılıklarını güncelleyin. Özellikle add_dependency "activesupport", ">= 5.2" gibi Rails bağımlılıklarını eklediğinizden emin olun, çünkü ActiveModel'den türemiş validatörler Rails'in parçasıdır. Geliştirme bağımlılıkları arasına rspec ve rubocop gibi araçları eklemeyi unutmayın.
Bu adımlarla, özel validatörünüz artık bir gem olarak paketlenmeye hazır. Gem'i bir Rails projesine eklemek için, projenizin Gemfile'ına gem 'my_custom_validators', path: 'path/to/my_custom_validators_gem' (yerel geliştirme için) veya gem 'my_custom_validators', '~> 0.1.0' (eğer RubyGems.org'a yayınladıysanız) satırını eklemeniz yeterli olacaktır. Daha sonra bundle install çalıştırarak gem'i projenize dahil edebilirsiniz. Bu süreç, kodunuzun modülerliğini ve yeniden kullanılabilirliğini maksimum düzeye çıkararak geliştirme süreçlerinizi verimli hale getirir.
Gerçek Dünya Senaryosu: Harici API Entegrasyonlu Validatör Gem'i
Özel validatörlerin gücünü göstermenin en iyi yollarından biri, karmaşık iş gereksinimlerini karşılamak için harici servislerle entegrasyonu sağlamaktır. Diyelim ki bir e-ticaret uygulamanız var ve ürün SKU'larının (Stock Keeping Unit - Stok Tutma Birimi) yalnızca kendi veritabanınızda değil, aynı zamanda harici bir envanter yönetim sisteminde de benzersiz ve geçerli olması gerekiyor. Bu senaryo, API entegrasyonlu bir özel validatör gem'i oluşturmak için mükemmel bir fırsattır.
Senaryo: Harici Envanter Sistemi ile SKU Doğrulama
Bir Product modelimiz var ve sku adında bir özelliği bulunuyor. Bu sku, aşağıdaki kurallara uymalıdır:
- Uygulamanın kendi veritabanında benzersiz olmalıdır (Rails'in standart
uniquenessvalidatörü ile kolayca yapılabilir). - Harici bir envanter yönetim sistemi API'si tarafından geçerli ve mevcut olarak onaylanmalıdır.
Bu ikinci kural, özel bir validatör gerektirir. Harici API çağrıları ağ gecikmelerine neden olabileceği için, validatörün performans ve hata yönetimi konusunda dikkatli olması gerekir. Ayrıca, API anahtarları gibi hassas bilgileri doğrudan validatör kodunda tutmak yerine, Rails'in yapılandırma mekanizmalarıyla entegre etmeliyiz.
SkuAvailabilityValidator Gem'i Geliştirme
İlk adım olarak, gem yapımızda yeni bir validatör dosyası oluşturacağız: lib/my_custom_validators/validators/sku_availability_validator.rb.
# lib/my_custom_validators/validators/sku_availability_validator.rb
require 'net/http'
require 'json'
module MyCustomValidators
class SkuAvailabilityValidator < ActiveModel::EachValidator
# API URL'ini ve anahtarını yapılandırma seçeneklerinden veya Rails uygulamasının env'inden alabiliriz.
# Bu örneği basitleştirmek adına, varsayılan bir URL kullanacağız.
# Gerçek uygulamada, Rails.application.credentials veya ENV değişkenleri tercih edilir.
API_BASE_URL = options[:api_url] || "https://api.externalinventory.com/check_sku"
API_KEY = options[:api_key] || ENV["EXTERNAL_INVENTORY_API_KEY"]
def validate_each(record, attribute, value)
return if value.blank? # Boş değerler için kontrolü atla, presence: true ile halledilir.
if sku_already_exists_in_app?(record, attribute, value)
record.errors.add(attribute, :taken, message: "bu SKU zaten sistemde kayıtlı.")
return
end
unless is_sku_available_via_api?(value)
record.errors.add(attribute, "harici sistemde bulunamadı veya geçerli değil.")
end
rescue Timeout::Error, Errno::ECONNREFUSED, SocketError => e
# Ağ sorunları veya API'nin erişilememesi durumunda
record.errors.add(attribute, "envanter sistemi ile bağlantı kurulamadı. Lütfen daha sonra tekrar deneyin. (Hata: #{e.message})")
rescue StandardError => e
# Diğer beklenmedik hatalar
Rails.logger.error "SKU doğrulama sırasında beklenmedik hata: #{e.message}"
record.errors.add(attribute, "doğrulama sırasında bir hata oluştu. Lütfen yöneticinizle iletişime geçin.")
end
private
# Uygulama içinde benzersizlik kontrolünü validatör seviyesinde yapmak (uniqueness validatörüne ek olarak)
# veya doğrudan modelde uniqueness validatörünü kullanmak daha doğru olur.
# Bu örnek sadece bir ihtimali göstermektedir.
def sku_already_exists_in_app?(record, attribute, value)
record.class.where(attribute => value).where.not(id: record.id).exists?
end
def is_sku_available_via_api?(sku)
uri = URI("#{API_BASE_URL}?sku=#{sku}&api_key=#{API_KEY}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.read_timeout = 5 # 5 saniye zaman aşımı
request = Net::HTTP::Get.new(uri)
response = http.request(request)
if response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
data["available"] == true # API'nin { "sku": "ABC", "available": true } döndürdüğünü varsayalım.
else
Rails.logger.warn "Harici SKU kontrol API'sinden hata: #{response.code} #{response.message}"
false
end
end
end
end
Rails.application.credentials veya ortam değişkenleri (ENV) aracılığıyla yönetmek güvenlik açısından kritik öneme sahiptir.
Rails Uygulamasında Validatör Gem'ini Kullanma
Gem'imizi bir Rails uygulamasına entegre etmek için öncelikle Gemfile'ımıza eklemeliyiz. Eğer gem'i yerel olarak geliştiriyorsak:
# Gemfile
gem 'my_custom_validators', path: '~/path/to/my_custom_validators_gem' # Yerel geliştirme yolu
# veya eğer gem'i RubyGems.org'a yayınladıysanız:
# gem 'my_custom_validators', '~> 0.1.0'
Ardından bundle install çalıştırın. Şimdi Product modelimizde bu validatörü kullanabiliriz:
# app/models/product.rb
class Product < ApplicationRecord
validates :name, presence: true
validates :sku, presence: true, uniqueness: true # Kendi DB'mizdeki benzersizlik
validates :sku, sku_availability: { api_url: "https://api.externalinventory.com/check_sku", api_key: ENV["EXTERNAL_INVENTORY_API_KEY"] }
end
Bu örnekte, SkuAvailabilityValidator'a API URL'i ve API anahtarını seçenek olarak geçirdik. Bu, validatörün yapılandırılabilir olmasını sağlar. Artık bir ürün oluştururken:
# Valid API'den gelen SKU
p1 = Product.create(name: "Widget A", sku: "VALID-SKU-123") # API'nin true döndürdüğünü varsayalım
p1.valid? # => true
# API'den geçersiz gelen SKU
p2 = Product.new(name: "Gadget B", sku: "INVALID-SKU-XYZ") # API'nin false döndürdüğünü varsayalım
p2.valid? # => false
p2.errors.full_messages # => ["Sku harici sistemde bulunamadı veya geçerli değil."]
# Bağlantı hatası olması durumunda (API kapalıysa)
p3 = Product.new(name: "Tool C", sku: "ANY-SKU-ERROR")
p3.valid? # => false
p3.errors.full_messages # => ["Sku envanter sistemi ile bağlantı kurulamadı. Lütfen daha sonra tekrar deneyin. (Hata: Connection refused - connect(2) for "api.externalinventory.com" port 443)"]
Bu gerçek dünya senaryosu, özel validatörlerin sadece basit kuralları değil, aynı zamanda harici sistemlerle etkileşime giren karmaşık iş mantığını da nasıl kapsayabileceğini net bir şekilde göstermektedir. Bu tür bir yaklaşım, uygulamanızın modülerliğini artırırken, iş kurallarınızın merkezileştirilmesine ve kolayca yönetilmesine olanak tanır.
Test Yazmak: Özel Validatör Gem'inizi Güvenceye Almak
Yazılım geliştirmede testler, kodunuzun güvenilirliğini ve doğru çalıştığını sağlamanın en önemli yollarından biridir. Özel validatör gem'leri için de bu durum geçerlidir. Hatta, bir gem olarak tasarlanmış yeniden kullanılabilir kod blokları için test yazmak daha da kritik hale gelir, çünkü bu kod birçok farklı projede kullanılabilecek potansiyele sahiptir. Testler, validatörünüzün beklenen tüm senaryolarda doğru hata mesajlarını döndürdüğünü ve kenar durumları (edge cases) düzgün bir şekilde ele aldığını garanti eder.
Neden Özel Validatörleri Test Etmeliyiz?
- Doğruluk: Validatörün iş mantığının, belirlenen kurallara harfiyen uyup uymadığını doğrular.
- Gerileme (Regression) Önleme: Yapılan değişikliklerin mevcut işlevselliği bozmadığından emin olunur.
- Belgeleme: Testler, validatörün nasıl çalışması gerektiğini gösteren canlı bir belge görevi görür.
- Güven: Geliştiricilere, gem'i gönül rahatlığıyla kullanabilecekleri konusunda güven verir.
- Hata Mesajları: Kullanıcılara gösterilen hata mesajlarının doğru ve anlaşılır olduğundan emin olunur.
RSpec ile Test Yazma
Ruby projelerinde test yazmak için RSpec popüler ve güçlü bir çerçevedir. Gem'inizi oluşturduğunuzda Bundler genellikle sizin için bir RSpec yapılandırması hazırlar. Şimdi PasswordStrengthValidator'ımız için nasıl test yazacağımıza bakalım.
# spec/my_custom_validators/validators/password_strength_validator_spec.rb
require 'spec_helper'
require 'my_custom_validators/validators/password_strength_validator'
# Test için ActiveModel'den türetilmiş basit bir model tanımlayalım
class ValidatableModel
include ActiveModel::Model
include ActiveModel::Attributes # Attributes API'sini kullanmak için
include ActiveModel::Validations
attr_accessor :password, :email # Validatörlerin erişeceği alanlar
attribute :password, :string
attribute :email, :string
def initialize(attributes = {})
super
attributes.each { |name, value| send("#{name}=", value) }
end
end
RSpec.describe MyCustomValidators::PasswordStrengthValidator do
let(:model_class) do
Class.new(ValidatableModel) do
validates :password, password_strength: { min_length: 10, requires: [:uppercase, :number, :special] }
end
end
it "geçerli bir parola ile başarılı olur" do
model = model_class.new(password: "GuclU_Parola1!")
expect(model).to be_valid
expect(model.errors[:password]).to be_empty
end
it "minimum uzunluğun altında parola ile başarısız olur" do
model = model_class.new(password: "Kisa1!")
expect(model).not_to be_valid
expect(model.errors[:password]).to include("en az 10 karakter içermeli")
end
it "büyük harf olmadan parola ile başarısız olur" do
model = model_class.new(password: "gucsuparola1!")
expect(model).not_to be_valid
expect(model.errors[:password]).to include("en az bir büyük harf içermeli")
end
it "sayı olmadan parola ile başarısız olur" do
model = model_class.new(password: "GucluParola!")
expect(model).not_to be_valid
expect(model.errors[:password]).to include("en az bir rakam içermeli")
end
it "özel karakter olmadan parola ile başarısız olur" do
model = model_class.new(password: "GucluParola1")
expect(model).not_to be_valid
expect(model.errors[:password]).to include("en az bir özel karakter (örn: !@#$%) içermeli")
end
it "boş parola ile başarısız olur" do
model = model_class.new(password: "")
expect(model).not_to be_valid
expect(model.errors[:password]).to include("can't be blank") # presence: true eklenmediği için bu gelir
end
context "farklı validatör seçenekleri ile" do
let(:model_class_relaxed) do
Class.new(ValidatableModel) do
validates :password, password_strength: { min_length: 6, requires: [:lowercase, :number] }
end
end
it "daha az kısıtlayıcı seçeneklerle geçerli olur" do
model = model_class_relaxed.new(password: "sifre1")
expect(model).to be_valid
end
it "daha az kısıtlayıcı seçeneklerle büyük harf olmadan başarısız olmaz" do
model = model_class_relaxed.new(password: "sifre1")
expect(model).to be_valid
expect(model.errors[:password]).not_to include("en az bir büyük harf içermeli")
end
end
end
Yukarıdaki test bloğu, PasswordStrengthValidator'ın farklı senaryolarda nasıl davrandığını kapsar. Geçerli parolalar, eksik karakter türleri olan parolalar ve boş parolalar için testler yazılmıştır. Ayrıca, farklı validatör seçenekleriyle (min_length, requires) nasıl çalıştığını doğrulamak için bağlamlar (context) kullanılmıştır. Testleri çalıştırmak için gem'in ana dizininde bundle exec rspec komutunu kullanabilirsiniz.
Harici API entegrasyonlu validatörler için test yazarken, API çağrılarını "mock" veya "stub" etmeniz kritik öneme sahiptir. Bu, gerçek API'ye bağımlı olmadan validatörün kendi mantığını test etmenizi sağlar ve testlerinizin hızlı ve güvenilir olmasını garantiler. Örneğin, SkuAvailabilityValidator için is_sku_available_via_api? metodunu taklit ederek, API'nin farklı yanıtlarını (başarılı, başarısız, hata) simüle edebiliriz. Bu yaklaşım, gem'inizin sadece geliştirme sırasında değil, gelecekteki güncellemelerde de sağlam kalmasını sağlar.
Sonuç: Uygulamalarınızı Özel Validatörlerle Güçlendirin
Bu makale boyunca, Ruby on Rails uygulamalarınızda model doğrulamasını nasıl bir üst seviyeye taşıyacağınızı detaylı bir şekilde inceledik. Standart Rails validatörlerinin yetersiz kaldığı durumlarda, özel validatörlerin kurtarıcı rolünü, nasıl adım adım oluşturulacaklarını ve hatta bunları bir gem olarak paketleyerek yeniden kullanılabilirlik ve sürdürülebilirlik açısından nasıl maksimum fayda sağlayacağımızı öğrendik. Karmaşık iş kurallarını basitleştirmekten, harici API entegrasyonlarıyla veri bütünlüğünü sağlamaya kadar, özel validatörler geliştirici araç setinizin vazgeçilmez bir parçası olmalıdır.
Özel validatörler, kodunuzu daha modüler, okunabilir ve test edilebilir hale getirerek uygulama kalitenizi artırır. Bir validatörü gem olarak yayımlamak, hem kendi projelerinizde tutarlılığı sağlar hem de Ruby topluluğuna katkıda bulunma fırsatı sunar. Unutmayın ki, her zaman en karmaşık çözüme gitmeye gerek yoktur; bazen basit bir ActiveModel::Validator yeterli olurken, bazı durumlarda ActiveModel::EachValidator'ın esnekliği kritik öneme sahip olabilir. Seçiminiz ne olursa olsun, doğru ve eksiksiz testler yazmak, validatörlerinizin gelecekte de güvenilirliğini garanti altına almanın anahtarıdır.
Artık Rails uygulamalarınızda daha güçlü, daha temiz ve daha esnek doğrulama mekanizmaları oluşturmak için gerekli bilgi ve araçlara sahipsiniz. Bu bilgileri kullanarak, projelerinizdeki veri bütünlüğünü korurken geliştirme süreçlerinizi optimize edebilirsiniz.
Sıkça Sorulan Sorular (SSS)
1. Özel validatörler performansı etkiler mi?
Evet, özellikle harici API çağrıları veya yoğun hesaplamalar içeren özel validatörler performansı etkileyebilir. Bu tür validatörler için önbellekleme (caching) teknikleri kullanmak veya asenkron doğrulama (arka plan işleri aracılığıyla) uygulamak performansı önemli ölçüde artırabilir. Basit, dahili doğrulayıcılar genellikle gözle görülür bir performans düşüşüne neden olmaz.
2. Birden fazla validatörü aynı modele uygulayabilir miyim?
Kesinlikle! Bir model, hem standart Rails validatörlerini (presence: true, uniqueness: true vb.) hem de birçok özel validatörü aynı anda kullanabilir. Örneğin, bir kullanıcının e-postasını özel bir format validatörü ile doğrular ve aynı zamanda benzersizliğini standart Rails validatörü ile kontrol edebilirsiniz.
3. Validatörler içinde veritabanı sorgusu yapmak uygun mudur?
Evet, belirli durumlarda uygun olabilir, ancak dikkatli olunmalıdır. Örneğin, bir kaydın başka bir veritabanı tablosundaki belirli bir durumla uyumlu olup olmadığını kontrol etmek gerekebilir. Ancak, her doğrulama çağrısında yapılan yoğun sorgular performans sorunlarına yol açabilir. Bu tür durumlarda, sorguları optimize etmek, indeksler kullanmak veya mümkünse verileri önbelleğe almak önemlidir.
4. EachValidator ile Validator arasındaki fark nedir?
ActiveModel::Validator: Modelin genel durumunu doğrulamak için kullanılır.validate(record)metodunu uygular ve modelin tamamına uygulanır (validates_with MyValidator).ActiveModel::EachValidator: Modelin belirli bir özelliğinin değerini doğrulamak için kullanılır.validate_each(record, attribute, value)metodunu uygular ve doğrudan bir özelliğe (veya birkaç özelliğe) uygulanır (validates :field, my_each_validator: { option: value }). Genellikle daha esnektir ve seçenekler kabul edebilir.
5. Özel validatörler güvenlik riskleri taşır mı?
Yanlış yazılmış özel validatörler güvenlik riskleri taşıyabilir. Özellikle harici servislerle etkileşime giren validatörler (örn: API anahtarlarının açıkta bırakılması, SQL enjeksiyonuna açık sorgular, kötü niyetli veri işleme) dikkatli yazılmalıdır. Daima hassas bilgileri güvenli bir şekilde yönetin (ENV, Rails credentials), girdi verilerini sanitize edin ve potansiyel güvenlik açıklarını minimize etmek için güvenilir kütüphaneler kullanın. Ayrıca, hata mesajlarında aşırı detay vermekten kaçının, bu da potansiyel saldırganlara bilgi sağlayabilir.