Search code examples
ruby-on-railsrubyruby-on-rails-5customvalidator

How can I load a custom validator which does not directly inherit from a Rails validator class in Rails 5?


I was writing some custom validators for a Rails 5 project I have been working on. For example:

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    if value.present? && !/\A[^\p{Z}]+@[^\p{Z}]+\.[^\p{Z}]+\z/.match?(value)
      record.errors.add(attribute, "must be an email")
    end
  end
end

This approach worked fine, but many of my custom validators were similarly checking for a regex match so I decided to create an abstract RegexValidator class:

class RegexValidator < ActiveModel::EachValidator
  self.abstract_class = true

  def regex
    raise "Children of this class must implement the 'regex' method."
  end

  def message(record, attribute, value)
    raise "Children of this class must implement the 'message(record, attribute, value)' method."
  end

  def validate_each(record, attribute, value)
    if value.present? && !regex.match?(value)
      record.errors.add(attribute, message(record, attribute, value))
    end
  end
end

So now EmailValidator is implemented like so:

class EmailValidator < RegexValidator
  def regex
    /\A[^\p{Z}]+@[^\p{Z}]+\.[^\p{Z}]+\z/
  end

  def message(record, attribute, value)
    "must be an email"
  end
end

However, since this change Rails 5 no longer autoloads my EmailValidator even though both RegexValidator and EmailValidator are placed in appropriately named files, regex_validator.rb and email_validator.rb, in the app/validators folder which was being autoloaded before (this makes this question different from a similar question). I suspect this is because EmailValidator no longer directly inherits from ActiveModel::EachValidator, but this should not matter.

For reference, the following error occurred when I tried to generate a migration file:

