class Brand < ActiveRecord::Base validate :name_likeness private def name_likeness like_conditions = name.split.map { |term| self.class.arel_table[:name].matches("%#{term}%") }.inject(&:or) return if self.class.where(like_conditions).count.zero? errors.add :name, "has already been taken likewise" end endwhich ensures the brands name has no term, which already was used in any brands name component.
and using it:
Brand.where("name LIKE ?", "%Fox%") => [<Brand id:1, name: 'Fox Media'] brand = Brand.new name: 'Fox Racing' brand.save => false brand.full_messages_for :name => ["Name has already been taken likewise"]which works but there are various reasons why it should be refactored:
- when the custom validation has to be shared on several model classes
- because complex validation logic should be extracted into a concerning class
class LikenessValidator < ActiveModel::EachValidator def validate_each record, attribute, value like_conditions = value.split.map { |term| record.class.arel_table[:name].matches("%#{term}%") }.inject(&:or) return if record.class.where(like_conditions).count.zero? record.errors.add :name, "has already been taken likewise" end endwhich inherits from ActiveModel::EachValidator and overwrites its validate_each. The logic mostly was copied from the former validating method with the exception of moving the scope from self to record. The former name is passed by the parameter value.
Of course the autoload_paths should be expanded (config/application.rb):
config.autoload_paths += %W["#{config.root}/app/validators/"]Then the Brand model class can be refactored:
class Brand < ActiveRecord::Base validates :name, likeness: true endPlease note the naming convention: the validation class was named LikenessValidator and is pointed to with the option likeness.
Validating again:
Brand.where("name LIKE ?", "%Fox%") => [<Brand id:1, name: 'Fox Media'] brand = Brand.new name: 'Fox Racing' brand.save => false brand.full_messages_for :name => ["Name has already been taken likewise"]works as expected.
Additional parameters can be assigned like:
class Brand < ActiveRecord::Base validates :name, likeness: { min_length: 5 } endand used in the custom validation class:
class LikenessValidator < ActiveModel::EachValidator def validate_each record, attribute, value terms = terms_with_min_length value, options[:min_length] return if terms.empty? like_conditions = terms.map { |term| record.class.arel_table[:name].matches("%#{term}%") }.inject(&:or) return if record.class.where(like_conditions).count.zero? record.errors.add :name, "has already been taken likewise" end private def terms_with_min_length term, min_length=nil terms = term.split return terms if min_length.nil? term.split.select { |term| term.length >= min_length } end endBrand name terms with less than 5 characters satisfy the validation:
Brand.where("name LIKE ?", "%Fox%") => [<Brand id:1, name: 'Fox Media'] brand = Brand.new name: 'Fox Racing' brand.save => true
Further articles of interest:
Supported by Ruby 2.1.1 and Ruby on Rails 4.1.8
Keine Kommentare:
Kommentar veröffentlichen