Skip to content

Instantly share code, notes, and snippets.

@biscuitvile
Last active August 29, 2015 13:57
Show Gist options
  • Select an option

  • Save biscuitvile/9401081 to your computer and use it in GitHub Desktop.

Select an option

Save biscuitvile/9401081 to your computer and use it in GitHub Desktop.
Validation as a service

This is my take on isolating validations from ActiveRecord models, inspired by Corey Haines' gist here: https://gist.github.com/coreyhaines/3305349

The main desires are putting validation responsibility into their own object, reducing the bloat of god classes, and easing validation concerns from tests which should not care about them, particularly when validations are quite complex.

This method relies upon inclusion of the ValidationService module within an ActiveRecord class. The model will invoke its validator automatically while saving provided there is a validator that follows the appropriate naming convention.

In order to perform tests that don't concern themselves with validation logic that you desired to be unencumbered by validation, you have two options. One is to instantiate with a NilValidator: Account.new(validation_service: NilValidator)

Alternatively you can monkey-patch the validating class at the top of your test file:

AccountValidator
  def validate; end
end

The latter approach may have negative impacts on other tests when using tools like Spork and may not be advisable in that scenario.

The loss of the ActiveModel helpers is noteworthy, but I feel not having them may steer away from greater trouble provided by such 'niceties'.

module ValidationService
  def self.included(base)

    base.class_eval do

      attr_accessor :validation_service

      after_initialize do
        unless self.validation_service
          self.validation_service = "#{self.class.name}Validator".constantize
        end
      end

      validate do |record|
        validation_service.new(record).validate
      end

    end
  end
end

class NilValidator
  attr_accessor :record

  def initialize(record)
    self.record = record
  end

  def validate
  end
end
class Account < ActiveRecord::Base
  include ValidationService
end
class BaseValidator

  attr_accessor :record

  def initialize(record)
    self.record = record
  end

  def validate
    before_validate
    perform_validations
  end

  private

  def perform_validations
    raise NotImplementedError
  end

  def before_validate
    clear_errors
  end

  def clear_errors
    record.errors.clear
  end

  def value_of(attribute)
    record.public_send(attribute)
  end

  def record_has?(attribute)
    value_of(attribute).present?
  end

end
class AccountValidator < BaseValidator

  private

  def perform_validations
    presence_of :store_name
    presence_of :contact_email
    email_format_of :contact_email
  end

  def presence_of(attribute)
    unless record_has? attribute
      record.errors.add(attribute, :blank)
    end
  end

  def email_format_of(attribute)
    return unless record_has? attribute
    if value_of(attribute) !~ /\A([^@\s]+)@((?:[-a-zA-Z0-9]+\.)+[a-zA-Z]{2,})\z/
      record.errors.add(attribute, :invalid)
    end
  end

end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment