Using a default validator in custom validations - ruby-on-rails

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

Related

Rails: calling standard validations from a custom validator

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.

Rails - one model, 2 kinds of validation rules

In an app I have 3 types of contact forms - in the model - the attributes :aaa, :bbb, :ccc belongs to the second contact form, the previous attributes belongs to the first contact form.
class Message
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :body, :aaa, :bbb, :ccc
validates :name, :email, :body, :aaa, :bbb, :ccc, :presence => true
validates :email, :format => { :with => %r{.+#.+\..+} }, :allow_blank => true
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
What I am trying to do: I am looking for a way, how to validate attributes for the respective contact forms, specifically:
the first contact form contains attributes: :name, :email, :body, which I need to validate
the second contract form contains attributes: :aaa, :bbb, :ccc, :email, which I need to validate
How to do that? How to distinguish, which attributes belongs to which form and validate them?
If there are 3 types of contract forms why not make them 3 separate classes?
If for some erason you still want to keep it in one class, you can do it using 'with_options' magic:
with_options :if => :is_form_1? do |p|
p.validates_presence_of :attr1
p.validates_presence_of :attr2
p.validates_presence_of :attr3
end
with_options :if => :is_form_2? do |p|
p.validates_presence_of :attr4
p.validates_presence_of :attr5
p.validates_presence_of :attr6
end
def is_form_1?
#some logic
end
def is_form_2?
#some logic
end
Still, I don't like the idea of keeping it in one class.
I'd suggest you think about this in behavioural rather than implementation terms. You mention there are three contact forms, but what is the underlying use that you're putting each one to? You shouldn't be thinking about forms when you're setting up your model.
That having been said, you can achieve what you want using the validation_scopes gem. Using validation_scopes, you can define sets of validation rules that you can treat independently. In your controllers, you can then check whichever set of validation rules apply to the context (i.e. which form the user has filled in).
In your model you can set up validation scopes named for each form (or better, named for the context in a way that has semantic value, but I don't know enough about your app to know what the contexts are), like this:
validation_scope :form_one_errors do |vs|
validates :name, :body, :presence => true
end
validation_scope :form_two_errors do |vs|
validates :aaa, :bbb, :ccc, :presence => true
end
Since email needs to be validated in both contexts, you can just set it up as a normal validation (as per your code in the question).
Then in the controller for, say, form one, you can check the scope to see if there are any errors for that context. Note that you have to check the errors for the validation scope separately for the regular validation errors.
if !message.valid?
# Do something with message.errors
elsif message.has_form_one_errors?
# Do something with message.form_one_errors
else
# All good
end

Refactoring custom validation for multiple fields in model

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.

Dynamic form validation in Rails

I have a form which I want to validate. The validation is based on properties in a couple of other model objects, but the form itself does not correspond to a ActiveRecord model.
Would it be possible to use ActiveModel to achieve this?
class Person < ActiveModel
has_one :shoe
validates :name, :length => { :maximum => self.shoe.size }
end
I basically want to validate a form based on the properties of another model object. Is this possible in anyway?
class Person
include ActiveModel::Validations
# has_one :shoe # This won't work
validates :validates_name_length
private
def validates_name_length
errors.add :name, 'too long' if name && name.length > shoe.size
end
end

Retrieve validating field names

I am using Ruby on Rails 3 and I would like to retrieve validating field names. That is, I defined some validation for a class and I would like to retrieve what fields (their names) are candidate for validation on submitting a form.
I need that because I would like to "play" with class error attributes (<name_class>.errors).
How can I do?
You can access your model's validators method. This will return an array of validators on your model.
For example, if you had this:
class User < ActiveRecord::Base
validates :name, :presence => true
validates :email, :uniqueness => true
end
Then you could access the validators like this:
User.validators
# => [#<ActiveModel::Validations::PresenceValidator:0x123456 #attributes=[:name], #options={}>....]
User.validators.first.attributes
# => [:name]
User.validators.first.class
# => ActiveModel::Validations::PresenceValidator

Resources