Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one put validations on individual ActiveModel/ActiveRecord objects?

You have a model, say, Car. Some validations apply to every Car instance, all the time:

class Car
  include ActiveModel::Model

  validates :engine, :presence => true
  validates :vin,    :presence => true
end

But some validations are only relevant in specific contexts, so only certain instances should have them. You'd like to do this somewhere:

c = Car.new
c.extend HasWinterTires
c.valid?

Those validations go elsewhere, into a different module:

module HasWinterTires
  # Can't go fast with winter tires.
  validates :speed, :inclusion => { :in => 0..30 }
end

If you do this, validates will fail since it's not defined in Module. If you add include ActiveModel::Validations, that won't work either since it needs to be included on a class.

What's the right way to put validations on model instances without stuffing more things into the original class?

like image 260
John Feminella Avatar asked Oct 21 '22 17:10

John Feminella


1 Answers

There are several solutions to this problem. The best one probably depends on your particular needs. The examples below will use this simple model:

class Record
  include ActiveModel::Validations

  validates :value, presence: true

  attr_accessor :value
end

Rails 4 only

Use singleton_class

ActiveSupport::Callbacks were completely overhauled in Rails 4 and putting validations on the singleton_class will now work. This was not possible in Rails 3 due to the implementation directly referring to self.class.

record = Record.new value: 1
record.singleton_class.validates :value, numericality: {greater_than: 1_000_000}
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

Rails 3 and 4

In Rails 3, validations are also implemented using ActiveSupport::Callbacks. Callbacks exist on the class, and while the callbacks themselves are accessed on a class attribute which can be overridden at the instance-level, taking advantage of that requires writing some very implementation-dependent glue code. Additionally, the "validates" and "validate" methods are class methods, so you basically you need a class.

Use subclasses

This is probably the best solution in Rails 3 unless you need composability. You will inherit the base validations from the superclass.

class BigRecord < Record
  validates :value, numericality: {greater_than: 1_000_000}
end

record = BigRecord.new value: 1
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

For ActiveRecord objects, there are several ways to "cast" a superclass object to a subclass. subclass_record = record.becomes(subclass) is one way.

Note that this will also preserve the class methods validators and validators_on(attribute). The SimpleForm gem, for example, uses these to test for the existence of a PresenceValidator to add "required" CSS classes to the appropriate form fields.

Use validation contexts

Validation contexts are one of the "official" Rails ways to have different validations for objects of the same class. Unfortunately, validation can only occur in a single context.

class Record
  include ActiveModel::Validations

  validates :value, presence: true

  attr_accessor :value

  # This can also be put into a module (or Concern) and included
  with_options :on => :big_record do |model|
    model.validates :value, numericality: {greater_than: 1_000_000}
  end
end

record = Record.new value: 1
record.valid?(:big_record) || record.errors.full_messages
# => ["Value must be greater than 1000000"]

# alternatively, e.g., if passing to other code that won't supply a context:
record.define_singleton_method(:valid?) { super(:big_record) }
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

Use #validates_with instance method

#validates_with is one of the only instance methods available for validation. It accepts one or more validator classes and any options, which will be passed to all classes. It will immediately instantiate the class(es) and pass the record to them, so it needs to be run from within a call to #valid?.

module Record::BigValidations
  def valid?(context=nil)
    super.tap do
      # (must validate after super, which calls errors.clear)
      validates_with ActiveModel::Validations::NumericalityValidator,
        :greater_than => 1_000_000,
        :attributes => [:value]
    end && errors.empty?
  end
end

record = Record.new value: 1
record.extend Record::BigValidations
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

For Rails 3, this is probably your best bet if you need composition and have so many combinations that subclasses are impractical. You can extend with multiple modules.

Use SimpleDelegator

big_record_delegator = Class.new(SimpleDelegator) do
  include ActiveModel::Validations

  validates :value, numericality: {greater_than: 1_000_000}

  def valid?(context=nil)
    return true if __getobj__.valid?(context) && super
    # merge errors
    __getobj__.errors.each do |key, error|
      errors.add(key, error) unless errors.added?(key, error)
    end
    false
  end

  # required for anonymous classes
  def self.model_name
    Record.model_name
  end
end

record = Record.new value: 1
big_record = big_record_delegator.new(record)
big_record.valid? || big_record.errors.full_messages
# => ["Value must be greater than 1000000"]

I used an anonymous class here to give an example of using a "disposable" class. If you had dynamic enough validations such that well-defined subclasses were impractical, but you still wanted to use the "validate/validates" class macros, you could create an anonymous class using Class.new.

One thing you probably don't want to do is create anonymous subclasses of the original class (in these examples, the Record class), as they will be added to the superclass's DescendantTracker, and for long-lived code, could present a problem for garbage collection.

like image 88
Steve Avatar answered Oct 23 '22 17:10

Steve