Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Introduce validates_with to encapsulate attribute validations in a cl…
…ass. [#2630 state:committed] Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
- Loading branch information
Showing
6 changed files
with
255 additions
and
1 deletion.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
*Edge* | ||
|
||
* Introduce validates_with to encapsulate attribute validations in a class. #2630 [Jeff Dean] | ||
|
||
* Extracted from Active Record and Active Resource. |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
module ActiveModel | ||
module Validations | ||
module ClassMethods | ||
|
||
# Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions. | ||
# | ||
# class Person < ActiveRecord::Base | ||
# validates_with MyValidator | ||
# end | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def validate | ||
# if some_complex_logic | ||
# record.errors[:base] << "This record is invalid" | ||
# end | ||
# end | ||
# | ||
# private | ||
# def some_complex_logic | ||
# # ... | ||
# end | ||
# end | ||
# | ||
# You may also pass it multiple classes, like so: | ||
# | ||
# class Person < ActiveRecord::Base | ||
# validates_with MyValidator, MyOtherValidator, :on => :create | ||
# end | ||
# | ||
# Configuration options: | ||
# * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <tt>:update</tt> | ||
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should | ||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). | ||
# The method, proc or string should return or evaluate to a true or false value. | ||
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should | ||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). | ||
# The method, proc or string should return or evaluate to a true or false value. | ||
# | ||
# If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>: | ||
# | ||
# class Person < ActiveRecord::Base | ||
# validates_with MyValidator, :my_custom_key => "my custom value" | ||
# end | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def validate | ||
# options[:my_custom_key] # => "my custom value" | ||
# end | ||
# end | ||
# | ||
def validates_with(*args) | ||
configuration = args.extract_options! | ||
|
||
send(validation_method(configuration[:on]), configuration) do |record| | ||
args.each do |klass| | ||
klass.new(record, configuration.except(:on, :if, :unless)).validate | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
|
116 changes: 116 additions & 0 deletions
116
activemodel/test/cases/validations/with_validation_test.rb
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
# encoding: utf-8 | ||
require 'cases/helper' | ||
|
||
require 'models/topic' | ||
|
||
class ValidatesWithTest < ActiveRecord::TestCase | ||
include ActiveModel::ValidationsRepairHelper | ||
|
||
repair_validations(Topic) | ||
|
||
ERROR_MESSAGE = "Validation error from validator" | ||
OTHER_ERROR_MESSAGE = "Validation error from other validator" | ||
|
||
class ValidatorThatAddsErrors < ActiveRecord::Validator | ||
def validate() | ||
record.errors[:base] << ERROR_MESSAGE | ||
end | ||
end | ||
|
||
class OtherValidatorThatAddsErrors < ActiveRecord::Validator | ||
def validate() | ||
record.errors[:base] << OTHER_ERROR_MESSAGE | ||
end | ||
end | ||
|
||
class ValidatorThatDoesNotAddErrors < ActiveRecord::Validator | ||
def validate() | ||
end | ||
end | ||
|
||
class ValidatorThatValidatesOptions < ActiveRecord::Validator | ||
def validate() | ||
if options[:field] == :first_name | ||
record.errors[:base] << ERROR_MESSAGE | ||
end | ||
end | ||
end | ||
|
||
test "vaidation with class that adds errors" do | ||
Topic.validates_with(ValidatorThatAddsErrors) | ||
topic = Topic.new | ||
assert !topic.valid?, "A class that adds errors causes the record to be invalid" | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
end | ||
|
||
test "with a class that returns valid" do | ||
Topic.validates_with(ValidatorThatDoesNotAddErrors) | ||
topic = Topic.new | ||
assert topic.valid?, "A class that does not add errors does not cause the record to be invalid" | ||
end | ||
|
||
test "with a class that adds errors on update and a new record" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :on => :update) | ||
topic = Topic.new | ||
assert topic.valid?, "Validation doesn't run on create if 'on' is set to update" | ||
end | ||
|
||
test "with a class that adds errors on create and a new record" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :on => :create) | ||
topic = Topic.new | ||
assert !topic.valid?, "Validation does run on create if 'on' is set to create" | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
end | ||
|
||
test "with multiple classes" do | ||
Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors) | ||
topic = Topic.new | ||
assert !topic.valid? | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
assert topic.errors[:base].include?(OTHER_ERROR_MESSAGE) | ||
end | ||
|
||
test "with if statements that return false" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :if => "1 == 2") | ||
topic = Topic.new | ||
assert topic.valid? | ||
end | ||
|
||
test "with if statements that return true" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :if => "1 == 1") | ||
topic = Topic.new | ||
assert !topic.valid? | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
end | ||
|
||
test "with unless statements that return true" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :unless => "1 == 1") | ||
topic = Topic.new | ||
assert topic.valid? | ||
end | ||
|
||
test "with unless statements that returns false" do | ||
Topic.validates_with(ValidatorThatAddsErrors, :unless => "1 == 2") | ||
topic = Topic.new | ||
assert !topic.valid? | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
end | ||
|
||
test "passes all non-standard configuration options to the validator class" do | ||
topic = Topic.new | ||
validator = mock() | ||
validator.expects(:new).with(topic, {:foo => :bar}).returns(validator) | ||
validator.expects(:validate) | ||
|
||
Topic.validates_with(validator, :if => "1 == 1", :foo => :bar) | ||
assert topic.valid? | ||
end | ||
|
||
test "validates_with with options" do | ||
Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name) | ||
topic = Topic.new | ||
assert !topic.valid? | ||
assert topic.errors[:base].include?(ERROR_MESSAGE) | ||
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
module ActiveRecord #:nodoc: | ||
|
||
# A simple base class that can be used along with ActiveRecord::Base.validates_with | ||
# | ||
# class Person < ActiveRecord::Base | ||
# validates_with MyValidator | ||
# end | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def validate | ||
# if some_complex_logic | ||
# record.errors[:base] = "This record is invalid" | ||
# end | ||
# end | ||
# | ||
# private | ||
# def some_complex_logic | ||
# # ... | ||
# end | ||
# end | ||
# | ||
# Any class that inherits from ActiveRecord::Validator will have access to <tt>record</tt>, | ||
# which is an instance of the record being validated, and must implement a method called <tt>validate</tt>. | ||
# | ||
# class Person < ActiveRecord::Base | ||
# validates_with MyValidator | ||
# end | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def validate | ||
# record # => The person instance being validated | ||
# options # => Any non-standard options passed to validates_with | ||
# end | ||
# end | ||
# | ||
# To cause a validation error, you must add to the <tt>record<tt>'s errors directly | ||
# from within the validators message | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def validate | ||
# record.errors[:base] << "This is some custom error message" | ||
# record.errors[:first_name] << "This is some complex validation" | ||
# # etc... | ||
# end | ||
# end | ||
# | ||
# To add behavior to the initialize method, use the following signature: | ||
# | ||
# class MyValidator < ActiveRecord::Validator | ||
# def initialize(record, options) | ||
# super | ||
# @my_custom_field = options[:field_name] || :first_name | ||
# end | ||
# end | ||
# | ||
class Validator | ||
attr_reader :record, :options | ||
|
||
def initialize(record, options) | ||
@record = record | ||
@options = options | ||
end | ||
|
||
def validate | ||
raise "You must override this method" | ||
end | ||
end | ||
end |
22f3398
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great addition, but given it introduces the Validator class could not all ActiveModel validations defined in terms a of validator class? I am thinking of the Merb style of validation.
22f3398
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where should one install their Validator class(es)?
22f3398
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I alternate between putting validator classes in app/models (for small apps) or in a separate app/validators directory, and adding that directory to the load path in environment.rb.