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
end
which 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
end
which 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 }
end
and 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
end
Brand 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