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.
Related
I'm trying to add validations to my mobility-powered application and i'm confused a little.Earlier I've used code like this
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, uniqueness: {scope: :animal_type}
end
And it worked fine. But in my last project it doesn't work at all. Any ideas how to perform validations? My config is below:
Mobility.configure do
plugins do
backend :container
active_record
reader
writer
backend_reader
query
cache
presence
locale_accessors
end
end
UPD: I've identified my problem - it is because of , uniqueness: {scope: :animal_type}. Is it possible to use mobility with similar type of validations?
When you use uniqueness validator it uses query to the database to ensure that record haven't already taken and you get this query:
Let's assume you have Animal model
SELECT 1 AS one FROM "animals" WHERE "animals"."name_en" = "Cat" AND "animals"."animal_type" = "some_type" LIMIT 1
And of course there is no name_en field in the animals table that is why you have an error
To archive this you have to write your own validator
class CustomValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if translation_exists?(record, attribute)
record.errors.add(attribute, options[:message] || :taken)
end
end
private
def translation_exists?(record, attribute)
attribute, locale = attribute.to_s.split('_')
record.class.joins(:string_translations).exists?(
mobility_string_translations: { locale: locale, key: attribute },
animals: { animal_type: record.animal_type }
)
end
end
And then in your model do next:
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, custom: true
end
Let's say I have lots of attributes that can only have a specific set of string values.
Typically we'd see the following.
class User < ApplicationRecord
validates :foo, inclusion: { in: ['some', 'array'] }
validates :bar, inclusion: { in: ['another', 'array'] }
validates :moo, inclusion: { in: ['one_more', 'array'] }
end
I have lots of these types of validations in my model and I want to DRY them up. So I tried the below but I get a error undefined method 'validates' for #User:0x00007fdc10370408.
class User < ApplicationRecord
VALIDATION_ENUMS = {
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.freeze
validate :validate_enums
def validate_enums
VALIDATION_ENUMS.each_key do |attribute|
validates attribute, inclusion: { in: VALIDATION_ENUMS[attribute] }
end
end
end
How do I get access to the ActiveModel::Validations helper methods from within my function?
Or is there a better way to do this?
Remember that validates is a class method, only executed once when the class is loaded to establish what will be validated. validate is calling an instance method.
A better way might be to execute the DRY code immediately when loading the class.
class User < ApplicationRecord
validate_enums = {
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.freeze
validate_enums.each do |key, array|
validates key, inclusion: { in: array }
end
Note that as you don't reference validate_enums ever again, you don't need to make it a class constant, which is why I didn't.
But you don't really save any lines and add complexity, so I'd stick with the explicit validates, myself.
This approach won't fly. The validation methods are class methods that modify the class itself while you are writing an instance method that get called on an instance of the class when #valid? is called.
If you want to dynamically add existing validations to the class you need to create a class method:
class User < ApplicationRecord
def self.add_inclusion_validations(hash)
# don't use each_key if you're iterating over both keys and values
hash.each do |key, values|
validates_presence_of key, in: values
end
end
add_inclusion_validations(
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
)
end
Of course you could also just skip the method completely:
class User < ApplicationRecord
{
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.each do |key, values|
validates_presence_of key, in: values
end
end
If what you instead want is to write a validation method that uses the existing functionality of other validations you would create a ActiveRecord::Validator or ActiveRecord::EachValidator subclass and use the existing validations there. But you really need to start by reading the guides and API docs so that you have a base understanding of how that works.
I've done some searching online, and understand how ActiveRecord validations can be built with "if" statements and separately defined methods. However, I'm wondering if it's possibly to simply combine two validations together, and if either is true, the whole thing passes.
What I'm trying to do is have a user input a contact field that can either be an email or a phone number, but not both. Obviously the code I have below isn't working, but I'm wondering if something similar to it could work?
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
VALID_PHONE = /\d{10}/
validates :contact, presence: true, format: { with: VALID_EMAIL_REGEX } || presence: true, length: { is: 10 }, format: { with: VALID_PHONE }
I was going to recommend using a standard :if option for the validates callback, but having done a little more research, I found something you may benefit from:
Custom Validations
According to the Rails guide:
#app/models/concerns/my_validator.rb
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
This allows you to create your own validation method, allowing you to append error messages directly into the instance variable (which is then shown on the form). For your question, I'd to do this:
#app/models/concerns/phone_email_validator.rb
class PhoneEmailValidator < ActiveModel::Validator
def validate(record)
contact = record.contact
phone = /\d{10}/
email = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
if contact.validate(phone) || contact.validate(email)
if contact.validate(phone) && contact.length < 10
error = "Length too short for phone number!"
end
else
error = 'Needs To Be Phone Or Email '
end
record.errors[:contact] << error
end
end
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
The function is too verbose, and might not work with the validation regex; but it's an idea either way!
In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects to abstract away multi-layer forms and stop using accepts_nested_attributes_for.
Edit: see below for a solution.
I've almost exactly duplicated his code sample, as I had the same problem to solve:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations doesn't have a uniqueness validator, as it's supposed to be a non-database backed variant of ActiveRecord.
I figured there are three ways to handle this:
Write my own method to check this (feels redundant)
Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
Or add the constraint in the data storage layer
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
I could do something like (metaprogramming, I would need to modify some other areas):
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
But now I have two checks running in my class, first I use valid? and then I use a rescue statement for the data storage constraints.
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
A simplified approach...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
#klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
You can use it in your ActiveModel classes like so:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model class has validations as well. Those validations aren't run when you call Signup.new.save, so you will have to check those some other way. You can always use save(validate: false) inside the above persist! method, but then you have to make sure all validations are in the Signup class, and keep that class up to date, when you change any validations in Account or User.
I currently have a model Attend that will have a status column, and this status column will only have a few values for it. STATUS_OPTIONS = {:yes, :no, :maybe}
1) I am not sure how i can validate this before a user inserts an Attend? Basically an enum in java but how could i do this in rails?
Now that Rails 4.1 includes enums you can do the following:
class Attend < ActiveRecord::Base
enum size: [:yes, :no, :maybe]
validates :size, inclusion: { in: sizes.keys }
end
Which then provides you with a scope (ie: Attend.yes, Attend.no, Attend.maybe), a checker method to see if certain status is set (ie: #yes?, #no?, #maybe?), along with attribute setter methods (ie: #yes!, #no!, #maybe!).
Rails Docs on enums
Create a globally accessible array of the options you want, then validate the value of your status column:
class Attend < ActiveRecord::Base
STATUS_OPTIONS = %w(yes no maybe)
validates :status, :inclusion => {:in => STATUS_OPTIONS}
end
You could then access the possible statuses via Attend::STATUS_OPTIONS
This is how I implement in my Rails 4 project.
class Attend < ActiveRecord::Base
enum size: [:yes, :no, :maybe]
validates :size, inclusion: { in: Attend.sizes.keys }
end
Attend.sizes gives you the mapping.
Attend.sizes # {"yes" => 0, "no" => 1, "maybe" => 2}
See more in Rails doc
You could use a string column for the status and then the :inclusion option for validates to make sure you only get what you're expecting:
class Attend < ActiveRecord::Base
validates :size, :inclusion => { :in => %w{yes no maybe} }
#...
end
What we have started doing is defining our enum items within an array and then using that array for specifying the enum, validations, and using the values within the application.
STATUS_OPTIONS = [:yes, :no, :maybe]
enum status_option: STATUS_OPTIONS
validates :status_option, inclusion: { in: STATUS_OPTIONS.map(&:to_s) }
This way you can also use STATUS_OPTIONS later, like for creating a drop down lists. If you want to expose your values to the user you can always map like this:
STATUS_OPTIONS.map {|s| s.to_s.titleize }
For enums in ActiveModels you can use this gem Enumerize
After some looking, I could not find a one-liner in model to help it happen. By now, Rails provides Enums, but not a comprehensive way to validate invalid values.
So, I opted for a composite solution: To add a validation in the controller, before setting the strong_params, and then by checking against the model.
So, in the model, I will create an attribute and a custom validation:
attend.rb
enum :status => { your set of values }
attr_accessor :invalid_status
validate :valid_status
#...
private
def valid_status
if self.invalid_status == true
errors.add(:status, "is not valid")
end
end
Also, I will do a check against the parameters for invalid input and send the result (if necessary) to the model, so an error will be added to the object, thus making it invalid
attends_controller.rb
private
def attend_params
#modify strong_params to include the additional check
if params[:attend][:status].in?(Attend.statuses.keys << nil) # to also allow nil input
# Leave this as it was before the check
params.require(:attend).permit(....)
else
params[:attend][:invalid_status] = true
# remove the 'status' attribute to avoid the exception and
# inject the attribute to the params to force invalid instance
params.require(:attend).permit(...., :invalid_status)
end
end
To define dynamic behavior you can use in: :method_name notation:
class Attend < ActiveRecord::Base
enum status: [:yes, :no, :maybe]
validates :status, inclusion: {in: :allowed_statuses}
private
# restricts status to be changed from :no to :yes
def allowed_statuses
min_status = Attend.statuses[status_was]
Attend.statuses.select { |_, v| v >= min_status }.keys
end
end
You can use rescue_from ::ArgumentError.
rescue_from ::ArgumentError do |_exception|
render json: { message: _exception.message }, status: :bad_request
end
Want to place another solution.
#lib/lib_enums.rb
module LibEnums
extend ActiveSupport::Concern
included do
validate do
self.class::ENUMS.each do |e|
if instance_variable_get("#not_valid_#{e}")
errors.add(e.to_sym, "must be #{self.class.send("#{e}s").keys.join(' or ')}")
end
end
end
self::ENUMS.each do |e|
self.define_method("#{e}=") do |value|
if !self.class.send("#{e}s").keys.include?(value)
instance_variable_set("#not_valid_#{e}", true)
else
super value
end
end
end
end
end
#app/models/account.rb
require 'lib_enums'
class Account < ApplicationRecord
ENUMS = %w(state kind meta_mode meta_margin_mode)
include LibEnums
end