How to make Rails' format with validator dynamic? - ruby-on-rails

I am trying to validate the format of exchange_rate in my Invoice class:
class Invoice < ActiveRecord::Base
attr_accessible :currency, :exchange_rate
validates :exchange_rate, :format => { :with => exchange_rate_format }
private
def exchange_rate_format
if currency != user.preference.base_currency
DECIMAL_REGEX
else
ANOTHER_REGEX
end
end
end
The problem is: It doesn't work at all. I guess I need to use a Proc here? I never really figured out how to use it though. Maybe somebody can help.
Thanks a lot.

Yes, you need to use a Proc or lambda so that the validation will be called at runtime.
validates :exchange_rate, format: { with: ->(invoice) { invoice.exchange_rate_format } }
# Note, I used Ruby 1.9 hash and lambda syntax here.
To do this you'll need to move the exchange_rate_format out of the private methods list since we're defining an explicit receiver (invoice). You can make it protected instead, if you'd like. Or you can put the conditional into the lambda.

One way to do it is with a custom validator:
class Invoice < ActiveRecord::Base
class ExchangeRateFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if !value =~ record.exchange_rate_format
record.errors[attribute] << "your currency is weak sauce"
end
end
end
validates :exchange_rate, exchange_rate_format: true
# make public
def exchange_rate_format
if currency != user.preference.base_currency
DECIMAL_REGEX
else
ANOTHER_REGEX
end
end
end

Related

Custom validator with argument

I have the following class:
class PatientPaymentSpreadsheetRow < ApplicationSpreadsheetRow
include ActiveModel::Validations
validate :date_format
def date_format
unless value('Transaction').split('/').last.length == 4
errors.add('Transaction', 'date format invalid')
end
end
end
This particular validation happens to act on value('Transaction'). I'd like my validator to be sufficiently generic that I can pass in any value (e.g. value('Date of birth')) and have it act on that value.
How can I accomplish this?
This is an old question, but I think this is more in the ballpark of what you were asking.
ActiveModel::EachValidator allows you to pass in options as a hash, which will be available in the class instance with options.
You can set up the validation in your model like
class SomeModel < ApplicationRecord
validates :transaction, date_format: { value: 'Transaction' }
end
And the custom validator would look like:
class DateFormatValidator < ActiveModel::EachValidator
def validate_each(model, attribute, value)
return if options[:value].split('/').last.length == 4
# ^^^^^^^ hash you pass is here
model.errors.add(attribute, 'date format invalid')
end
end
You can write custom 'each validator', like described in Guides:
class DateFormatValidator < ActiveModel::EachValidator
def validate_each(model, attribute, value)
return if value.split('/').last.length == 4
model.errors.add(attribute, 'date format invalid')
end
end
And use it like this:
class MyCustomModel
include ActiveModel::Validations
attr_accessor :my_date_attr
validates :my_date_attr, date_format: true
end
But presumably you want the validations to run unconditionally, so...
validate :date_format
DATES_TO_VALIDATE = ['Transaction', 'Date of birth', 'Other date']
def date_format
DATES_TO_VALIDATE.each do |key|
unless value(key).split('/').last.length == 4
errors.add(key, 'date format invalid')
end
end
end
This can be extracted to an each_validator as per Marek Lipka's answer, with a custom constant DATES_TO_VALIDATE for each model and access it in the validator as model.class::DATES_TO_VALIDATE

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 serialized array validation

I need to validate email saving in email_list. For validation I have EmailValidator. But I cannot figure out how to use it in pair of validates_each. Are there any other ways to make validations?
class A < ActiveRecord::Base
serialize :email_list
validates_each :email_list do |r, a, v|
# what should it be here?
# EmailValidator.new(options).validate_each r, a, v
end
end
validates_each is for validating multiple attributes. In your case you have one attribute, and you need to validate it in a custom way.
Do it like this:
class A < ActiveRecord::Base
validate :all_emails_are_valid
...
private
def all_emails_are_valid
unless self.email_list.nil?
self.email_list.each do |email|
if # email is valid -- however you want to do that
errors.add(:email_list, "#{email} is not valid")
end
end
end
end
end
Note that you could also make a custom validator for this or put the validation in a proc on the validate call. See here.
Here's an example with a custom validator.
class A < ActiveRecord::Base
class ArrayOfEmailsValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
return if value.nil?
value.each do |email|
if # email is valid -- however you want to do that
record.errors.add(attribute, "#{email} is not valid")
end
end
end
end
validates :email_list, :array_of_emails => true
...
end
Of course you can put the ArrayOfEmailsValidator class in, i.e., lib/array_of_emails_validator.rb and load it where you need it. This way you can share the validator across models or even projects.
I ended up with this:
https://gist.github.com/amenzhinsky/c961f889a78f4557ae0b
You can write your own EmailValidator according to rails guide and use the ArrayValidator like:
validates :emails, array: { email: true }

How do i specify and validate an enum in rails?

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

How can I make Rails ActiveRecord automatically truncate values set to attributes with maximum length?

Assuming that I have a class such as the following:
class Book < ActiveRecord::Base
validates :title, :length => {:maximum => 10}
end
Is there a way (gem to install?) that I can have ActiveRecord automatically truncate values according to maximum length?
For instance, when I write:
b = Book.new
b.title = "123456789012345" # this is longer than maximum length of title 10
b.save
should save and return true?
If there is not such a way, how would you suggest that I proceed facing such a problem more generally?
Well, if you want the value truncated if its too long, you don't really need a validation, because it will always pass. I'd handle that like this:
class Book < ActiveRecord::Base
before_save :truncate_values
def truncate_values
self.title = self.title[0..9] if self.title.length > 10
end
end
I have come up with a new validator that does truncation. Here is how I did that:
I created the "validators" folder inside "app" folder and then created the file "length_truncate_validator.rb" with the following content:
class LengthTruncateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ml = options[:maximum]
record.send("#{attribute}=", value.mb_chars.slice(0,ml)) if value.mb_chars.length > ml unless value.nil? or ml.nil?
end
class << self
def maximum(record_class, attribute)
ltv = record_class.validators_on(attribute).detect { |v| v.is_a?(LengthTruncateValidator) }
ltv.options[:maximum] unless ltv.nil?
end
end
end
And inside my model class I have something like:
class Book < ActiveRecord::Base
validates :title, :length_truncate => {:maximum => 10}
end
which is quite handy and works the way I require.
But still, if you think that this one can be improved or done in another way, you are welcome.
This may not have been an option in 2011, but now there's a before_validation callback that will work.
class Book < ApplicationRecord
before_validation do
if self.params && self.params.length > 1000
self.params = self.title[0...10]
end
end
validate :title, length: { maximum: 10 }, allow_nil: true
end
I like the idea of using the before_validation callback. Here's my stab that automatically truncates all strings to within the database's limit
before_validation :truncate_strings
def truncate_strings
self.class.columns.each do |column|
next if column.type != :string
if self[column.name].length > column.limit
self[column.name] = self[column.name][0...column.limit]
end
end
end

Resources