I have created a custom validation for a ruby model (using the luhn algorithm). Even when I explicitly just return false from the custom validation, the object still saves.
This is what I have in my CreditCard model:
before_save :check_card_number
private
def check_card_number
return false unless card_number_luhn
end
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
but even if I just return false:
before_save :check_card_number
private
def check_card_number
return false
end
#so this is never even called
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
the object still saves. This is true EVEN IF I use validate instead of before_save. What is going on?
From Rails 5, you need to explicitly call throw(:abort)
# ...
before_save :check_card_number
# ...
def card_number_luhn
valid = #luhn_algorithm_here_that_returns_true_or_false
throw(:abort) unless valid
end
another way:
ActiveSupport.halt_callback_chains_on_return_false = true
reference: https://www.bigbinary.com/blog/rails-5-does-not-halt-callback-chain-when-false-is-returned
Related
I'm working on a refactor for some ruby on rails v6 application. I have made a custom validator to test if the string being passed could be parsed into a Date object, like so:
class DateFormatValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any? do |field|
if record.send(field).nil?
return true
end
unless valid_date?(record.send(field))
record.errors.add(field, :invalid)
return false
end
end
end
end
def valid_date?(date_string)
Date.parse(date_string)
true
rescue ArgumentError
false
end
end
It is being called, and it adds the error to the record with the correct key for the field i'm passing. It is being called like so from inside my model:
validates_with DateFormatValidator, fields: [:date_of_birth]
But, when I ran a test to failed that parser like this:
def test_it_raises_ArgumentError_due_to_invalid_date
customer = customers(:one)
assert_raises(ArgumentError) do
customer.update({date_of_birth: "18/0-8/1989"})
end
end
Now, the assert_raises is my problem. I would expect to be able to do customer.valid? and since an error has been added to the customer it would return false. But instead, it returns:
ArgumentError: invalid date
Any ideas of what am I doing wrong or how to make the valid? turn false when i add an error into the errors obj holding my record??
I'm calling #foo.update and inside 1 of the attributes it's updating, I'm calling a the write method (def attribute=) in foo's model class and want it to fail the entire update conditionally. What can I put in there? I tried using errors[:base] but it doesn't fail the save. I can't use validates either because the attribute will be transformed into something else before it gets saved.
def attribute=(attr)
if bar
# code to fail entire db save
end
end
You can just check the condition on a before_save callback in model foo.rb and return false if you don't want to save it.
before_save :really_want_to_save?
private
def really_want_to_save?
conditional_says_yes ? true : false
end
If you want error message too, then
def really_want_to_save?
if conditional_says_yes
true
else
errors[:base] << "failed"
false
end
end
If you want to abort from within the setter, then raising an exception will suffice.
def attribute=(attr)
if bar
raise "Couldn't save because blah blah"
end
end
However, as mentioned in other posts, it is likely better to do this check before save. That's what validation is for.
validate :my_condition
def my_condition
if bar
errors.add(:base, "Couldn't save because blah blah")
end
end
I'm trying to substitute ActiveRecord validations with Dry-validations, but I've been unable to find any in-app implementation examples to follow.
Dry-validation docs: http://dry-rb.org/gems/dry-validation/
I've added below to the form object, but I don't understand how to actually implement it so the validation fails if title is not inputted in the UI's form.
schema = Dry::Validation.Schema do
required(:title).filled
end
Form Object (setup with Virtus):
class PositionForm
include Virtus.model
include ActiveModel::Model
require 'dry-validation'
require 'dry/validation/schema/form'
# ATTRIBUTES
attribute :id, Integer
attribute :title, String
...
# OLD ACTIVE RECORD VALIDATIONS
#validates :title, presence: true
# NEW DRY VALIDATIONS
schema = Dry::Validation.Schema do
required(:title).filled
end
def save
if valid?
persist!
true
else
false
end
end
def persist!
#position = Position.create!(title: title...)
end
end
I've never used dry-validation before - any guidance would be super appreciated!
UPDATE
I've been able to "make it work" but it still doesn't feel like the correct design pattern.
Updated save method
def save
schema = Dry::Validation.Schema do
required(:title).filled
end
errors = schema.(title: title)
if valid? && errors.messages.empty?
persist!
true
else
false
end
end
If anyone could share guidance on the appropriate design pattern to implement dry-validation into a virtus-style form object it would be super appreciated!
I would try to keep validation at the model level.
Have a ModelValidations model in your initializers, each method named after the model it validates.
config/initialize/model_validations.rb
module ModelValidations
def position_form
Dry::Validation.Schema do
required(:title).filled
end
end
end
In the model, call the dry_validation module for that model.
app/models/position_form.rb
class PositionForm
validates :dry_validation
def dry_validation
ModelValidations.position_form(attributes).each do |field, message|
errors.add(field, message)
end
end
end
I have a model
class Vehicule < ActiveRecord::Base
before_validation :set_principal, :if =>:new_record?
private
def set_principal
self.principal ||= !!principal
end
end
If I test it, when I do Vehicule.new.valid? it always return false. Why did I had to add ̀€true in the function to pass the test ?
private
def set_principal
self.principal ||= !!principal
true
end
For starters, failed validations populate an errors object in your record instance. You can see the errors by running model.errors.full_messages which is always useful for debugging.
Secondly, test for the presence of a value using the present? method rather than using the awkward !! approach which returns false when called on nil. It would make your code much more clear but it's also useful because it recognizes "empty" objects as well. In other words "".present? == false and [].present? == false and {}.present? == false nil.present? == false and so on. There's also the blank? method which is the logical opposite of present?.
Finally, be mindful of your return value when using the :before_validation callback. Unlike other callbacks it will prevent validation if the function returns false.
because not not nil => false.
In IRB:
foo = nil
!!foo => false
if you try Vehicule.new(principal: 'WORKS').valid?
def set_principal
self.principal ||= !!principal
end
It should pass.
Is there a way to hook into the save! with a callback?
I am looking for something like:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
before_save :save_subservices! if bang_save?
private
def save_subservices
#subservices.each(&:save)
end
def save_subservices!
#subservices.each(&:save!)
end
end
Where a save! is cascaded and calls save! on the (faux) association subservices.
Technically you can do this, but I would advise not to use this approach in production because it can change in newer rails. And it is just wrong.
You can inspect call stack of your before callback and check if there is save! method.
class CompositeService < Service
before_save :some_callback
def some_callback
lines = caller.select { |line| line =~ /persistence.rb/ && line =~ /save!/ }
if lines.any?
#subservices.each(&:save!)
else
#subservices.each(&:save)
end
end
end
I wonder: is this extra logic even necessary?
If the save method on each of your #subservices obeys the ActiveRecord save semantics, then you probably will get the correct behavior for free.
In other words, make sure your save methods return true or false for success or failure. Then, the composite code becomes as simple as this:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
private
def save_subservices
#subservices.all?(&:save)
end
end
If any of your sub services fail to save, then the save_subservices callback will return false, which will abort the callback chain. This will cause the wrapping save to return false. And in the case of save!, it will raise an exception.
composite.save
# => false
composite.save!
# => ActiveRecord::RecordNotSaved
Look at ActiveRecord autosave attribute:
http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html