Dan Wanek
Dan Wanek

Reputation: 102

Instance-level validations in Rails

In my pursuit to add some dynamic policy logic to my ActiveRecord models I've attempted to create a way to add instance-level validations. Has anyone had experience with this? The stuff I've searched for has been less than helpful. Here is the solution I came up with. Please critique.

# This extension can be used to create instance-level validations. For example
# if you have an instance of 'user' you can do something like the following:
# @example:
#   user.custom_validation ->(scope) {
#     if scope.bad_logins >= scope.account.max_bad_logins
#       scope.errors.add :bad_logins, "too many bad logins for your account policy"
#     end
#   }
#   user.account.max_bad_logins = 5
#   user.bad_logins = 5
#   user.valid?   => false
#
module ActiveRecordExtension
  module CustomValidation

    def self.included(base)
      base.class_eval do
        attr_accessor :custom_validation
        validate :run_custom_validation
        send :include, InstanceMethods
      end
    end

    module InstanceMethods
      def run_custom_validation
        if custom_validation
          custom_validation.call(self)
        else
          true
        end
      end
    end

  end
end

ActiveRecord::Base.send :include, ActiveRecordExtension::CustomValidation

Upvotes: 2

Views: 512

Answers (1)

Chris Heald
Chris Heald

Reputation: 62668

This is a violation of separation of concerns. You're moving model validation logic into controller code. You really don't want your controllers to know about what makes a model valid - they should simply pass data to the model and get a valid-or-not response back. If you have a validator that should only be run on some instances of a model, then you can scope those validations to be run when certain conditions are set.

class User
  attr_accessor :enforce_login_limits
  validate :if => :enforce_login_limits do |user|
    if user.bad_logins >= user.account.max_bad_logins
      user.errors.add :bad_logins, "too many bad logins for your account policy"
    end
  end
end

# Controller
user.enforce_login_limits = true
user.bad_logins = 10
user.valid? # => false

Or, just attach custom validators with ActiveModel's existing #validates_with mechanism:

# Controller/service/whatever
@user.validates_with Validators::BadLoginValidator

# lib/validators/bad_login_validator.rb
class Validators::BadLoginValidator < ActiveModel::Validator
  def validate(user)
    if user.bad_logins && user.bad_logins >= user.account.max_bad_logins
      user.errors.add :bad_logins, "too many bad logins for your account policy"
    end
  end
end

Upvotes: 2

Related Questions