Pull to refresh

Новые подходы к валидации в Rails 3

Reading time 4 min
Views 8.7K
Original author: Mikel Lindsaar

Введение


Как вы уже знаете из поста тов. Yehuda Katz об ActiveModel абстракции, в Rails 3.0, ActiveRecord отныне содержит в себе некоторые аспекты ActiveModel, среди которых модули валидации.

И прежде чем мы начнем, давайте вспомним, какие методы валидации у нас уже есть:
  • validates_acceptance_of
  • validates_associated
  • validates_confirmation_of
  • validates_each
  • validates_exclusion_of
  • validates_format_of
  • validates_inclusion_of
  • validates_length_of
  • validates_numericality_of
  • validates_presence_of
  • validates_size_of
  • validates_uniqueness_of
Все они по прежнему в строю, но Rails 3 предлагает несколько новых отличных альтернатив.

Новый метод validate


Метод validate принимает атрибут и хеш с опциями валидации. Это значит, что теперь привычную валидацию можно записать вот так:
class Person < ActiveRecord::Base
  validates :email, :presence => true
end

В качестве опций, которые можно передать, выступают следующие:
  • :acceptance => Boolean
  • :confirmation => Boolean
  • :exclusion => { :in => Ennumerable }
  • :inclusion => { :in => Ennumerable }
  • :format => { :with => Regexp }
  • :length => { :minimum => Fixnum, maximum => Fixnum, }
  • :numericality => Boolean
  • :presence => Boolean
  • :uniqueness => Boolean
Что дает обширную область очень простых, кратких опций для тех или иных атрибутов предоставляя возможность писать все нужные валидации в одном месте.

К примеру, если нужно проверить имя и электронную почту, можно сделать так:
class User < ActiveRecord::Base
  validates :name,  :presence => true, 
                    :length => {:minimum => 1, :maximum => 254}
                   
  validates :email, :presence => true, 
                    :length => {:minimum => 3, :maximum => 254},
                    :uniqueness => true,
                    :format => {:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i}
end
Таким образом, теперь можно взглянув на модель сразу увидеть, какие валидации навешаны для каждого атрибута — что есть небольшая победа для кода и удобочитаемости :)

Извлечение привычных сценариев использования


Тем не менее, записаь :format => {:with => EmailRegexp}, немного тяжеловата, чтоб писать её в нескольких местах, что наталкивает на мысль о создании многократно используемой валидации, которую можно было бы применить в других моделях.

Пойдем дальше — а что если нужно использовать гораздо более выразительное регулярно выражение, состоящее из более чем нескольких символов, чтобы показать, как классно вы умеете гуглить? :)

Чтож, валидации могут быть и написанными вручную. Для начала создадим файл email_validator.rb в каталоге lib, в недрах нашего приложения:
# lib/email_validator.rb
class EmailValidator < ActiveModel::EachValidator

  EmailAddress = begin
    qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
    dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
    atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
      '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
    quoted_pair = '\\x5c[\\x00-\\x7f]'
    domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
    quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
    domain_ref = atom
    sub_domain = "(?:#{domain_ref}|#{domain_literal})"
    word = "(?:#{atom}|#{quoted_string})"
    domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
    local_part = "#{word}(?:\\x2e#{word})*"
    addr_spec = "#{local_part}\\x40#{domain}"
    pattern = /\A#{addr_spec}\z/
  end

  def validate_each(record, attribute, value)
    unless value =~ EmailAddress
      record.errors[attribute] << (options[:message] || "не корректный") 
    end
  end

end

Так как каждый файл из директории lib загружается автоматически, и, так как наш валидатор унаследован от класса ActiveModel::EachValidator, имя нашего класса используется в качестве динамического валидатора, который можно применять в любом объекте, у которого есть доступ к ActiveModel::Validations. Тоесть, к примеру, это все объекты ActiveRecord.

Название динамического валидатора — это всё что стоит левее от слова Validator, приведенное к нижнему регистру.

Таким образом наш класс User теперь будет выглядеть вот так:
# app/models/person.rb
class User < ActiveRecord::Base
  validates :name,  :presence => true, 
                    :length => {:minimum => 1, :maximum => 254}
                   
  validates :email, :presence => true, 
                    :length => {:minimum => 3, :maximum => 254},
                    :uniqueness => true,
                    :email => true
end

Обратили внимание на :email => true? Так гораздо проще, но что самое главное — теперь это можно использовать где угодно!

А в консоли теперь мы увидим нечто следующее (с нашим собственным сообщением “не корректный”):
$ ./script/console 
Loading development environment (Rails 3.0.pre)
?> u = User.new(:name => 'Mikel', :email => 'bob')
=> #<User id: nil, name: "Mikel", email: "bob", created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors
=> #<OrderedHash {:email=>["не корректный"]}>

Валидации для классов


А что если, скажем, существуют три различные модели (user, visitor и customer), каждый из которых должен использовать общие валидации. В таком случае, заменив validates на validates_with, мы просто должны сделать так:
# app/models/person.rb
class User < ActiveRecord::Base
  validates_with HumanValidator
end

# app/models/person.rb
class Visitor < ActiveRecord::Base
  validates_with HumanValidator
end

# app/models/person.rb
class Customer < ActiveRecord::Base
  validates_with HumanValidator
end

А в каталог lib поместим файл:
class HumanValidator < ActiveModel::Validator

  def validate(record)
    record.errors[:base] << "This person is dead" unless check(human)
  end

  private

    def check(record)
      (record.age < 200) && (record.age > 0)
    end
  
end

И проверим на, явно притянутом за уши, примере:
$ ./script/console 
Loading development environment (Rails 3.0.pre)
>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors
=> #<OrderedHash {:base=>["This person is dead"]}>

Время триггеров


Как и стоило ожидать, каждая валидация может принимать следующие под-опции:
  • :on
  • :if
  • :unless
  • :allow_blank
  • :allow_nil
Каждая из которых может принимать вызов произвольного метода. Таким образом:
class Person < ActiveRecord::Base
  
  validates :post_code, :presence => true, :unless => :no_postcodes?

  def no_postcodes?
    true if ['TW'].include?(country_iso)
  end
  
end
Этого, пожалуй, будет достаточно чтобы составить первое впечатление о новом уровне гибкости.
Tags:
Hubs:
+34
Comments 8
Comments Comments 8

Articles