Sergey Potapov

A word from rustacean, rubist and linuxoid.

Validation in Rails With Themis

Sometimes 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:

1
2
3
4
5
6
7
8
9
class Doctor < ActiveRecord::Base
  validates :first_name, :last_name, :email, :diploma,
            :presence => true
end

class Patient < ActiveRecord::Base
  validates :first_name, :last_name, :email, :age,
            :presence => true
end

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Module with common validations.
module PersonValidation
  extend Themis::Validation

  validates :first_name, :last_name, :email, :presence => true
end

class Doctor < ActiveRecord::Base
  # import validation of first_name, last_name, email
  include PersonValidation

  validates :diploma, :presence => true
end

class Patient < ActiveRecord::Base
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User < ActiveRecord::Base
  has_one :person
  has_many :user_accounts
end

class Person < ActiveRecord::Base
  attr_accessible :first_name, :last_name, :birhday

  belongs_to :user
end

class UserAccount < ActiveRecord::Base
  attr_accessible :email, :login

  belongs_to :user
end

So we have a model graph like this with User model on the top:

Themis - model graph

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:

1
2
3
4
5
6
7
class Person < ActiveRecord::Base
  validates :first_name, :layout, :birhday, :presence => true
end

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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class User < ActiveRecord::Base
  has_one :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

class Person < ActiveRecord::Base
  attr_accessible :first_name, :last_name, :birhday

  belongs_to :user

  # Declare full validation
  has_validation :full do |model|
    model.validates :first_name, :last_name, :birhday, :presence => true
  end

  # Delcare partial validation. Nothing to validate.
  has_validation :partial
end

class UserAccount < ActiveRecord::Base
  attr_accessible :email, :login

  has_validation :full do |model|
    model.validates :login, :email, :presence => true
  end

  has_validation :partial do |model|
    model.validates :email, :presence => true
  end
end

And here is how you would use it somewhere in a controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Create model initialized with params
user = User.new(
  :person => {
    :first_name => "Alex",
    :last_name  => "DeLarge",
    :birhday    => "1962"
  },
  :user_accounts => [{
    :email => "clockwork@orange.com"
  }]
)

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:

1
2
3
4
5
6
7
module PersonValidation
  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.

1
2
3
4
5
6
7
8
9
10
11
12
class Person < ActiveRecord::Base
  validates :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?

  def use_full_validation?
    # Some logic goes here
  end
end

To DRY up :if options it’s good to use with_options method:

1
2
3
4
5
6
class Person < ActiveRecord::Base
  with_options :if => :use_full_validation? do |person|
    person.validates :first_name, :last_name, :birhday, :presence => true
    person.validates :first_name, :last_name, :length => { :maximum => 255 }
  end
end

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

Comments