Is there a way to call standard Rails validators from within a custom validator?
I have a combination of OAuth/email signup/sign in and I want to be able to call certain validators on each method of authentication. For instance, I want to be able to call validates_uniqueness_of :email if the user signs up through email and then call a single validator, for instance validates_with UserValidator.
If there isn't a way to do this I'm just going to use state tracking and a series of :if validations.
I believe there's no way to call other validators from custom one, this also may possibly cause circular dependency which is dangerous.
You have to go with conditional validations, but keep in mind that you can scope them like this (taken from Rails Guides)
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
If your goal is to call some combination of custom and standard rails validators you can do that with the validates method provided by ActiveModel::Validations
For example, you've created a custom Email Validator:
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add attribute, (options[:message] || "is not an email") unless
value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
And you want to include it in your Person class. You can do like so:
class Person < ApplicationRecord
attr_accessor :email
validates :email, presence: true, email: true
end
which will call your custom validator, and the PresenceValidator. All examples here are taken from the docs ActiveModel::Validations::Validates
I'm not sure if this a recent change in Rails but in 6.1 it is possible to call the standard validators from a custom validator:
class VintageYearValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ActiveModel::Validations::NumericalityValidator.new({
attributes: attributes,
only_integer: true,
greater_thank_or_equal_to: 1900,
less_than_or_equal_to: 2100
}).validate_each(record, attribute, value)
# Your custom validation
errors.add() unless bla?
end
end
Those standard Validators are not really documented (https://apidock.com/rails/v6.1.3.1/ActiveModel/Validations/NumericalityValidator) so use at your own risk. But there doesn't seem to be a risk of circular dependency. Both your custom validator and the standard Validator inherit from EachValidator.
Related
I've some Active Record validations on my model:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
end
That seems fine. It validates that the field name is not nil, "" and that it must have exactly 10 characters. Now, if I want to add a custom validation, I'd add the validate call:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_must_start_with_abc
private
def name_must_start_with_abc
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
end
The problem is: when the name field is nil, the presence_of validation will catch it, but won't stop it from validating using the custom method, name_must_start_with_abc, raising a NoMethodError, as name is nil.
To overcome that, I'd have to add a nil-check on the name_must_start_with_abc method.
def name_must_start_with_abc
return if name.nil?
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
That's what I don't wan't to do, because if I add more "dependant" validations, I'd have to re-validate it on each custom validation method.
How to handle dependant validations on Rails? Is there a way to prevent a custom validation to be called if the other validations haven't passed?
I think there is no perfect solution unless you write all your validations as custom methods. Approach I use often:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_custom_validator
private
def name_custom_validator
return if errors.include?(:name)
# validation code
end
end
This way you can add as many validations to :name and if any of them fails your custom validator won't execute. But the problem with this code is that your custom validation method must be last.
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_must_start_with_abc, unless: Proc.new { name.nil? }
private
def name_must_start_with_abc
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
end
Please check allow_blank, :allow_nil and conditional validation as well for more options.
I have a model "User" with attribute "Username". Can I use validations to prevent a User being created with the Username "home"?
class User < ActiveRecord::Base
validates :username, presence: true
end
You can use an exclusion validator:
class User < ActiveRecord::Base
USERNAME_BLACKLIST = ['home'].freeze
validates :username, presence: true, exclusion: { in: USERNAME_BLACKLIST }
end
Alternatively, you can always rely on a custom validation method, using validate instead of validates, for more complex types of validation that aren't easily expressed using built-in validators:
class User < ActiveRecord::Base
validates :username, presence: true
validate :username_not_on_restricted_list
protected
def username_not_on_restricted_list
errors.add(:username, :invalid) if username == 'home'
end
end
You could also write a custom validator if you intend to reuse this functionality across multiple models.
I've defined a class which is getting fat because of many validations defined in it. So, I created a custom validator which includes all validations specific to a given context, and it's working fine.
But the issue is that, while validaing any attribute, the options which are passed while defining a validation aren't getting considered.
Consider this Post class,
class Post
include Mongoid::Document
field :state
field :description
validates_with PublishableValidator, on: :publish
end
Now, while publishing a post, its description is mandatory. So I am validating it with publish context.
#post.valid?(:publish)
Custom validator for all publishable validations is defined as,
class PublishableValidator < ActiveModel::Validator
include ActiveModel::Validations
validates :description, presence: true, unless: :admin?
def validate(post)
self.class.validators.each do |validator|
validator.validate(post)
end
end
end
Now, there is constraint in description validation that, for admin, don't run this validation( not a good idea but admin can do whatever they want :] ).
But when I validate it with blank description and admin privilege, it still gives error, without considering provided constraint.
Suggestions ??
I managed to solve it by using SimpleDelegator class.
First, I inherited PublishableValidator from SimpleDelegator.
Delegated PublishableValidator object to #post.
Ran validations on publishable object.
And last, merged publishable errors to post object.
Updated PublishableValidator
class PublishableValidator < SimpleDelegator
include ActiveModel::Validations
validates :description, presence: true, unless: :admin?
def validate(post)
self.__setobj__(post)
super
post.errors.messages.merge!(self.errors.messages)
end
end
Thanks to this blog
What I am basically trying to do is to create a custom validation which calls a RoR default validation with specific options to try and reduce boilerplate (and have this validation to be used globally by all models)
The way to do custom validations on a certain field in RoR is by using the validates_each method, like so
class SelectBooleanValidator < ActiveModel::EachValidator
def validate_each(record,attr,value)
#do validation here
end
end
What I am trying to do is to call the inclusion validator method within the validator_each, so that the select_boolean validation (which I am implementing) just uses the :inclusion validator with certain options, i.e. I want to do something like this (note that this code doesn't actually work, but the below is what I am basically trying to do)
class SelectBooleanValidator < ActiveModel::EachValidator
include ActiveModel::Validations
def validate_each(record,attr,value)
validates_with InclusionValidator, record,attr,value, {:in => [true, false],:message=>'can\'t be blank'}
end
end
And then I would (inside my model) just do this
validates :awesome_field, select_boolean:true
Instead of having to do this all the time
validates :awesome_field, :inclusion => {:in => [true, false], message: 'can\'t be blank'}
class SelectBooleanValidator < ActiveModel::Validations::InclusionValidator
def options
super.merge(:in => [true, false], message: 'can\'t be blank')
end
end
I have a model with a custom validation function which verifies that the date is not in the past. Currently, the validation is hard-coded to check a single field, selected_date, in the model. How do I go about refactoring the validation so that I can either pass a parameter to the custom validation so I can test 2 fields?
class Appointment < ActiveRecord::Base
attr_accessible :selected_date, :alternate_date
validates_presence_of :selected_date
validate :validate_date
def validate_date
if selected_date < Date.today
errors.add(:selected_date, 'Date has passed')
end
end
end
create file lib/future_validator.rb:
class FutureValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
if value < Date.today
object.errors[attribute] << "has passed"
end
end
end
In your models:
validates :selected_date, presence: true, future: true
validates :other_date, presence: true, future: true
See this RailsCast: Validations in Rails 3
NOTE: Make sure lib files are auto loaded in config/application.rb and restart the server after adding that file.