Skip to content

Instantly share code, notes, and snippets.

@cflipse
Last active October 1, 2019 15:56
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save cflipse/2961010 to your computer and use it in GitHub Desktop.
Save cflipse/2961010 to your computer and use it in GitHub Desktop.
External Validations using only ActiveModel
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
@loveybot
Copy link

Yay Ruby!!

@cflipse
Copy link
Author

cflipse commented Jun 20, 2012

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.

@therealadam
Copy link

👍 on principle. The __getobj__ bit gives me pause, though.

@cflipse
Copy link
Author

cflipse commented Jun 21, 2012

I wrapped this up in a blog post with a bit more context:
http://devcaffeine.com/blog/2012/06/20/isolating-validations-in-activemodel/

@cflipse
Copy link
Author

cflipse commented Jun 21, 2012

@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

@fmnoise
Copy link

fmnoise commented Nov 24, 2016

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment