Efficient way to report record validation warnings as well as errors?

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


Another options is to have a seperate object for warnings as such:

class MyModelWarnings < SimpleDelegator
  include ActiveModel::Validations

  validates :name, presence: true

  def initialize(model)
    super
    validate
  end  

  def warnings; errors; end
end

class MyModel < ActiveRecord::Base
  def warnings
    @warnings ||= MyModelWarnings.new(self).warnings
  end
end