Ruby on Rails 3 made dynamic validations really easy. Yet I feel like most people follow bad advice when implementing dynamic validations in rails. If you read posts on Stackoverflow you’ll find something like this:
class Project < ActiveRecord::Base
validates_presence_of :name, if: :name_should_be_present
attr_accessor :name_should_be_present
end
# ...
class ProjectsController < ApplicationController
def create
@project = Project.new(params)
@project.name_should_be_present = true
if @project.save
# snip
else
# snap
end
end
end
The more complex your validations get e.g. when implementing a multi-step wizard your model will get unreadable and most probably untestable as well. If you need access to your current_user
inside your validations all hell will break loose (like Thread global variables).
While the above version works I opt for the slightly longer version which cleanly separates my custom validations: ActiveModel::Validator
. I have to write a little more code which enables me to cleanly separate my custom per-case validation logic from the remaining persistence layer.
class Project < ActiveRecord::Base
validate :instance_validations
attr_accessor :custom_validator
attr_accessor :custom_validator_options
def instance_validations
validates_with custom_validator, (custom_validator_options || {}) if custom_validator.present?
end
end
# ...
class ProjectsController < ApplicationController
def create
@project = Project.new(params)
@project.custom_validator = ProjectValidator
@project.custom_validator_options = { current_user: current_user }
if @project.save
# snip
else
# snap
end
end
end
# ...
class ProjectValidator < ActiveModel::Validator
def validate(project)
current_user = options[:current_user]
if current_user.project_owner?
unless project.name.present?
project.errors[:name] << I18n.t('activerecord.errors.messages.blank', attribute: I18n.t(:"activerecord.attributes.project.name"))
end
end
end
end
A word of warning: you might be tempted to write this inside your controller:
class ProjectsController < ApplicationController
def create
@project = Project.new(params)
@project.validates_with ProjectValidator, { current_user: current_user }
if @project.save
# fail, save works because #valid? returned true
else
# snap
end
end
end
While the errors are added to your instance #valid?
will still return true because the errors need to be present when evaluating the validation callback. If you know a working solution which does not require an attr_accessor
I’d be grateful for sharing.