How do I remove validation duplication from a model? - ruby-on-rails

Short of extracting shipping and billing addresses into an Address model, how can I remove this validation duplication?
I only want to validate the billing address if it's not the same as the shipping address. How would I go about extracting it into a module? An example would be really helpful as I never know what to include in modules, or self refers to.
validates :shipping_name, :shipping_address1, :shipping_street_number, :shipping_city, presence: true
validates :shipping_state, inclusion: { in: Address.states.values }
validates :shipping_post_code, length: { is: 5 }, numericality: { only_integer: true }
validates :billing_name, :billing_address1, :billing_street_number, :billing_city, presence: true, unless: -> { self.bill_to_shipping_address? }
validates :billing_state, inclusion: { in: Address.states.values }, unless: -> { self.bill_to_shipping_address? }
validates :billing_post_code, length: { is: 5 }, numericality: { only_integer: true }, unless: -> { self.bill_to_shipping_address? }

You can make a method and then pass in the bits that are different between the two types of addresses. In this case, the difference is the prefix word for the fields and the ability to pass in extra options.
module AddressValidator
def validates_address(type, options = {})
validates :"#{type}_name", :"#{type}_address1", :"#{type}_street_number", :"#{type}_city", {presence: true}.merge(options)
validates :"#{type}_state", {inclusion: { in: Address.states.values }}.merge(options)
validates :"#{type}_post_code", {length: { is: 5 }, numericality: { only_integer: true }}.merge(options)
end
end
class MyModel < ActiveRecord::Base
extend AddressValidator
validates_address(:shipping)
validates_address(:billing, unless: -> { self.bill_to_shipping_address? })
end

Related

Fasterer yields "Calling argumentless methods within blocks is slower than using symbol to proc" within my validations. How to solve it?

