Skip to content

Instantly share code, notes, and snippets.

@cflipse
Last active October 1, 2019 15:56
Show Gist options
  • 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
@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