ruby-on-railsrubyactivesupportactivesupport-concern

Including a Validator class from a Concern in Rails


I've got a custom EachValidator that is used in two different models. I moved it to a Concern to DRY the models:

module Isbn
  extend ActiveSupport::Concern

  included do
    class IsbnValidator < ActiveModel::EachValidator
      GOOD_ISBN = /^97[89]/.freeze

      def validate_each(record, attribute, value)
       # snip...
      end
    end
  end
end
class Book < ApplicationRecord
  include Isbn

  validates :isbn, allow_nil: true, isbn: true
end
class BookPart < ApplicationRecord
  include Isbn

  validates :isbn, allow_nil: true, isbn: true
end

When running Rails (in this case via RSpec), I get this warning:

$ bundle exec rspec
C:/Users/USER/api/app/models/concerns/isbn.rb:16: warning: already initialized constant Isbn::IsbnValidator::GOOD_ISBN
C:/Users/USER/api/app/models/concerns/isbn.rb:16: warning: previous definition of GOOD_ISBN was here

Is there any way to avoid it and include the validator cleanly in each model?


Solution

  • Each time you include your Isbn module it triggers included method which opens IsbnValidator < ActiveModel::EachValidator class and creates GOOD_ISBN constant and validate_each method inside of it. Note that these constant and method are created each time in the same class - IsbnValidator < ActiveModel::EachValidator.

    So, the first time you included Isbn module you created GOOD_ISBN constant inside IsbnValidator < ActiveModel::EachValidator, after that you included Isbn module into another class and included method tried to create GOOD_ISBN constant again in IsbnValidator < ActiveModel::EachValidator and obviously failed with that error you got.

    So instead your included method should look like this:

    module Isbn
      extend ActiveSupport::Concern
    
      included do
        GOOD_ISBN = /^97[89]/.freeze
    
        def validate_each(record, attribute, value)
         # snip...
        end
      end
    end
    

    This way GOOD_ISBN and validate_each will be created for the classes you import Isbn into (i.e. for Book and BookPart)