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: