Inspector

a ruby validation library

Installation

gem install object-inspector

Background

We often need to validate data. And a lot of the time we're forced to put those validation rules on our so-called models. I think this is making too many assumptions about the styles of applications that we're writing and doesn't give us enough flexibility to implement something outside of this box.

Validating models is great, sure. Very often I find myself needing to validate hashes or arrays, before I even hydrate that data on my models. Other times, I don't have the luxury of using a traditional ORM - maybe I store my data in XML files or maybe I don't even store it anywhere at all.

Inspector is designed to avoid those assumptions and give the developer flexibility and power of object validation dressed in a nice DSL. The actual validations definition syntax takes inspiration from RSpec's powerful matchers. And with nested validations your validation rules are guaranteed to be ever so concise and readable.

Read through quick start to get basic idea of what I'm talking about.

Quick start

require 'inspector'

Post   = Struct.new(:title, :body, :author)
Author = Struct.new(:email, :first_name, :last_name)

Inspector.valid(Post) do
  attribute(:title) do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(3).characters
  end

  attribute(:body) do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(3).characters
  end

  attribute(:author).should validate(:as => Author)
end

Inspector.valid(Author) do
  attribute(:email) do
    should_not be_empty
    should be_an_email
  end

  attribute(:first_name) do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(1).character
    should have_at_most(32).characters
  end

  attribute(:last_name) do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(1).character
    should have_at_most(32).characters
  end
end

author = Author.new("not an email", "John", "Smith")
post   = Post.new(123, nil, author)

violations = Inspector.validate(post)

if violations.empty?
  puts "post #{post.inspect} is valid"
else
  puts "invalid post #{post.inspect}:"
  puts violations.to_s.split("\n").map { |line| "  #{line}" }.join("\n")
end

Above code will result in the following:

invalid post #<struct Post title=123, body=nil, author=#<struct Author email="not an email", first_name="John", last_name="Smith">>:
  title:
    should.be_kind_of
  body:
    should_not.be_empty
    should.be_kind_of
    should.have_at_least
  author:
    email:
      should.be_an_email

The above example is fairly simplistic, yet demonstrates several important features:

  • Validation constraints can be negated (the use of should_not enforces this)
  • It is possible to nest validations (author attribute in Post validation rules)
  • Validations are not tied to error messages

Usage

The quick start above highlighted basic usage scenario. However, this is definitely not everything Inspector can do.

Validating hashes

require 'inspector'

Inspector.valid("request parameters") do
  property("title") do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(3).characters
  end

  property("body") do
    should_not be_empty
    should be_kind_of(String)
    should have_at_least(3).characters
  end
end

violations = Inspector.validate({
  "title" => 123,
  "body"  => nil
}, :as => "requests parameters")

puts violations unless violations.empty?

The code above will result in the following:

[title]:
  should.be_kind_of
[body]:
  should_not.be_empty
  should.be_kind_of
  should.have_at_least

Validating arrays

require 'inspector'

Inspector.valid("emails") do
  each_item.should be_an_email
end

puts Inspector.validate(["not an email", "[email protected]"], :as => "emails")

Above code produces:

[0]:
  should.be_an_email

DRYing validations

Sometimes we end up with almost exactly same validations on different attributes or properties. It is quite easy to remove the duplication by using validate constraint:

The validations above seem a little too verbose, but we can simplify them:

require 'inspector'

Post   = Struct.new(:title, :body, :author)
Author = Struct.new(:email, :first_name, :last_name)

Inspector.valid("required string") do
  should_not be_empty
  should be_kind_of(String)
  should have_at_least(3).characters
end

Inspector.valid("required short string") do
  should_not be_empty
  should be_kind_of(String)
  should have_at_least(1).character
  should have_at_most(32).characters
end

Inspector.valid(Post) do
  attribute(:title).should  validate :as => "required string"
  attribute(:body).should   validate :as => "required string"
  attribute(:author).should validate :as => Author
end

Inspector.valid(Author) do
  attribute(:email) do
    should_not be_empty
    should be_an_email
  end

  attribute(:first_name).should validate :as => "required short string"
  attribute(:last_name).should  validate :as => "required short string"
end

Built-in constraints

Inspector ships with some built-in constraints. Most of them are inspired by RSpec's matchers.

be_false

validate falsiness of a value.

attribute(:attribute) do
  should be_false
end

be_true

validate truthyness of a value.

attribute(:attribute) do
  should be_true
end

validate

validate an object as a valid type (defaults to its class):

attribute(:attribute) do
  should validate
end
attribute(:attribute) do
  should validate(:as => 'validation metadata')
end

be_email/be_an_email

validate value as email.

attribute(:attribute) do
  should be_email
end
attribute(:attribute) do
  should be_an_email
end

have/have_exactly

validate collection length.

attribute(:attribute) do
  should have(5).characters
end
attribute(:attribute) do
  should have_exactly(5).characters
end

have_at_least

validate collection minimum length.

attribute(:attribute) do
  should have_at_least(5).characters
end

have_at_most

validate collection maximum length.

attribute(:attribute) do
  should have_at_most(5).characters
end

be_*

validate using predicate method.

attribute(:attribute) do
  should be_valid # passes of attribute.valid? is true
end

Defining simple validations (TODO)

Inspector.define_constraint(:have_properties) do |*properties|
  valid? do |object|
    properties.all? { |property| object.has_key?(property) }
  end
end

Defining custom validations (TODO)


class HavePropertiesValidator
  def validate(value, constraint, violations_list)
    valid = constraint.properties.all? { |property| object.has_key?(property) }

    if valid ^ constraint.positive?
      violations_list << Inspector::Constraint::Violation.new(constraint)
    end
  end
end

class HavePropertiesConstraint
  include Inspector::Constraint

  def validator
    :have_properties
  end
end

Inspector.validators[:have_properties] = HavePropertiesValidator.new

Inspector.define_constraint(:have_properties, HavePropertiesConstraint)