Search code examples
ruby-on-railsvalidationruby-on-rails-5.2

Best practice for only running a group of validations when a certain condition is met?


I have a Review model that has 3 validations that should only be run if the status of the review is "submitted". Here is my current model...

    class Review < ApplicationRecord
      enum status: { pending: 0, submitted: 1 }
      ...

      belongs_to :reviewable, polymorphic: true
      belongs_to :user

      validates :title, presence: true, length: { in: 5..50 }, if: -> { status == "submitted" }
      validates :description, length: { in: 10..500 }, if: -> { status == "submitted" }
      validates :stars, presence: true, inclusion: { in: (1..5) }, if: -> { status == "submitted" }
      ...
    end

A new Review is generated with the status of "pending". I still need to validate that a reviewable and user are associated with the review when a new one is created (so, I can't skip validations all together in the create action)...but only need to validate title, description, and stars when the user updates the review.

Is there a way to wrap the three validations (for title, description, and stars) in a conditional, so that it only runs the validations when a review is updated by the user (which changes the status from pending to submitted)...instead of calling if: -> { status == "submitted" } on all of them?


Solution

  • The first simplification is that you can use a symbol instead of an inline block:

      validates :title, presence: true, length: { in: 5..50 }, if: :submitted?
      validates :description, length: { in: 10..500 }, if: :submitted?
      validates :stars, presence: true, inclusion: { in: (1..5) }, if: :submitted?
    

    In general, you'd then define a method to match:

      def submitted?
        status == "submitted"
      end
    

    .. but you don't need to here, because enum gives you such a method for free.


    If you really want to squeeze it, there's a more obscure option:

      with_options if: -> { status == "submitted" } do |x|
        x.validates :title, presence: true, length: { in: 5..50 }
        x.validates :description, length: { in: 10..500 }
        x.validates :stars, presence: true, inclusion: { in: (1..5) }
      end
    

    (I switched back to the block because there's only one copy, though I'd still favour the if: :submitted? form in this case.)

    with_options is not a particularly well-known, or widely used, method -- it's there, but I'd only suggest reaching for it in very unusual circumstances.