/var/lib/gems/2.5.0/gems/activemodel-5.2.2/lib/active_model/validations/validates.rb:121:in `rescue in block in validates': Unknown validator: 'EmailValidator' (ArgumentError)
        from /var/lib/gems/2.5.0/gems/activemodel-5.2.2/lib/active_model/validations/validates.rb:118:in `block in validates'
        from /var/lib/gems/2.5.0/gems/activemodel-5.2.2/lib/active_model/validations/validates.rb:114:in `each'
        from /var/lib/gems/2.5.0/gems/activemodel-5.2.2/lib/active_model/validations/validates.rb:114:in `validates'
        from /home/tomeraberbach/Desktop/msf/src/app/models/user.rb:7:in `<class:User>'
        from /home/tomeraberbach/Desktop/msf/src/app/models/user.rb:4:in `<main>'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `load'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `load'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:476:in `block in load_file'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:661:in `new_constants_in'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:475:in `load_file'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:373:in `block in require_or_load'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:37:in `block in load_interlock'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies/interlock.rb:14:in `block in loading'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/concurrency/share_lock.rb:151:in `exclusive'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies/interlock.rb:13:in `loading'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:37:in `load_interlock'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:356:in `require_or_load'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:46:in `block in require_or_load'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:16:in `allow_bootsnap_retry'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:45:in `require_or_load'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:510:in `load_missing_constant'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:58:in `block in load_missing_constant'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:16:in `allow_bootsnap_retry'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:57:in `load_missing_constant'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:195:in `const_missing'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/inflector/methods.rb:283:in `const_get'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/inflector/methods.rb:283:in `block in constantize'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/inflector/methods.rb:281:in `each'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/inflector/methods.rb:281:in `inject'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/inflector/methods.rb:281:in `constantize'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:582:in `get'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:613:in `constantize'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise.rb:316:in `get'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/mapping.rb:83:in `to'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/mapping.rb:78:in `modules'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/mapping.rb:95:in `routes'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/mapping.rb:162:in `default_used_route'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/mapping.rb:72:in `initialize'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise.rb:346:in `new'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise.rb:346:in `add_mapping'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/rails/routes.rb:243:in `block in devise_for'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/rails/routes.rb:242:in `each'
        from /var/lib/gems/2.5.0/gems/devise-4.5.0/lib/devise/rails/routes.rb:242:in `devise_for'
        from /home/tomeraberbach/Desktop/msf/src/config/routes.rb:3:in `block (2 levels) in <main>'
        from /var/lib/gems/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/routing/mapper.rb:879:in `scope'
        from /home/tomeraberbach/Desktop/msf/src/config/routes.rb:2:in `block in <main>'
        from /var/lib/gems/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rb:432:in `instance_exec'
        from /var/lib/gems/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rb:432:in `eval_block'
        from /var/lib/gems/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rb:414:in `draw'
        from /home/tomeraberbach/Desktop/msf/src/config/routes.rb:1:in `<main>'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `load'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `load'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:285:in `block in load'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:257:in `load_dependency'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:285:in `load'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:41:in `block in load_paths'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:41:in `each'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:41:in `load_paths'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:20:in `reload!'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:30:in `block in updater'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/file_update_checker.rb:83:in `execute'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/routes_reloader.rb:10:in `execute'                                                      
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application/finisher.rb:130:in `block in <module:Finisher>'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/initializable.rb:32:in `instance_exec'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/initializable.rb:32:in `run'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/initializable.rb:61:in `block in run_initializers'
        from /usr/lib/ruby/2.5.0/tsort.rb:228:in `block in tsort_each'
        from /usr/lib/ruby/2.5.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
        from /usr/lib/ruby/2.5.0/tsort.rb:431:in `each_strongly_connected_component_from'
        from /usr/lib/ruby/2.5.0/tsort.rb:349:in `block in each_strongly_connected_component'
        from /usr/lib/ruby/2.5.0/tsort.rb:347:in `each'
        from /usr/lib/ruby/2.5.0/tsort.rb:347:in `call'
        from /usr/lib/ruby/2.5.0/tsort.rb:347:in `each_strongly_connected_component'
        from /usr/lib/ruby/2.5.0/tsort.rb:226:in `tsort_each'
        from /usr/lib/ruby/2.5.0/tsort.rb:205:in `tsort_each'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/initializable.rb:60:in `run_initializers'
        from /var/lib/gems/2.5.0/gems/railties-5.2.2/lib/rails/application.rb:361:in `initialize!'
        from /home/tomeraberbach/Desktop/msf/src/config/environment.rb:5:in `<main>'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `block in require_with_bootsnap_lfi'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/loaded_features_index.rb:65:in `register'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:20:in `require_with_bootsnap_lfi'
        from /var/lib/gems/2.5.0/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:29:in `require'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:291:in `block in require'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:257:in `load_dependency'
        from /var/lib/gems/2.5.0/gems/activesupport-5.2.2/lib/active_support/dependencies.rb:291:in `require'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application.rb:102:in `preload'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application.rb:153:in `serve'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application.rb:141:in `block in run'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application.rb:135:in `loop'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application.rb:135:in `run'
        from /var/lib/gems/2.5.0/gems/spring-2.0.2/lib/spring/application/boot.rb:19:in `<top (required)>'
        from /usr/local/lib/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        from /usr/local/lib/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        from -e:1:in `<main>'

My User class:

##
# A class representing a user account for this web application
# Instances of this class represent rows in the +users+ table on the database.
class User < ApplicationRecord
  # Validation
  validates_presence_of :email, :encrypted_password
  validates :email, email: true

  # Associations
  devise :database_authenticatable, :registerable, :recoverable,
         :rememberable, :trackable, :validatable
  has_and_belongs_to_many :roles

  # Events
  before_validation do
    # Removes unnecessary whitespace
    self.email = email.strip
  end
end

Any ideas?


Solution

  • I figured out the problem. The self.abstract_class = true line in RegexValidator was silently raising an error because there is no self.abstract_class method in the class. I thought there was because I saw it in the ApplicationRecord class, but I now realize that this is defined in the ActiveRecord::Base class.

    I determined this by adding require_relative "../validators/email_validator" to the top of the user.rb file. After doing that I was able to see the error which was silent before:

    /home/tomeraberbach/Desktop/msf/src/app/validators/regex_validator.rb:4:in `<class:RegexValidator>': undefined method `abstract_class=' for RegexValidator:Class (NoMethodError)
    

    After removing the self.abstract_class = true line and the require_relative "../validators/email_validator" line, the problem was resolved.