-
-
Save cflipse/2961010 to your computer and use it in GitHub Desktop.
require 'delegate' | |
require 'active_model' | |
class DraftPostValidator < SimpleDelegator | |
include ActiveModel::Validations | |
validates :title, :presence => true | |
validate :future_publication_date | |
private | |
def errors | |
__getobj__.errors | |
end | |
def future_publication_date | |
errors.add(:publication_date, "must be in the future") if publication_date && publication_date <= Date.today | |
end | |
end |
irb(main):063:0> post = Post.new | |
=> #<Post:0x10d196f28 @errors=#<ActiveModel::Errors:0x10d196ed8 @messages=#<OrderedHash {}>, @base=#<Post:0x10d196f28 ...>>> | |
irb(main):064:0> PublishedPostValidator.new(post).valid? | |
=> false | |
irb(main):065:0> post.errors.full_messages | |
=> ["title can't be blank", "author can't be blank"] | |
irb(main):070:0> post.publication_date = Date.yesterday | |
=> Tue, 19 Jun 2012 | |
irb(main):071:0> DraftPostValidator.new(post).valid? | |
=> false | |
irb(main):072:0> post.errors.full_messages | |
=> ["title can't be blank", "publication_date must be in the future"] | |
irb(main):073:0> |
require 'active_model' | |
# Most of this is the basic boilerplate described in the docs for active_model/errors; ie, the bare minimum | |
# a class must have to use AM::Errors | |
class Post | |
extend ActiveModel::Naming | |
attr_reader :errors | |
attr_accessor :title, :author, :publication_date | |
def initialize | |
@errors = ActiveModel::Errors.new(self) | |
end | |
def read_attribute_for_validation(attr) | |
send(attr) | |
end | |
def self.human_attribute_name(attr, options = {}) | |
attr | |
end | |
def self.lookup_ancestors | |
[self] | |
end | |
en |
# A Validator for published objects. It may have more stringent validation rules than unpublished posts. | |
require 'delegate' | |
require 'active_model' | |
class PublishedPostValidator < SimpleDelegator | |
include ActiveModel::Validations | |
validates :title, :presence => true | |
validates :author, :presence => true | |
validates :publication_date, :presence => true | |
private | |
def errors | |
__getobj__.errors | |
end | |
end |
A quick demonstration of validations outside the context of an ActiveRecord class.
Our domain class is a PoRo that implements the API needed for the Errors attribute provided by ActiveModel. Implementing this allows the domain class to keep track of it's own errors, and communicate them back to the front-end of the rails app. ActiveModel is the API that ActionPack depends on.
Our validators are basic delegators that work by wrapping an instance of our domain class. They include the ActiveModel::Validations module, which gives most of the validations available in Rails -- a notable exception is the uniqueness validation, which hangs off ActiveRecord. The interesting property here is that the nice validation DSL that we're all used to is fully available in these objects.
Why would we bother? By separating the concerns of validations from our persistence or domain logic, we allow for more flexible code, at the potential cost of more verbosity. We can apply differerent sets of validations to different states without resorting to a raft of if: and unless: riders. We can ignore validation in contexts where the full and complete object is not necessary -- some testing situations, for example. We can keep our domain objects small, and focused purely on the domain they're operating in, without adding a lot of validation and verification code, and we can apply the same validations to numerous different domain objects.
👍 on principle. The __getobj__
bit gives me pause, though.
I wrapped this up in a blog post with a bit more context:
http://devcaffeine.com/blog/2012/06/20/isolating-validations-in-activemodel/
@therealadam
I was a bit perplexed by that as well, but it looks like including ActiveModel::Validations defines an errors method, so the errors get set on the validator, instead of the domain object. I didn't want that. But it's easy to miss. :|
http://api.rubyonrails.org/classes/ActiveModel/Validations.html
@cflipse
I've used the same approach, but divided into 3 parts:
domain object(data provider) - validator(defines validation rules) - validation result(stores errors)
it seemed to follow SRP a bit better
Yay Ruby!!