I was trying to create a custom validator class in Ruby on Rails that can be expanded. However, I cannot get it to use validation from both the sub-class and super-class.
This example will clarify what I am trying to achieve:
Super-class
class NameValidator < ActiveModel::EachValidator
def validate_each (record, attribute, value)
#Checks to see if the incoming string is free of any numerical characters
if value.match(/\A[+-]?\d+\Z/)
record.errors[attribute] << "String must contain no numerical characters"
end
end
end
sub-class
class SevenNameValidator < NameValidator
def validate_each (record, attribute, value)
# Checks and only allows 7 character strings to be validated
if value.length != 7
record.errors[attribute] << "String must be 7 characters exactly"
end
end
end
Model class
class User < ActiveRecord::Base
attr_accessible :firstname
validates :firstname, :seven_name => true
end
so if the string "hello" is tested the resulting error => "Strings must be 7 characters exactly"
However if the string "hello77" is tested, it is validated successfully.
Shouldn't it check from NameValidator first and see that it has digits? If not, how can I get inheritance to work in custom validators? Do I need to use methods within my validator classes? An example would be appreciated, I have searched a lot, but I cannot find an example for custom validators.
Call super in sub-class:
class SevenNameValidator < NameValidator
def validate_each (record, attribute, value)
# Checks and only allows 7 character strings to be validated
if value.length != 7
record.errors[attribute] << "String must be 7 characters exactly"
else
#call super to trigger super class method
super
end
end
end
I think it might an issue with your regular expression. If you're trying to match any string with digits you should have something like /\A\D*\d+\D*\z/ right now you're matching a ton of stuff that I don't think you want.
Related
I have created a custom validator which has it's own specific unit tests to check that it works.
Using should-matchers there was a suggestion to add a validates_with matcher, so you could write:
subject.validates_with(:custom_validator)
Quite rightly the suggestion was declined, since it does not really test the behaviour of the model.
But my model has 4 fields that use the custom validator, and I want that behaviour to be tested - ie that those 4 fields are being validated, just as I am testing that they are being validated for presence:
describe '#attribute_name' do
it { is_expected.to validate_presence_of(:attribute_name) }
end
So how can I write a test that basically does the same thing, something sort of like this:
describe '#attribute_name' do
it { is_expected.to use_custom_validator_on(:attribute_name) }
end
This question asks the same thing and the answer suggests building a test model. However, my validator requires an option, it is used like this:
\app\models\fund.rb
class Fund < ActiveRecord
validates :ein, digits: { exactly: 9 }
end
So if I build a test model, and test it as suggested:
it 'is has correct number of digits' do
expect(build(:fund, ein: '123456789')).to be_valid
end
it 'is has incorrect number of digits' do
expect(build(:fund, ein: '123').to be_invalid
end
I receive RecordInvalid error (from my own validator! lol) saying I did not supply the required option for the validator. That option is called 'exactly'.
1) Fund#ein validates digits
Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
ActiveRecord::RecordInvalid:
Record invalid
So is Rspec not 'seeing' the value '9' defined in the model file?
Obviously it makes no sense to define that in the test as that is the defined behaviour I am trying to test for. Think of it like the validates_length_of testing for the { length: x } option.
Surely there must be a way to test that this custom validator option is set on the model?
The validator code
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] << (options[:message] || error_msg(length))
end
private
def error_msg(length)
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
end
Interesting side note
Obviously if I remove the 'raise' line from the DigitsValidator then both the tests succeed. Is there something wrong with my code that I cannot see?
I think you would have to add a return statement, no? :-)
def error_msg(length)
return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
Alternatively, remove that method and use a guard after setting length:
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
raise ActiveRecord::RecordInvalid if length.nil?
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] <<
(options[:message] ||
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
end
end
I think you should not aim for testing whether the model is using a specific validator. Rather check if the model is valid/invalid in specific cases. In other words, you should be able to test the behaviour of the model without knowing the implementation.
So in this case, you should setup you model correctly with you 'exactly' option for the validator and test if the model validation is sufficient overall.
On the other hand, if you are worried about that someone will misuse the validator in the future and 'exactly' is a required option for the validator, then you should raise error every time when the option is not present and test the validator in isolation like explained here: How to test a custom validator?
I like the idea of not including tests on the model that assume knowledge of exactly what the custom validator is validating. (Otherwise, we'll be repeating logic in the tests for the custom validators, and the tests for the model.)
I solved this by using Mocha (mocking library for Ruby) to set up expectations that the validate_each method of each my custom validators were being called on the correct corresponding field of my model. Simplified example:
Model class:
class User
include ActiveModel::Model
attr_accessor :first_name, :last_name
validates :first_name, first_name: true
validates :last_name, last_name: true
end
Custom validator classes:
class FirstNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
class LastNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
Model test class:
class UserTest < ActiveSupport::TestCase
def test_custom_validators_called_on_the_appropriate_fields
user = User.new(first_name: 'Valued', last_name: 'Customer')
FirstNameValidator.any_instance.expects(:validate_each).with(user, :first_name, 'Valued')
LastNameValidator.any_instance.expects(:validate_each).with(user, :last_name, 'Customer')
assert_predicate user, :valid?
end
end
Does rails do any validation for datetime? I found a plugin
http://github.com/adzap/validates_timeliness/tree/master,
but it seems like something that should come in out of the box.
There's no built-in ActiveRecord validator for DateTimes, but you can easily add this sort of capability to an ActiveRecord model, without using a plugin, with something like this:
class Thing < ActiveRecord::Base
validate :happened_at_is_valid_datetime
def happened_at_is_valid_datetime
errors.add(:happened_at, 'must be a valid datetime') if ((DateTime.parse(happened_at) rescue ArgumentError) == ArgumentError)
end
end
Gabe's answer didn't work for me, so here's what I did to validate my dates:
class MyModel < ActiveRecord::Base
validate :mydate_is_date?
private
def mydate_is_date?
if !mydate.is_a?(Date)
errors.add(:mydate, 'must be a valid date')
end
end
end
I was just looking to validate that the date is in fact a date, and not a string, character, int, float, etc...
More complex date validation can be found here: https://github.com/codegram/date_validator
Recent versions of Rails will type cast values before validation, so invalid values will be passed as nils to custom validators. I'm doing something like this:
# app/validators/date_time_validator.rb
class DateTimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.public_send("#{attribute}_before_type_cast").present? && value.blank?
record.errors.add(attribute, :invalid)
end
end
end
# app/models/something.rb
class Something < ActiveRecord::Base
validates :sold_at, date_time: true
end
# spec/models/something_spec.rb (using factory_girl and RSpec)
describe Something do
subject { build(:something) }
it 'should validate that :sold_at is datetimey' do
is_expected.not_to allow_value(0, '0', 'lorem').for(:sold_at).with_message(:invalid)
is_expected.to allow_value(Time.current.iso8601).for(:sold_at)
end
end
You can create a custom datetime validator by yourself
1) create a folder called validators in inside app directory
2) create a file datetime_validator.rb. with the following content inside app/validators directory
class DatetimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if ((DateTime.parse(value) rescue ArgumentError) == ArgumentError)
record.errors[attribute] << (options[:message] || "must be a valid datetime")
end
end
end
3) Apply this validation on model
class YourModel < ActiveRecord::Base
validates :happend_at, datetime: true
end
4) Add the below line in application.rb
config.autoload_paths += %W["#{config.root}/app/validators/"]
5) Restart your rails application
Note: The above method tested in rails 4
I recommend a gem date_validator. See https://rubygems.org/gems/date_validator. It is well maintained and its API is simple and compact.
It's quite necessary to validate dates. With the default Rails form helpers you could select dates like September 31st.
I need to validate credit card number.
model Billing
validates :card, credit_card_number: true, allow_nil: true
Validation gem code:
def validate_each(record, attribute, value)
record.errors.add(attribute, options[:message] || :invalid) unless credit_card_valid?(value, extract_brands(record, options))
end
It works ok.
But then I try to override geter this way:
def card
"****#{self[:card][-4,4]}" if self[:card]
end
Validation fails.
when I monkey patched validates_each like that:
def validate_each(record, attribute, value)
value = record[attribute]
record.errors.add(attribute, options[:message] || :invalid) unless credit_card_valid?(value, extract_brands(record, options))
end
It back to work well.
Is it correct validation behavior, to validates getters instead of persisted values (validates_each first variant is follows guide).
Or what is preffered way to solve my problem?
Update: Activemodel/Activerecord version: 4.2.3
Firstly, the method you defined is not a model concern - it is a view concern and as such should be moved to a helper or presenter. This is however very common practice to have such methods within the model, so I wouldn't say that's a huge problem.
You can easily get around the whole problem, by picking the other name for your method:
def starred_card
card && "****#{card[-4,4]}"
end
When you override the getter method (as you have done here), you have to write a custom validation method.
For example, here is a simple validation that allows nil and ensures the card number is a string of 16 digits.
validate :correct_credit_card_number
def correct_credit_card_number
if self[:card] && self[:card] !~ /^\d{16}$/
errors.add(:card, "is not in the right format")
end
end
Does rails do any validation for datetime? I found a plugin
http://github.com/adzap/validates_timeliness/tree/master,
but it seems like something that should come in out of the box.
There's no built-in ActiveRecord validator for DateTimes, but you can easily add this sort of capability to an ActiveRecord model, without using a plugin, with something like this:
class Thing < ActiveRecord::Base
validate :happened_at_is_valid_datetime
def happened_at_is_valid_datetime
errors.add(:happened_at, 'must be a valid datetime') if ((DateTime.parse(happened_at) rescue ArgumentError) == ArgumentError)
end
end
Gabe's answer didn't work for me, so here's what I did to validate my dates:
class MyModel < ActiveRecord::Base
validate :mydate_is_date?
private
def mydate_is_date?
if !mydate.is_a?(Date)
errors.add(:mydate, 'must be a valid date')
end
end
end
I was just looking to validate that the date is in fact a date, and not a string, character, int, float, etc...
More complex date validation can be found here: https://github.com/codegram/date_validator
Recent versions of Rails will type cast values before validation, so invalid values will be passed as nils to custom validators. I'm doing something like this:
# app/validators/date_time_validator.rb
class DateTimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.public_send("#{attribute}_before_type_cast").present? && value.blank?
record.errors.add(attribute, :invalid)
end
end
end
# app/models/something.rb
class Something < ActiveRecord::Base
validates :sold_at, date_time: true
end
# spec/models/something_spec.rb (using factory_girl and RSpec)
describe Something do
subject { build(:something) }
it 'should validate that :sold_at is datetimey' do
is_expected.not_to allow_value(0, '0', 'lorem').for(:sold_at).with_message(:invalid)
is_expected.to allow_value(Time.current.iso8601).for(:sold_at)
end
end
You can create a custom datetime validator by yourself
1) create a folder called validators in inside app directory
2) create a file datetime_validator.rb. with the following content inside app/validators directory
class DatetimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if ((DateTime.parse(value) rescue ArgumentError) == ArgumentError)
record.errors[attribute] << (options[:message] || "must be a valid datetime")
end
end
end
3) Apply this validation on model
class YourModel < ActiveRecord::Base
validates :happend_at, datetime: true
end
4) Add the below line in application.rb
config.autoload_paths += %W["#{config.root}/app/validators/"]
5) Restart your rails application
Note: The above method tested in rails 4
I recommend a gem date_validator. See https://rubygems.org/gems/date_validator. It is well maintained and its API is simple and compact.
It's quite necessary to validate dates. With the default Rails form helpers you could select dates like September 31st.
For example, if I have a user model and I need to validate login only (which can happen when validating a form via ajax), it would be great if I use the same model validations defined in the User model without actually instantiating a User instance.
So in the controller I'd be able to write code like
User.valid_attribute?(:login, "login value")
Is there anyway I can do this?
Since validations operate on instances (and they use the errors attribute of an instance as a container for error messages), you can't use them without having the object instantiated. Having said that, you can hide this needed behaviour into a class method:
class User < ActiveRecord::Base
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
unless mock.valid?
return mock.errors.has_key?(attr)
end
true
end
end
Now, you can call
User.valid_attribute?(:login, "login value")
just as you intended.
(Ideally, you'd include that class method directly into the ActiveRecord::Base so it would be available to every model.)
Thank you Milan for your suggestion. Inspired by it I created a simple module one can use to add this functionality to any class. Note that the original Milans suggestion has a logic error as line:
return mock.errors.has_key?(attr)
should clearly be:
return (not mock.errors.has_key?(attr))
I've tested my solution and it should work, but ofc I give no guarantees. And here's my glorious solution. Basically a 2-liner if you take away the module stuff.. It accepts method names as stings or symbols.
module SingleAttributeValidation
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def valid_attribute?(attr, value)
mock = self.new(attr => value)
(not mock.valid?) && (not mock.errors.has_key?(attr.class == Symbol ? attr : attr.to_sym))
end
end
end
To use your standard validation routines:
User.new(:login => 'login_value').valid?
If that does not work for you, build a custom class method for this:
class User < ActiveRecord::Base
validate do |user|
user.errors.add('existing') unless User.valid_login?(user.login)
end
def self.valid_login?(login)
# your validation here
!User.exist?(:login=> login)
end
end
I had a hell of a time getting this to work in Rails 3.1. This finally worked. (Not sure if it's the best way to do it, I'm kind of a newb.). The problem I was having was that value was being set to type ActiveSupport::SafeBuffer, and was failing validation.
def self.valid_attribute?(attr, value)
mock = User.new(attr => "#{value}") # Rails3 SafeBuffer messes up validation
unless mock.valid?
return (not mock.errors.messages.has_key?(attr))
end
return true
end
I have gone with the custom class solution but I just wanted to make sure there was no better way
class ModelValidator
def self.validate_atrribute(klass, attribute, value)
obj = Klass.new
obj.send("#{attribute}=", value)
obj.valid?
errors = obj.errors.on(attribute).to_a
return (errors.length > 0), errors
end
end
and I can use it like
valid, errors = ModelValidator.validate_attribute(User, "login", "humanzz")
class User < ActiveRecord::Base
validates_each :login do |record, attr, value|
record.errors.add attr, 'error message here' unless User.valid_login?(value)
end
def self.valid_login?(login)
# do validation
end
end
Just call User.valid_login?(login) to see if login itself is valid
An implementation of the 'valid_attribute' method you are suggesting:
class ActiveRecord:Base
def self.valid_attribute?(attribute, value)
instance = new
instance[attribute] = value
instance.valid?
list_of_errors = instance.errors.instance_variable_get('#errors')[attribute]
list_of_errors && list_of_errors.size == 0
end
end
How about:
User.columns_hash.has_key?('login')