I am coding my own ERP. For the People (model class) I have the following validations:
class People < ApplicationRecord
# some code for N:M relations
# validations
validates :aka, presence: { if: proc { |person| person.aka? } },
uniqueness: true,
length: { within: 3..25,
if: proc { |person| person.aka? } }
validates :last_name, presence: { if: proc { |person| person.last_name? } },
uniqueness: { scope: %i[last_name first_name] },
length: { within: 2..100,
if: proc { |person| person.last_name? } }
validates :phone_ext, presence: { if: proc { |person| person.phone_ext? } },
length: { within: 1..10,
if: proc { |person| person.phone_ext? } },
format: { with: /\A\d{1,10}\Z/i,
if: proc { |person| person.phone_ext? } }
validates :first_name, presence: true,
uniqueness: { scope: %i[last_name first_name] },
length: { within: 2..100 }
end
As you can see in all the if: proc { .... lines, they are almost kind of the same stuff. And fasterer knows about it, that's why I am getting the Calling argumentless methods within blocks is slower than using symbol to proc message.
Now, unsuccessfully all day I have trying all day long to figure a way out to solve this Fasterer's message. I have tried lambdas, closures, &:, ->, so I give up.
Any ideas?
This is referring to, for example, a.map(&:foo) being faster than a.map { |o| o.foo }.
In this context, validates will take a method name to check as a symbol. For example, if: :aka? instead of if: proc { |person| person.aka? }

undefined method `user' for #<Class:0x00007ffbd1c309b8>

I don't understand why I can't use self here?
class PayoutRequest < ApplicationRecord
validates :phone, confirmation: true, on: :create
validates :phone_confirmation, presence: true, on: :create
belongs_to :user, foreign_key: "user_id"
validates :amount, numericality: { only_integer: true, greater_than_or_equal_to: 300, smaller_than_or_equal: self.user.balance }
scope :paid, -> { where(:paid => true) }
scope :unpaid, -> { where(:paid => false) }
end
How can I write this?
Use a custom method, for example:
validate :amount_not_greater_than_balance
def amount_not_greater_than_balance
return if amount <= user.balance
errors.add(:amount, "can't be greater than balance")
end
In addition, you should probably only run this specific validation rule on: :create -- because it would presumably be totally acceptable for a payment request to become more than the user's balance, later on n the future.
Because self is not what you think it is. In case you didn't know or forgot, validation DSL is just methods called on the class itself. Here you basically call PayoutRequest.validates and pass it some parameters.
validates :amount, numericality: { only_integer: true, greater_than_or_equal_to: 300, smaller_than_or_equal: self.user.balance }
^ ^ arg ^ kw arg ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
method name just a regular hash, defined at the class level. So `self` is the class.

Validate only numbers

In one of my rails models I have this :only_integer validation:
validates :number, presence: true, numericality: { only_integer: true }
This validation also allows inputs like +82938434 with +-signs.
Which validation should I use to only allow inputs without + - only numbers?
The documentation for only_integer mentions this regex :
/\A[+-]?\d+\z/
It means you could just use:
validates :number, format: { with: /\A\d+\z/, message: "Integer only. No sign allowed." }
Rails 7 added :only_numeric option to numericality validator
validates :age, numericality: { only_numeric: true }
User.create(age: "30") # failure
User.create(age: 30) # success

getting undefined method when trying to call method created in model

I am writing a hacker news clone in rails to learn the framework and encountered a problem calling helper methods within a model:
class User < ActiveRecord::Base
has_secure_password validations: false
has_many :posts
validates :name,
presence: { message: username_error_message },
uniqueness: { case_sensitive: false, message: username_error_message },
length: { minimum: 2, maximum: 15, message: username_error_message }
validates :password,
presence: { message: password_error_message },
length: { minimum: 4, message: password_error_message }
private
def username_error_message
"Usernames can only contain letters, digits, dashes and underscores, and should be between 2 and 15 characters long. Please choose another."
end
def password_error_message
"Passwords should be a least 4 characters long. Please choose another."
end
end
I get the following error (Rails 4):
undefined local variable or method `username_error_message' for #<Class:XXX>
You can use constants for the repeated error messages and it will work.
class User < ActiveRecord::Base
has_secure_password validations: false
has_many :posts
USERNAME_ERROR_MESSAGE = "Usernames can only contain letters, digits, dashes and underscores, and should be between 2 and 15 characters long. Please choose another."
PASSWORD_ERROR_MESSAGE = "Passwords should be a least 4 characters long. Please choose another."
validates :name,
presence: { message: USERNAME_ERROR_MESSAGE },
uniqueness: { case_sensitive: false, message: USERNAME_ERROR_MESSAGE },
length: { minimum: 2, maximum: 15, message: USERNAME_ERROR_MESSAGE }
validates :password,
presence: { message: PASSWORD_ERROR_MESSAGE },
length: { minimum: 4, message: PASSWORD_ERROR_MESSAGE }
end
Also, there is a little problem: the error message will be repeated if more than one validation condition fails for every field. One solution is to write a custom validation method, as can be seen here: http://guides.rubyonrails.org/active_record_validations.html#custom-methods
Try using class methods instead:
validates :name,
presence: { message: Proc.new { username_error_message } },
uniqueness: { case_sensitive: false, message: Proc.new { username_error_message } },
length: { minimum: 2, maximum: 15, message: Proc.new { username_error_message } }
validates :password,
presence: { message: Proc.new { password_error_message } },
length: { minimum: 4, message: Proc.new { password_error_message } }
private
def self.username_error_message
"Usernames can only contain letters, digits, dashes and underscores, and should be between 2 and 15 characters long. Please choose another."
end
def self.password_error_message
"Passwords should be a least 4 characters long. Please choose another."
end
The fundamental problem is that you're accessing your methods before they are defined. This problem has nothing per se to do with whether they are private, constants, instance methods or class methods.
The methods are being referenced as your User class is being defined. The references exist within a hash constructor {} being passed as a parameter to the method call validates, which means it is getting evaluated at the time validates is called. It's not like you're passing a block to validates that gets evaluated at a later time.
If you move your definitions to before you reference them, it will address this fundamental problem, but it is also true that you are defining them as instance methods and referencing to them as class methods. You need to bring your mode of definition in line with your mode of access (e.g. by defining the methods as class methods).
(Note: There are a myriad of other options available to you for defining and referencing these strings beyond those noted in the other answers, including class variables, class local variables, etc. They vary primarily in terms where and how they can be accessed. You can even use instance variables and methods, although that would be odd in this case, requiring you to instantiate an object in order to reference them. The point is that Ruby is rich with possibilities and it's well worth investigating/understanding them when you have the time.)
Try this
class User < ActiveRecord::Base
has_secure_password validations: false
has_many :posts
validates :name,
presence: { message: self.username_error_message },
uniqueness: { case_sensitive: false, message: self.username_error_message },
length: { minimum: 2, maximum: 15, message: self.username_error_message }
validates :password,
presence: { message: self.password_error_message },
length: { minimum: 4, message: self.password_error_message }
def username_error_message
"Usernames can only contain letters, digits, dashes and underscores, and should be between 2 and 15 characters long. Please choose another."
end
def password_error_message
"Passwords should be a least 4 characters long. Please choose another."
end
end
private :username_error_message, :password_error_message
I just added some "self." before the methods call to give it a context of this instance.
I also changed the private declaration method

DRYing up conditional validation in a Ruby class

How can I dry up my validation code? I have a Discussion model that has a a category and status fields. The status value depends on the category value. A discussion where category == 'question' can only have a status in STATUSES[:question], for example.
STATUSES = {
question: %w[answered],
suggestion: %w[pending planned started completed declined],
problem: %w[started solved]
}
validates :status, allow_blank: true, inclusion: { in: STATUSES[:question] }, if: lambda { self.category == 'question' }
validates :status, allow_blank: true, inclusion: { in: STATUSES[:suggestion] }, if: lambda { self.category == 'suggestion' }
validates :status, allow_blank: true, inclusion: { in: STATUSES[:problem] }, if: lambda { self.category == 'problem' }
I'm using Rails 3.
:inclusion :in accepts a lambda itself:
validates :status, inclusion: { in: lambda { |o| STATUSES[o.category.to_sym] } }
Documentation: http://apidock.com/rails/ActiveModel/Validations/HelperMethods/validates_inclusion_of

Resources