How custom validators is auto included into model - ruby-on-rails

Currently, I've created custom email validator for rails model.
models/concerns/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "provided format for email is not valid")
end
end
end
So now I can use it like:
validates :email, email: true, uniqueness: true
I'm just curious how it's auto included into model? I mean we are not including it explicitly, by using include method.

Everything under the app/ folder is auto-loaded. So, since you've placed it in models/concerns and models is under app/, it is auto-loaded. Once it is auto-loaded, it will be used as the name is inferred from the option name you pass to validates :email. You can place it in app/foo/bar/baz/email_validator.rb and it will be auto-loaded as well. Move this validator to lib/email_validator.rb and this will not work (as long as you have not required the whole lib/ folder).

Additionally validator classes may be in another namespace and still used within any class.
validates :email, :'custom_validators/email' => true
Module CustomValidators
class EmailValidator < ActiveModel::EachValidator
# Code
end
end
Please refer this link for more info

Related

Checking if attribute_changed? in ActiveModel::EachValidator

I have a class Person that has first_name, middle_name, last_name.
I have a customed Each validator for these 3 attributes like so
validates :first_name, :middle_name, :last_name, nameValidator: true
In this validator I want to check if any one of these 3 attributes changed and given a few more conditions I'll validate name. For that I'm trying attribute_changed? but it doesn't work.
I've checked different methods from ActiveModel::Dirty and Activerecod::Dirty but nothing seems to work to check changes in each attribute. What am I missing?
module Person
class nameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless can_normalize?(record)
#Normalization code
end
def can_normalize?(record)
anything_new = record.new_record? || attribute_changed?
end
end
end
If you need to check that some attribute was changed, you need to call attribute_changed? method on the record and pass this attribute like this
return unless record.new_record? || record.attribute_changed?(attribute)
Or may be use metaprogramming method like this
return unless record.new_record? || record.public_send("#{attribute}_changed?")
Some notes:
In Ruby we usually use PascalCase for class names and snake_case for hash keys
Validations are used for data validation, not for some normalization. It is usually used to add validation errors

How can I store a regex expression string in a helper method to use to validate several different fields?

I have several different fields that store different phone numbers in my rails app as a string. I can use the built-in rails format validator with a regex expression. Is there a way to place this expression in a helper method for all my phone number validations instead of having to open each model file and paste the expression string.
You can require any file from application.rb:
# application.rb
require Rails.root.join "lib", "regexes.rb"
# lib/regexes.rb
PHONE_NUMBER_REGEX = /your regex/
Then you simply use the constant wherever needed
You can alternatively make use of the built in autoload functionality of Rails, for example with the concern approach the other commenter laid out - the concern file is autoloaded by default, as are models, controllers, etc
Loading custom files instead of using the Rails' defaults might not seem idiomatic or the "rails way". However, I do think it's important to understand that you can load any files you want. Some people autoload the entire lib/ folder and subfolders (see Auto-loading lib files in Rails 4)
Another alternative is to place your code somewhere in the config/initializers folder, these files are automatically loaded at startup and you can define shared classes/modules/constants there
Add a custom validator app/validators/phone_validator.rb
class PhoneValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.to_s =~ /YOUR_REGEX_HERE/
record.errors.add attribute, 'must be a valid phone number'
end
end
end
Then in your models
class MyModel < ApplicationRecord
#phone: true tells it to use the PhoneValidator defined above
validates :phone_number, presence: true, phone: true
end
One way to do this is to create a concern that will be included in each model that has a phone number. In models/concerns, create a new file called something like phonable.rb.
# models/concerns/phonable.rb
module Phonable
extend ActiveSupport::Concern
VALID_PHONE_REGEX = /\A\+?\d+\z/ # Use your regex here
end
Now include the concern like this:
# models/my_model.rb
class MyModel < ApplicationRecord
include Phonable
validates :phone, format: {with: VALID_PHONE_REGEX}
...
end
Now that you have a Phonable concern, you can put any other phone-number-related logic here as well, such as parsing and the like. The advantage of this approach is that all your logic related to phone numbers will be available for use in the models that need it, and none of that logic will be available in models that don't need it.
You can put it in ApplicationRecord (application_record.rb)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
VALID_PHONE_NUMBER_REGEX = /\d{10}/ # your regex here
end
And then you can use it in any model that inherits fro ApplicationRecord

Use value in override error message in Rails 4 for custom Validator

I am on Rails 4.2 I have written a custom Validator which will check if a value being entered exists in another table. I have been reviewing some other posts and it seems there is either a context specific or rails version preferred way to reuse the value being validated. In the rails docs i see examples such as:
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
however, if I try to use %{value} in my custom message override it does not interpolate, but just prints "%{value}". I have seen various ways of calling "value". I also could not get %{value} to work in my Validator definition, but could get #{value} to work (New to ruby, if#{value} getting it from validate_each?).
I have also been struggling with various formats of the validation statement and putting in the custom message. Some things which look repeatable from the docs are not. If the way I am declaring my custom message is causing the error, please let me know how to correct?
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors[attribute] << (options[:message] || "#{value} is not a valid
group code")
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, :existing_group => {:message => "The code you have enterd ( **what goes here?** ) is not a valid code, please check with your teacher or group
leader for the correct code." }
end
Rails automatically puts the value at the beginning of the phrase, so you can just do this:
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(code: value).any?
record.errors[attribute] << (options[:message] || 'is not a valid group code')
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, existing_group: {message: 'is not a valid code, please check with your teacher or group leader for the correct code.' }
end
Separately, note that it is always "#{1+1}" to interpolate not %.
Rails is using Internationalization style string interpolation to add the value to the message. You can use the I18n.interpolate method to accomplish this. Something like this should do the trick:
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors[attribute] << (I18n.interpolate(options[:message], {value: value}) || "is not a valid group code")
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, :existing_group => {:message => "The code you have entered, "%{value}", is not a valid code, please check with your teacher or group leader for the correct code." }
end

Rails: validate by calling class method of other class

I have a lot of models that contains a field called source_name. I need to implement a validator in each of them that will check if the source_name lives up to curtain conditions.
Now I also have another class called SourceNameManager. In this model I have a method called valid_source_name? which takes a source_name_name and returns true or false.
What is the simplest way to make a validation that just validates source_name by calling the external service class SourceNameManager.valid_source_name?('some_name').
I was thinking about something like:
validates :source_name, ->(record) { SourceNameManager.valid_source_name?(record.source_name) }
but I don't think that works
Create a file app/models/source_name_validator.rb:
class SourceNameValidator < ActiveModel::EachValidator
validate_each(record, attribute, value)
unless SourceNameManager.valid_source_name?(value)
record.errors[attribute] << 'is not valid'
end
end
end
Then in each model where you want to validate the source name, add:
validates :source_name, source_name: true

Rails 4 - concerns for generic validation

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.
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
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.

Resources