Rails put validation in a module mixin?
module Validations
extend ActiveSupport::Concern
included do
validates :name, :length => { :minimum => 2 }, :presence => true, :uniqueness => true
validates :name_seo, :length => { :minimum => 2 }, :presence => true, :uniqueness => true
end
end
The validates
macro must be evaluated in the context of the includer, not of the module (like you probably were doing).
Rails accessing a shared custom validation in a module within a model
I tried the following and it worked for me:
Created validations.rb in models/concerns
module Validations
extend ActiveSupport::Concern
def email_format_validation
if self.email.present?
if !validates_format_of :email, with: email_regex
self.errors.add(:email, "doesn't exist")
end
end
end
def email_regex
/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
end
end
In the model:
include Validations
class User < ApplicationRecord
validate :email_format_validation
validates :user_email, with: :email_regex
end
user_email is the field name
Adding custom validations to ActiveRecord module via extend?
I hope this can help
6.1 Custom Validators
Custom validators are classes that extend ActiveModel::Validator. These classes must implement a validate method which takes a record as an argument and performs the validation on it. The custom validator is called using the validates_with method.
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
class Person
include ActiveModel::Validations
validates_with MyValidator
end
The easiest way to add custom validators for validating individual attributes is with the convenient ActiveModel::EachValidator. In this case, the custom validator class must implement a validate_each method which takes three arguments: record, attribute and value which correspond to the instance, the attribute to be validated and the value of the attribute in the passed instance.
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] || "is not an email")
end
end
end
class Person < ActiveRecord::Base
validates :email, :presence => true, :email => true
end
As shown in the example, you can also combine standard validations with your own custom validators.
https://guides.rubyonrails.org/active_record_validations.html#custom-validators
Removing or overriding an ActiveRecord validation added by a superclass or mixin
I ended up "solving" the problem with the following hack:
- look for an error on the
:email
attribute of type:taken
- check if the email is unique for this account (which is the validation I wanted to do)
- remove the error if the email is unique for this account.
Sounds reasonable until you read the code and discover how I remove an error. ActiveRecord::Errors
has no methods to remove errors once added, so I have to grab hold of it's internals and do it myself. Super duper mega ugly.
This is the code:
def validate
super
remove_spurious_email_taken_error!(errors)
end
def remove_spurious_email_taken_error!(errors)
errors.each_error do |attribute, error|
if error.attribute == :email && error.type == :taken && email_unique_for_account?
errors_hash = errors.instance_variable_get(:@errors)
if Array == errors_hash[attribute] && errors_hash[attribute].size > 1
errors_hash[attribute].delete_at(errors_hash[attribute].index(error))
else
errors_hash.delete(attribute)
end
end
end
end
def email_unique_for_account?
match = account.users.find_by_email(email)
match.nil? or match == self
end
If anyone knows of a better way, I would be very grateful.
Rails Apply Same Validation to Same Attribute in Different Models
There are a couple of ways you could do this, I prefer to store global constants is under the initializer folder.
For example:
Create an initializer file as /config/initializers/my_constants.rb
class MyConstants
MAX_LENGTH = 100
##.. other constants
end
After this use it in your models as:
validates :email, length: { maximum: MyConstants::MAX_LENGTH }
Please note every time you update this initializer file with new global constants, you must restart with rails s
to reload the changes.
Say you have two models User
with attributes username
and email
and Campaign
with attributes name
and email
- This approach would be more useful, when you need somewhat same validation to attributes with same/ different names across different models i.e., in the above case you can apply same validation for both
User#email
andCampaign#email
. Also, you have the freedom to use same validations forUser#username
andCampaign#name
even though they are named differently. - Even in cases when you need one option (say
minimum
inlength
validation) to have same value BUT another option(saymaximum
inlength
validation) to have different values.
Say in case ofUser#username
andCampaign#name
, you needminimum to be 2 characters
for both andmaximum
ofUser#username
to be30
andminimum
ofCampaign#name
to be50
then you can achieve that easily.
Like I said, you can achieve this in various ways. Another approach suggested by @CaptChrisD
is also good for the case where you need exactly same validation and you don't plan on tweaking it in future anyhow.
In that case you can make use of ActiveSupport::Concern
You would need to place the module in app/models/concerns
directory and name the file as email_validation.rb
.
module EmailValidation
extend ActiveSupport::Concern
included do
validates :email, length: { minimum: 5, maximum: 100 }
end
end
After this you would need to include the module in the models where you need the same validation on email
field.
class MyModel < ActiveRecord::Base
include EmailValidation ## Add this
##..
end
This is an excellent read on Ruby Mixins & ActiveSupport::Concern for your reference.
You can also check out this SO Answer: Rails put validation in a module mixin?
Rails 4: Validating associated ActiveRecord models when using a Form Object
I like what you're doing using a form object. validates_associated :user, :account
might help, but the error messages might be kind of odd. Rather, I might use mixins for common validations:
class Account < ActiveRecord::Base
module Validations
extend ActiveSupport::Concern
included do
validates :slug, presence: true
end
end
include Validations
end
class Signup
include ActiveModel::Model
include Account::Validations
extend Forwardable
def_delegators :account, :slug
end
How to mix in additional validations to ActiveModel::Model?
Okay.
One more snippet.
Im not sure, but how about this?
# This should be locate in lib/your_custom_validator.rb
class YourCustomValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# validation login is here
end
end
I think, now you can use that validator in any model.
# You can use that validator like this.
class RegistrationForm < ActiveRecord::Base
validates :some_column_name, your_custom_validator: true
end
cheers, Sangil.
Apply validation module to model in certain controllers only
One alternative is to include the following in your Model:
attr_accessor :guest
def run_special_validations?
guest
end
validate :special_validation1, if: run_special_validations?
validate :special_validation2, if: run_special_validations?
Then, by having the controller set @object.guest = true
, you will tell the object to run the conditional validations.
Share validations across models with rails 4
Create the module in app/models/concerns
, then include them in your classes with:
include ContactValidations
In this way Rails will automatically load the shared modules and make them available for you to include.
How would I implement my own Rails-style validates() method in Ruby?
So here are two alternative ways of doing it:
With "direct" access
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
def wordy?(value)
value.length > 2
end
module ClassMethods
def validates_wordiness_of(*attrs)
define_method(:valid?) do
attrs.all? do |attr|
wordy?(send(attr))
end
end
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"a"
end
def bar
"asrtioenarst"
end
end
puts MyClass.new.valid?
The downside to this approach is that several consecutive calls to validates_wordiness_of
will overwrite each other.
So you can't do this:
validates_wordiness_of :foo
validates_wordiness_of :bar
Saving validated attribute names in the class
You could also do this:
require 'set'
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
module Validation
def valid?
self.class.wordy_attributes.all? do |attr|
wordy?(self.send(attr))
end
end
def wordy?(value)
value.length > 2
end
end
module ClassMethods
def wordy_attributes
@wordy_attributes ||= Set.new
end
def validates_wordiness_of(*attrs)
include(Validation) unless validation_included?
wordy_attributes.merge(attrs)
end
def validation_included?
ancestors.include?(Validation)
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"aastrarst"
end
def bar
"asrtioenarst"
end
end
MyClass.new.valid?
# => true
I chose to make the valid?
method unavailable until you actually add a validation. This may be unwise. You could probably just have it return true if there are no validations.
This solution will quickly become unwieldy if you introduce other kinds of validations. In that case I would start wrapping validations in validator objects.
Related Topics
Rails - Pass Id Parameter on a Link_To
How to Use a Variable as Object Attribute in Rails
How to Customize the Table Builder Plugin for a Week Calendar
Escape Single Quote in Xpath with Nokogiri
When Did "Assigned But Unused" Become a Warning for Ruby
Reflection on Method Parameters in Ruby
How to Get Ruby to Print a Full Backtrace Instead of a Truncated One
How to Integrate Paypal with Ruby on Rails
How to Use Gems Not in a Gemfile When Working with Bundler
Generate an HTML Table from an Array of Hashes in Ruby
Trying to Install Ruby-Filemagic on Snow Leopard Using Brew Rather Than Ports
Finding If a Sentence Contains a Specific Phrase in Ruby
Jekyll Serve Dependency Error - Could Not Open 'Lib Curl'
Convert (Decode) Hexadecimal String to Binary String
Ruby on Rails Send_File Doesn't Work Until I Refresh the Page