This project is archived and is in readonly mode.
Adding a validates_with method
Reported by Jeff Dean | May 10th, 2009 @ 04:56 AM | in 3.0.2
This patch adds a method called validates_with that takes a class, or a number of classes, and allows those classes to add errors to the record.
Example
class Person < ActiveRecord::Base
validates_with MyValidator
end
class MyValidator < ActiveRecord::Validator
def validate
if some_complex_logic
record.errors.add_to_base("This record is invalid")
end
end
private
def some_complex_logic
# ...
end
end
The current set of validations focus on data-integrity, and this
patch adds support for more domain-specific validations, akin to
the Specification pattern described in "Domain Driven Design"
(although only fulfilling one function of the specification
pattern).
Use Cases
I wrote this to help de-clutter my model code of complex, multi-model validations and to allow for easier validation reuse. For example, let's say you have a setup like Highrise, where you charge users for the number of Contacts they create, and enforce limits based on their plan. Currently you might add a validation like so:
class Person < ActiveRecord::Base
belongs_to :account
validate :cannot_exceed_max_allowed_people
private
def cannot_exceed_max_allowed_people
if account.people.count >= account.max_allowed_people
errors.add_to_base("You have exceeded...")
end
end
end
In the apps I work on, these validations have started to increase both in number and complexity, and in some of my models I have more validation code than anything else. So I've started to move these validations to separate classes for easier testability (fewer things to stub) and readability.
class Person < ActiveRecord::Base
belongs_to :account
validate :cannot_exceed_max_allowed_people
private
def cannot_exceed_max_allowed_people
PersonAccountValidator.new(self).validate
end
end
class PersonAccountValidator
attr_reader :record
def initialize(record)
@record = record
end
def validate
if record.account.people.count >= record.account.max_allowed_people
record.errors.add_to_base("You have exceeded...")
end
end
end
I can now test the public validate method on the class on it's
own, and stub out the complex validations when I need to by just
creating a fake PersonAccountValidator class.
Over time these custom validator classes grow and have a lot in common, and can be DRY'ed up by using a common base class that looks like this:
class Validator
attr_reader :record
def initialize(record)
@record = record
end
def validate
raise "You must override this method"
end
end
In addition to just de-cluttering validations from models, this allows better support for:
- Shared validations - let's say that both Person and Organization count as a contact, and each needs to be validated against the Account's max_contact limit. Both classes could reuse the same validation class
- Pluggable validations - if your app has per-installation validations based on varying regulations (like an app that's deployed to several different states, and each has different tax laws) you could move validations to a gem
- Validation plugins - This makes it easy for gem/plugin authors to create custom validations without having to re-open validations at all. Since the non-standard configuration options get passed in, plugin authors can document their api to allow users to pass in custom attributes, like the name of a field:
class Article < ActiveRecord::Base
validates_with SwearWordValidator, :columns => [:content, :title]
end
This patch just adds:
- The ActiveRecord::Validator base class
- The validates_with method
- Tests
- RDoc
- An update to the guides
Comments and changes to this ticket
-
Pratik May 16th, 2009 @ 04:57 PM
- Tag changed from active_record, doc, patch, validations to active_record, patch, validations
-
CancelProfileIsBroken August 6th, 2009 @ 02:40 PM
- Tag changed from active_record, patch, validations to active_record, bugmash, patch, validations
-
Derander August 8th, 2009 @ 02:59 AM
+1
This would be useful in some of my applications. We have a huge number of models that all use essentially the same sets of validations, which are currently wrapped into modules. This would allow us to extract the "should I run this validation" logic into the model which is being validation more easily.
I've read the source completely, and it seems reasonable and consistent with the way the other validations are implemented.
The tests pass on 2-3-stable
-
Josh Sharpe August 8th, 2009 @ 03:04 AM
Cool idea. It would make sense for Observers and Validators to function the same way, since they both have the same idea of moving functionality out of the base class... As it stands now, Observers have to be explicitly defined in environment.rb for them to function. Based on the description in this post, this doesn't seem to be the case with Validators.
See Configuration here:
http://api.rubyonrails.org/classes/ActiveRecord/Observer.html
-
Matt Duncan August 8th, 2009 @ 03:05 AM
+1
Pluggable validations and validation plugins would be really nice.
Passes all tests on 2-3-stable.
-
Michael Koziarski August 8th, 2009 @ 03:35 AM
- Assigned user set to Michael Koziarski
- Milestone cleared.
Hey Jeff,
I like this but only for 3.0 (bit too new a feature for a point release).
Can you rebase it for master and re-upload. I'll apply it after that.
Josh, the only reason observers need to be defined in env.rb is that they're 'magic' so CustomerObserver automatically observes 'Customer'. Given we have a declaration here, it's fine.
-
Jeff Dean August 9th, 2009 @ 08:31 AM
Thanks! I've rebased from master and uploaded the new patch.
There are 3 things to note about this patch:
-
It was a non-trivial move, since validations are now in ActiveModel. I moved the code and tests to match the style of ActiveModel.
-
ActiveModel tests seem to cover the ability to send validations based on a certain key (:create, :update), and ActiveRecord tests seem to cover the fact that they send the correct key to the validations. In the original patch I put tests in to test this explicitly, but in this patch I removed the 2 test methods because they seemed redundant.
-
The validation guide is no longer up to date with the new syntax for errors.add_to_base => errors[:base] etc... I can fix the guide and add validates_with documentation, but I'll send that as a separate patch to the guides project. I've adjusted all of the RDoc comments to reflect the new style of validations.
Also, I had some mysterious test breakages on ActiveModel in master. CI was broken for other reasons, so can't say whether it was local or not. After looking into both of the failures, I can't imagine how any side effect of this patch would affect these tests, but since they were red for me to begin with, I can't say for sure. Here's the test output just for the record:
1) Failure: test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records(BelongsToAssociationsTest) [./test/cases/associations/belongs_to_associations_test.rb:417:in `test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records' ./test/cases/../../lib/../../activemodel/lib/../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../lib/../../activemodel/lib/../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: <nil> expected but was <"">. 2) Failure: test_schema_dump_keeps_id_column_when_id_is_false_and_id_column_added(SchemaDumperTest) [./test/cases/schema_dumper_test.rb:214:in `test_schema_dump_keeps_id_column_when_id_is_false_and_id_column_added' ./test/cases/../../lib/../../activemodel/lib/../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__' ./test/cases/../../lib/../../activemodel/lib/../../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run']: non-primary key id column not preserved. <" t.string \"id\", :default => \"\", :null => false"> expected to be =~ </t.string[[:space:]]+"id",[[:space:]]+:null => false$/>.
-
-
Jeff Dean August 9th, 2009 @ 08:50 AM
@josh - I based this patch off of the existing callback code, which allows you to do:
class Foo < ActiveRecord::Base after_save MyCallbackClass.new end
That code only exists, as far as I know, to support observers. It has the pleasant side effect that you can move complex callback code to a different class, and I use it all the time outside of observers.
Then there's another layer of observers which hooks these external classes up automatically. I've never felt the need to go to that level of abstraction with validations, so I didn't add it to this feature. At the very least, I see them as separate features.
-
Rizwan Reza August 9th, 2009 @ 03:58 PM
verified
+1 This patch works cleanly on master. Safe to commit.
-
Elad Meidar August 9th, 2009 @ 08:57 PM
+1 verified and applied on 2-3-stable and master. +1 on idea too, i have been doing those dependent validations for a while too, trying to find a better way.. good job!
-
Josh Nichols August 10th, 2009 @ 04:21 AM
+1, great idea which would make it a lot easier to build re-usable and testable validations.
Also verified it applies to master cleanly, and the tests pass.
-
Repository August 10th, 2009 @ 06:48 AM
- State changed from new to committed
(from [22f339825329e2d4463a4130e9fa68baf9d27eb6]) Introduce validates_with to encapsulate attribute validations in a class.
[#2630 state:committed]
Signed-off-by: Jeremy Kemper jeremy@bitsweat.net
http://github.com/rails/rails/commit/22f339825329e2d4463a4130e9fa68... -
Jeremy Kemper August 10th, 2009 @ 06:48 AM
- Assigned user changed from Michael Koziarski to josh
- Tag changed from active_record, bugmash, patch, validations to active_record, patch, validations
-
Jeremy Kemper October 15th, 2010 @ 11:01 PM
- Milestone set to 3.0.2
- Importance changed from to
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
<h2 style="font-size: 14px">Tickets have moved to Github</h2>
The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>
People watching this ticket
Attachments
Referenced by
- 2630 Adding a validates_with method [#2630 state:committed]