📜 ⬆️ ⬇️

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