⬆️ ⬇️

New approaches to validation in Rails 3

Introduction



As you already know from the post of Comrade. Yehuda Katz on ActiveModel abstraction , in Rails 3.0, ActiveRecord now contains some aspects of ActiveModel, including validation modules.



And before we begin, let's remember what validation methods we already have:All of them are still operational, but Rails 3 offers some new, great alternatives.



New validate method



The validate method accepts an attribute and a hash with validation options. This means that now the usual validation can be written like this:

 class Person < ActiveRecord::Base validates :email, :presence => true end 


The options that can be passed are the following:What gives a vast area of ​​very simple, short options for certain attributes, providing the opportunity to write all the necessary validations in one place.



For example, if you need to check the name and email, you can do this:

 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]+\.)+[az]{2,})$/i} end 
Thus, now you can immediately look at the model to see which validations are attached for each attribute - that there is a small victory for the code and readability :)

')

Fetching familiar usage scenarios



However, write :format => {:with => EmailRegexp} , a bit heavy to write it in several places, which suggests the idea of ​​creating reusable validation, which could be applied to other models.



Let's go further - what if you need to use a much more expressive regular expression, consisting of more than a few characters, to show how cool you can google? :)



Well, validations can be written by hand. First, create an email_validator.rb file in the lib directory, in the depths of our application:

 # 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 


Since each file from the lib directory is loaded automatically, and since our validator inherits from the ActiveModel::EachValidator , our class name is used as a dynamic validator that can be used in any object that has access to ActiveModel::Validations . Ie, for example, these are all objects of ActiveRecord.



The name of the dynamic validator is all that is to the left of the word Validator, reduced to lower case.



Thus, our User class will now look like this:

 # 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 


Notice :email => true ? So much easier, but most importantly - now it can be used anywhere!



And now in the console we will see something like the following (with our own message “not correct”):

 $ ./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=>[" "]}> 


Validations for classes



And what if, say, there are three different models ( user , visitor and customer ), each of which must use general validations. In this case, replacing validates with validates_with , we just have to do this:

 # 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 


And put the file in the lib directory:

 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 


And check on, clearly far-fetched, an example:

 $ ./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"]}> 


Time triggers



As expected, each validation can take the following sub-options:

Each of which can accept a call to an arbitrary method. In this way:

 class Person < ActiveRecord::Base validates :post_code, :presence => true, :unless => :no_postcodes? def no_postcodes? true if ['TW'].include?(country_iso) end end 
This will probably be enough to make a first impression about the new level of flexibility.

Source: https://habr.com/ru/post/82841/



All Articles