Validation in rails with Themis
Serhii Potapov August 19, 2013 #Rails #ActiveRecord #validation #rubySometimes ActiveRecord is not enough to meet complicated validation needs. At TMXCredit we've created Themis - ActiveRecord extension which helps to organize validations in a better way and adds some flexibility. Here I'm gonna describe some problems which Themis solves after that I'll take a brief look at possible alternative solutions.
Modular validation
Themis allows you to extract duplicated validations into module for reuse. Usually rails applications are small enough so you don't need it. But sometimes you do.
The next example is pretty flat(in real life you probably would use STI or composition to
represent Doctor
and Patient
models) but it illustrates where Themis could be useful.
Let's say you have 2 models:
:first_name, :last_name, :email, :diploma,
:presence => true
end
validates :first_name, :last_name, :email, :age,
:presence => true
end
validates
You see that both models have the same validation for first_name
, last_name
and email
.
Themis allows you to fix the duplication problem by extracting common validations into a module:
# Module with common validations.
extend Themis::Validation
validates :first_name, :last_name, :email, :presence => true
end
# import validation of first_name, last_name, email
include PersonValidation
validates :diploma, :presence => true
end
include PersonValidation
validates :age, :presence => true
end
So now we keep the common validation in one place. If you want, you can include validation modules into each other to combine necessary validation.
Validation scenarios
Here is another problem which Themis solves.
We have the following models:
:person
has_many :user_accounts
end
attr_accessible :first_name, :last_name, :birhday
belongs_to :user
end
attr_accessible :email, :login
belongs_to :user
end
has_one
So we have a model graph like this with User
model on the top:
It's pretty small, but in real life the graph can be much deeper.
What would you do if you needed to apply different validations depending on context?
For example according to your business requirements users must be allowed to use
your application only in case if they've filled in all of the fields.
So you need to validate presense of first_name
, last_name
and birhday
on Person
model and email
, login
and password
on UserAccount
.
It's not a problem, just add the validations to appropriate models:
:first_name, :layout, :birhday, :presence => true
end
validates :email, :login, :presence => true
end
validates
There is some percent of users who don't finish registration process. But your marketing department wants to have an ability to contact them if they have entered an email address.
So that's where the issue is: you can't save records using validation rules written above.
With Themis you can declare number of validation strategies, and depending on context, chose which one you need.
Here is how a complete solution looks:
:person
has_many :user_accounts
accepts_nested_attributes_for :person, :user_accounts
# Declare validations. Use :full as default.
has_validation :full, :default => true
has_validation :partial
end
attr_accessible :first_name, :last_name, :birhday
belongs_to :user
# Declare full validation
has_validation :full do
model.validates :first_name, :last_name, :birhday, :presence => true
end
# Delcare partial validation. Nothing to validate.
has_validation :partial
end
attr_accessible :email, :login
has_validation :full do
model.validates :login, :email, :presence => true
end
has_validation :partial do
model.validates :email, :presence => true
end
end
has_one
And here is how you would use it somewhere in a controller:
# Create model initialized with params
user = User.new(
:person => {
:first_name => ,
:last_name => ,
:birhday =>
},
:user_accounts => [{
:email =>
}]
)
user.valid? # => false, because login is missing
# Try to apply partial validation
user.use_validation(:partial)
user.valid? # => true
# We can save it
user.save!
Alternative solutions
If you think Themis is overkill for your project, you still have some options.
Using ActiveSupport::Concern for modularity
ActiveSupport::Concern
is another way which allows to extract common validations
into module. Here how would PersonValidation
module described above could look:
extend ActiveSupport::Concern
included do
validates :first_name, :last_name, :email, :presence => true
end
end
Using conditional validation
If your requirements aren't so fancy, you can be satisfied with a simple conditional validation, e. g.
:first_name, :last_name, :birhday, :presence => true,
:if => :use_full_validation?
# Lets add one more validation statement(for the next example)
validates :first_name, :last_name, :length => { :maximum => 255 },
:if => :use_full_validation?
# Some logic goes here
end
end
validates
To DRY up :if
options it's good to use with_options
method:
:if => :use_full_validation? do
person.validates :first_name, :last_name, :birhday, :presence => true
person.validates :first_name, :last_name, :length => { :maximum => 255 }
end
end
with_options
Now :if => :use_full_validation
will be additonaly passed to every method call
on person
inside the block.
Vanguard
The guys from the ROM project have their own validator called Vanguard(previous name is Aqeuitas). The sweet thing about it is that it allows to seperate validations and models according to DataMapper approach. The downside is if you use ActiveRecord you'll have a zoo of validation tools. Also it may be still raw and I'm not sure is it possible to apply it to solve the described problem, but I'd encourage you to take a look at it.
Conclusion
ActiveRecord is good for plain and straightforward projects. In big enterprise applications usually we need more flexibility to meet different exotic requiments. We've created Themis to extend ActiveRecord and solve some of the problems. Actually I hope that ROM will be ready soon and we'll have an ability to select right ORM before diving into development.
Thanks for reading. Hope the article was useful for you and I'm wating for your feedback!
Links
- Themis on Github - you'll find here comprehensive documentation in README;
- Conditional validation - extraction from Rails Guide;
- Vanguard on Github - validator for ROM project;
- Railscast: #42 - Ryan Bates describes how
with_options
works.