0

Lets say I have a UserModel and an InvitationModel both which contain an attribute email. Let's say that I need to have the same validation for the email across both these models (for example, length should be less than 100 characters).

Rather than duplicate this in the two models, which would make it difficult to change the maximum cap later (more so if there are five/six or more models with the email), I would like to keep the validation in one place. But I would rather make this a composition than an inheritance(since User has an email and not user is an email).

What is the best way I can achieve this with Ruby/Rails?

Can't Tell
  • 12,714
  • 9
  • 63
  • 91

3 Answers3

2

You want to place your validations in a module and include it in the models that need those validations.

module EmailValidation
    extend ActiveSupport::Concern

    included do
        validates :email, length: {minimum: 3, maximum: 100}, presence: true,   uniqueness: true
    end
end 

Then just include the module

class User < ActiveRecord::Base
    include EmailValidation
    ...
end
CaptChrisD
  • 195
  • 5
  • Thanks for your answer. Where would you suggest to put the `EmailValidation` class in the rails project hiearchy? – Can't Tell May 07 '14 at 06:08
  • `EmailValidation` is a module that you would just include into your classes as necessary. In Rails 4, the convention is to place model concerns in the `app/models/concerns` directory. – alkalinecoffee May 07 '14 at 14:10
  • If it was me I would place the module in the lib directory. Since this is a custom module of your own I tend to keep them together in the lib directory. – CaptChrisD May 07 '14 at 15:01
1

There are a couple of ways you could do this, I prefer to store global constants is under the initializer folder.

For example:

Create an initializer file as /config/initializers/my_constants.rb

class MyConstants
  MAX_LENGTH = 100
  ##.. other constants
end

After this use it in your models as:

validates :email, length: { maximum: MyConstants::MAX_LENGTH }

Please note every time you update this initializer file with new global constants, you must restart with rails s to reload the changes.

Say you have two models User with attributes username and email and Campaign with attributes name and email

  • This approach would be more useful, when you need somewhat same validation to attributes with same/ different names across different models i.e., in the above case you can apply same validation for both User#email and Campaign#email. Also, you have the freedom to use same validations for User#username and Campaign#name even though they are named differently.
  • Even in cases when you need one option (say minimum in length validation) to have same value BUT another option(say maximum in length validation) to have different values. Say in case of User#username and Campaign#name, you need minimum to be 2 characters for both and maximum of User#username to be 30 and minimum of Campaign#name to be 50 then you can achieve that easily.

Like I said, you can achieve this in various ways. Another approach suggested by @CaptChrisD is also good for the case where you need exactly same validation and you don't plan on tweaking it in future anyhow.

In that case you can make use of ActiveSupport::Concern

You would need to place the module in app/models/concerns directory and name the file as email_validation.rb.

module EmailValidation
    extend ActiveSupport::Concern
    included do
        validates :email, length: { minimum: 5, maximum: 100 }
    end
end 

After this you would need to include the module in the models where you need the same validation on email field.

class MyModel < ActiveRecord::Base
    include EmailValidation ## Add this
    ##..
end

This is an excellent read on Ruby Mixins & ActiveSupport::Concern for your reference.

You can also check out this SO Answer: Rails put validation in a module mixin?

Community
  • 1
  • 1
Kirti Thorat
  • 52,578
  • 9
  • 101
  • 108
  • Thanks for your answer, but I feel that the method suggested by @CaptChrisD seems to avoid having to declare `validates :email, length: { maximum: MyConstants::MAX_LENGTH }` in multiple models. Do you see any disadvantage in his method? – Can't Tell May 07 '14 at 06:07
  • It depends on the use-case. I prefer this way as it fits my project requirement. Read my updated answer for better insights. – Kirti Thorat May 07 '14 at 13:42
1

You can write your own validator:

class MyEmailValidator < ActiveModel::Validator
  def validate(record)
    # Whatever validation you want on the record
  end
end

And then use it in both your classes:

class User < ActiveRecord::Base
    validates_with MyEmailValidator
end

class InvitationModel < ActiveRecord::Base
    validates_with MyEmailValidator
end

Of course, you can combine this with modules or subclasses as well

John Naegle
  • 8,077
  • 3
  • 38
  • 47