I've got a Rails project where, as in most apps, we have a number of hard-and-fast validation rules to which all objects must conform before being persisted. Naturally, ActiveModel's Validations are perfect for that – we're using a combination of Rails defaults and our own hand-rolled validations.
More and more, though, we're coming up against use cases where we would like to alert the user to cases where, while their data is not invalid in the strictest sense, there are elements which they should review, but which shouldn't in themselves prevent record persistence from occurring. A couple of examples, off the top of my head:
The validations module is such a good metaphor for how we treat validation errors – and has so many matchers already available – that ideally I'd like to be able to reuse that basic code, but to generate a collection of warnings
items alongside errors
. This would allow us to highlight those cases differently to our users, rather than implying that possible violations of house style are equivalent to more egregious, strictly enforced rules.
I've looked at gems such as activemodel-warnings, but they work by altering which matchers are checked when the record is validated, expanding or shrinking the errors
collection accordingly. Similarly, I looked at the built-in :on
parameter for validations to see if I could hand-roll something, but again all violations would end up in an errors collection rather than separated out.
Has anybody tried anything similar? I can't imagine I'm the only one who'd like to achieve this goal, but am drawing a blank right now...
Here is some code I wrote for a Rails 3 project that does exactly what you're talking about here.
# Define a "warnings" validation bucket on ActiveRecord objects.
#
# @example
#
# class MyObject < ActiveRecord::Base
# warning do |vehicle_asset|
# unless vehicle_asset.description == 'bob'
# vehicle_asset.warnings.add(:description, "should be 'bob'")
# end
# end
# end
#
# THEN:
#
# my_object = MyObject.new
# my_object.description = 'Fred'
# my_object.sensible? # => false
# my_object.warnings.full_messages # => ["Description should be 'bob'"]
module Warnings
module Validations
extend ActiveSupport::Concern
include ActiveSupport::Callbacks
included do
define_callbacks :warning
end
module ClassMethods
def warning(*args, &block)
options = args.extract_options!
if options.key?(:on)
options = options.dup
options[:if] = Array.wrap(options[:if])
options[:if] << "validation_context == :#{options[:on]}"
end
args << options
set_callback(:warning, *args, &block)
end
end
# Similar to ActiveModel::Validations#valid? but for warnings
def sensible?
warnings.clear
run_callbacks :warning
warnings.empty?
end
# Similar to ActiveModel::Validations#errors but returns a warnings collection
def warnings
@warnings ||= ActiveModel::Errors.new(self)
end
end
end
ActiveRecord::Base.send(:include, Warnings::Validations)
The comments at the top show how to use it. You can put this code into an initializer and then warnings should be available to all of your ActiveRecord objects. And then basically just add a warnings do
block to the top of each model that can have warnings and just manually add as many warnings as you want. This block won't be executed until you call .sensible?
on the model.
Also, note that since warnings are not validation errors, a model will still be technically valid even if it isn't "sensible" (as I called it).
Years later, but in newer Rails versions there's a bit easier way:
attr_accessor :save_despite_warnings
def warnings
@warnings ||= ActiveModel::Errors.new(self)
end
before_save :check_for_warnings
def check_for_warnings
warnings.add(:notes, :too_long, count: 120) if notes.to_s.length > 120
!!save_despite_warnings
end
Then you can do: record.warnings.full_messages
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With