Skip to content

Instantly share code, notes, and snippets.

@augustl
Created September 3, 2008 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save augustl/8652 to your computer and use it in GitHub Desktop.
Save augustl/8652 to your computer and use it in GitHub Desktop.
module ActiveModel
module Validations
def self.included(base)
base.class_eval do
@validation_stack = {
:any => [],
:create => [],
:update => []
}.freeze
include InstanceMethods
extend ClassMethods
end
end
module InstanceMethods
NEW_RECORD_NOT_IMPLEMENTED = "You need to implement a new_record? instance method in your class."
# The list of errors.
def errors
@errors ||= Errors.new
end
# Runs all the validations
def validate
errors.clear
self.class.validate(self)
end
# Checks if there are any errors
def valid?
errors.empty? && errors.base.empty?
end
# Should we run validations for create or update?
def validation_scope
raise NoMethodError, NEW_RECORD_NOT_IMPLEMENTED unless respond_to?(:new_record?)
new_record? ? :create : :update
end
end
module ClassMethods
MESSAGES = {
:blank => "can not be blank",
:format => "is of invalid format"
}
# Runs the actual validations, one by one.
def validate(instance)
# Runs the validations for the current validation scope (create or update)
@validation_stack[instance.validation_scope].each {|validation| validation.run(instance) }
# Runs the validations that runs on both create and update
@validation_stack[:any].each {|validation| validation.run(instance) }
end
# Validates that the value of an attribute isn't "blank?".
#
# Post.validates_presence_of :title
def validates_presence_of(*args)
options = args.options :message => MESSAGES[:blank]
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] if value.blank?
end
end
# Regular expression matching.
#
# User.validates_format_of :email, :with => /^.+@.+\..+$/
def validates_format_of(*args)
options = args.options :message => MESSAGES[:format]
options.assert_required_keys :with
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] unless value =~ options[:with]
end
end
def validates_length_of(*args)
# TODO: :minimum and :maximum, to complement :length
options = args.options :message => MESSAGES[:length]
options.assert_required_keys :length
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] if value < options[:length]
end
end
def validates_numericality_of(*args)
# TODO: Use something other than Integer(value) and rescue.
# NOTE: This method is pretty nasty in AR. I think AR should handle the oddities in
# order to make the change backwards compatible.
options = args.options :message => MESSAGES[:numericality]
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] unless Integer(value) rescue(false)
end
end
# Uses Enumerable#in? to determine if the value matches the items in the :in option.
#
# User.validates_inclusion_of :gener, :in => %(male female)
def validates_inclusion_of(*args)
options = args.options :message => MESSAGES[:inclusion]
options.assert_required_keys :in
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] unless options[:in].include?(value)
end
end
# The opposite of validates_inclusion_of.
def validates_exclusion_of(*args)
options = args.options :message => MESSAGES[:exclution]
options.assert_required_keys :in
validate_attributes(args, options) do |record, attribute, value|
record.errors[attribute] << options[:message] unless options[:in].include?(value)
end
end
# Validate attribute names with the block passed as the actual validation.
#
# validate_attributes(:title, :body) do |record, attribute, value|
# record.errors[atribute] << "must no suck" if record.sucks? and value.is_silly?
# end
def validate_attributes(attributes, options, &proc)
options.reverse_merge!(:on => :any)
@validation_stack[options[:on]] << Validation.new(attributes, proc)
end
end
# Instances of this class is stored in the validation stack.
# TODO: Add support for :if and :unless (symbols for method names and procs getting the
# instance yielded to them)
class Validation
def initialize(attributes, proc)
@attributes = attributes
@proc = proc
end
def run(instance)
@attributes.each {|attribute| @proc.call(instance, attribute, instance.send(attribute)) }
end
end
class Errors < Hash
# These errors are instance-wide errors rather than attribute specific ones.
attr_reader :base
# We want errors[:foo] to yield an empty array. See the tests.
def initialize
super {|hash, key| hash[key] = [] }
@base = []
end
# As we can potentially have empty arrays from calling errors[:foo], we have to
# check if the values are all empty arrays as well.
def empty?
values.all?(&:empty?)
end
end
end
end
require 'test_helper'
class ValidationTest < Test::Unit::TestCase
class Post
include ActiveModel::Validations
attr_accessor :title, :excerpt, :body, :new_record
def new_record?
@new_record
end
end
def setup
clear_all_validations Post
@post = Post.new
end
def test_on_create
@post.new_record = true
Post.validates_presence_of :body, :on => :update
assert_valid @post
Post.validates_presence_of :title, :on => :create
assert_invalid @post
@post.title = "yep"
assert_valid @post
end
def test_on_update
@post.new_record = false
Post.validates_presence_of :body, :on => :create
assert_valid @post
Post.validates_presence_of :title, :on => :update
assert_invalid @post
@post.title = "yep"
assert_valid @post
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment