Validation Delegation

Validation delegation allows an object to proxy validations to other objects. This facilitates composition and prevents the duplication of validation logic.

Installation

Add this line to your application's Gemfile:

gem 'validation_delegation'

And then execute:

$ bundle

Or install it yourself as:

$ gem install validation_delegation

Usage

An example use case for validation delegation is a SignUp object which simultaneously creates a User and an Organization. The SignUp object is only valid if both the user and organization are valid.

class User < ActiveRecord::Base
  validates :email, presence: true
end

class Organization < ActiveRecord::Base
  validates :name, presence: true
end

class SignUp
  include ActiveModel::Validations

  # delegate validation to the user
  delegate_validation to: :user

  # and also delegate validation to the organization
  delegate_validation to: :organization

  attr_reader :user, :organization

  def initialize
    @user = user
    @organization = organization
  end

  def email=(email)
    @user.email = email
  end

  def name=(name)
    @organization.name = name
  end
end

Assigning invalid user and organization attributes, which are in turn assigned to the @user and @organization instance variables, invalidates the SignUp, and faithfully copies the user and organization errors.

signup = SignUp.new
signup.email = ""
signup.name = ""

signup.valid?
# => false

signup.errors.full_messages
# => ["email can't be blank", "name can't be blank"]

signup.email = "[email protected]"
signup.name = "My Organization"

signup.valid?

If you do not want to copy errors directly onto the composing object, you can specify to which attribute the errors should apply. In this case, we copy errors onto the "organization" attribute. This is useful for nesting forms via fields_for.

class SignUp
  include ActiveModel::Model

  delegate_validation :organization, to: :organization

  attr_reader :organization

  def initialize
    @organization = Organization.new
  end

  def name=(name)
    @organization.name = name
  end
end

signup = SignUp.new
signup.name = ""
signup.valid?
# => false

signup.errors.full_messages
# => ["organization name can't be blank"]

Options

delegate_validation accepts several options.

  • :if - errors are only copied if the method specified by the :if option returns true
class SignUp
  delegate_validation to: :user, if: :validate_user?

  def validate_user?
    # logic
  end
end
  • :unless - errors are only copied if the method specified by the :unless option returns false
class SignUp
  delegate_validation to: :user, unless: :skip_validation?

  def skip_validation?
    # logic
  end
end
  • :only - a whitelist of errors to be copied
class SignUp
  delegate_validation to: :user, only: :email
end

signup = SignUp.new
signup.user.errors.add(:email, :required)
signup.user.errors.add(:phone_number, :required)

signup.valid?
signup.errors.full_messages
# => ["email can't be blank"]
  • :except - a blacklist of errors to be copied
class SignUp
  delegate_validation to: :user, except: :email
end

signup = SignUp.new
signup.user.errors.add(:email, :required)
signup.user.errors.add(:phone_number, :required)

signup.valid?
signup.errors.full_messages
# => ["phone number can't be blank"]

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request