I’m importing lots of CSV restaurant inspection data with Ruby, and I need to make sure the cleaned up data matches the spec. For example, a violation must have a business_id and date. It can optionally have a code and description. My goal was to be able to write a class like this:
class Violation < ValidatedObject attr_accessor :business_id, :date, :code, :description validates :business_id, presence: true validates :date, presence: true, type: Date end
…and it would just work, Rails-style:
Violation.new do |v| v.business_id = '1234' v.date = '2015-01-15' end # => ArgumentError: date must be of the class Date Violation.new do |v| v.date = Date.new(2015, 1, 25) end # => ArgumentError: business_id is required Violation.new do |v| v.business_id = '1234' v.date = Date.new(2015, 1, 25) end # => new instance Violation<...>
ActiveModel::Validations was refactored out of ActiveRecord to enable just this sort of use case. This is because the awesome list of built-in validations (here and here) and methods like #valid? are useful in a variety of contexts, not just in Rails.
It turned out that it was easy to write a small base class with the ActiveModel::Validations mixin to make the checking automatic upon initialization:
require 'active_model'
class ValidatedObject
include ActiveModel::Validations
def initialize(&block)
block.call(self)
check_validations!
end
def check_validations!
fail ArgumentError, errors.messages.inspect if invalid?
end
end
For my importing and parsing purposes, I created a small custom TypeValidator:
# Ensure an object is a certain class. This is an example of a custom
# validator. It's here as a nested class for easy access by subclasses.
#
# @example
# class Dog < ValidatedObject
# attr_accessor :weight
# validates :weight, type: Float
# end
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.class == options[:with]
message = options[:message] || "is not of class #{options[:with]}"
record.errors.add attribute, message
end
end
Here’s the finished ValidatedObject, and an example subclass: information about a CSV feed. This set up is working great; the only change I can see making soon is changing ValidatedObject to be mixed in via include rather than subclassing.
Thanks to:
Leave a comment