Question

I just ran across Rails concerns and I want to use them for the validations of my models. But I want the validations to be generic, so that the validation is used only if the Class in which I include my concern has the attribute. I thought it would be easy, but I have tried many ways like using column_names, constantize, send and many other but nothing works. What is the right way to do it? The code:

module CommonValidator
  extend ActiveSupport::Concern

  included do
    validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") }, 
                      format: { with: /\A[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i, 
                      message: I18n.t(:"validations.commons.email_wrong_format"), 
                            allow_blank: true } if self.column_names.include? :email
  end
end

class Restaurant < ActiveRecord::Base
  include CommonValidator
  .
  .
  .
end

Restaurant of course has an email attribute. Is it possible to check the existence of an attribute in the class in which in include my concern? I want include my CommonValidations into many models which will not have email attribute. I'm using rails 4.

Was it helpful?

Solution

You can use respond_to? on the current instance as follows:

validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") }, 
                  format: { with: /\A[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i, 
                  message: I18n.t(:"validations.commons.email_wrong_format"), 
                  allow_blank: true }, 
                  if: lambda { |o| o.respond_to?(:email) }

Another option as suggested by @coreyward is to define a class extending EachValidator. For example, for email validation:

# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i
      record.errors[attribute] << (options[:message] || I18n.t(:"validations.commons.email_wrong_format"))
    end
  end
end

Then you could update the validation call as:

validates :email, 
          presence: { message: I18n.t(:"validations.commons.email_missing") },
          email: true, 
          allow_blank: true

OTHER TIPS

I was looking for something similar, but with custom validations. I ended up with something that I think could be shared, including generic tests.

First, set up the concern app/models/concern/my_concern.rb.

Please note that we don't define the validate_my_field into a ClassMethods module.

module MyConcern
  extend ActiveSupport::Concern

  included do
    validate :my_field, :validate_my_field
  end

private

  def validate_my_field
    ...
  end

end

Include concern into your model app/models/my_model.rb

class MyModel < ActiveRecord::Base
  include MyConcern
end

Load concerns shared examples in spec/support/rails_helper:

…
  Dir[Rails.root.join('spec/concerns/**/*.rb')].each { |f| require f }
…

Create concern shared examples spec/concerns/models/my_field_concern_spec.rb:

RSpec.shared_examples_for 'my_field_concern' do
  let(:model) { described_class } # the class that includes the concern

  it 'has a valid my_field' do
    instance = create(model.to_s.underscore.to_sym, my_field: …)
    expect(instance).not_to be_valid
    …
  end
end

Then finally call shared examples into your model spec spec/models/my_model_spec.rb:

require 'rails_helper'

RSpec.describe MyModel do
  include_examples 'my_field_concern'

  it_behaves_like 'my_field_concern'
end

I hope this could help.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top