Created
September 3, 2008 20:12
-
-
Save augustl/8652 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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