It is the pervading law of all things organic and inorganic,
Of all things physical and metaphysical,
Of all things human, and all things super-human,
Of all true manifestations of the head,
Of the heart, of the soul,
That the life is recognizable in its expression,
That form ever follows function. This is the law.
— Louis Sullivan, 1896
Sullivan is a functional, composable, simple way to validate nested data structures. It generates validation errors which are especially suitable for API responses.
Sullivan doesn't do much, because it doesn't need to. It's three things:
- A simple pattern for defining validators,
- A handful of useful, composable validators provided for free, and
- Some syntactic sugar for using the built-in validators.
require 'sullivan'
laugh_session_validation = Sullivan.validation do
laugh = hash(
sound: string_matching(/\Al(ol)+\z/, error: "must be be a laughing sound of some length"),
intensity: optional(kind_of(Numeric))
)
hash(
primary_laugh: laugh,
rebound_giggles: many(laugh)
)
end
laugh_session = {
primary_laugh: {
sound: "lolololol",
intensity: "High"
},
rebound_giggles: [
{
sound: "lololol",
intensity: 2
},
{
sound: "sigh",
mood: "pleasant"
}
]
}
laugh_session_validation.validate(laugh_session)
# =>
# {
# :primary_laugh => {
# :intensity => "must be a kind of Numeric, if present"
# },
# :rebound_giggles => [
# nil,
# {
# :sound => "must be be a laughing sound of some length",
# :mood => "is unexpected"
# }
# ]
# }
Add this line to your application's Gemfile:
gem 'sullivan'
And then execute:
$ bundle
Or install it yourself as:
$ gem install sullivan
Like it says above, Sullivan is three things.
Sullivan specifies a simple API for defining validators. It's so simple, you
don't need Sullivan to use it. All you need is an object which responds to
#validate
. If it fails to validate, it should return an error message (a
string). If it passes validation, it should return nil
. That's all there is
to it.
If your validator takes parameters, it might make sense to write it as a class and instantiate it:
class LegalVotingAge
def initialize(country:)
@minimum_age =
case country
when :united_states
18
when :austria
16
else
raise "Don't know the voting age in #{country}"
end
end
def validate(age)
"is too young to vote" if age < @minimum_age
end
end
LegalVotingAge.new(:united_states).validate(17) #=> "is too young to vote"
LegalVotingAge.new(:austria).validate(17) #=> nil
If your validator doesn't take parameters, you might want to just make it an object:
ApiBoolean = Object.tap do |v|
def v.validate(value)
"must be a boolean value" unless [true, false, 'true', 'false'].include?(value)
end
end
ApiBoolean.validate(true) #=> nil
ApiBoolean.validate('false') #=> nil
ApiBoolean.validate('not sure') #=> "must be a boolean value"
Sullivan doesn't care. In fact, Sullivan-the-libary isn't even involved yet.
Sometimes you need a really custom validator, but there are a few staples we
need in all sorts of projects. Sullivan provides those, including some
higher-order validators (validators which take other validators as
parameters, like Hash
and Optional
), which is where Sullivan's
composability really shines.
Sullivan's built-in validators live in Sullivan::Validators
. Look there to
read more about each one.
The built-in validators are a bit cumbersome to instantiate, considering you'll
be using them quite a bit. To help, there's Sullivan.validation
. Within its
block, you can instantiate the validators in Sullivan::Validators
as
snake_cased
methods. So:
v = Sullivan.validation do
hash({})
end
v.class #=> Sullivan::Validators::Hash
There's one catch: because this uses instance_eval
, inside the block self
will not be the same as self
outside the block, so you can't use method calls
the way you might like to. That is, this won't work:
class User
def valid_username_regex
%r{\w+}
end
def validation
Sullivan.validation do
string_matching(valid_username_regex)
#=> NameError: undefined local variable or method `valid_username_regex' for #<Sullivan::DSL:0x007fa5c44e1508>
end
end
end
If you need to do something like that, you can use the 1-arity form of the block, for a slightly more verbose syntax:
class User
def valid_username_regex
%r{\w+}
end
def validation
Sullivan.validation do |vals|
vals.string_matching(valid_username_regex)
end
end
end
Of course, Sullivan.validation
is completely optional. Feel free to instantiate the validation classes directly.
This is where it gets fun. Let's say you're validating API input. Suppose you have an API that can create a Person record and one that can create multiple Person records at once. You might have validations like:
module Validations
Person = Sullivan.validation do
hash(
name: kind_of(String),
favorite_ice_cream_flavor: optional(kind_of(String))
)
end
PersonCreation = Sullivan.validation do
hash(person: Person)
end
PeopleCreation = Sullivan.validation do
hash(people: many(Person))
end
end
Now your API could use Validations::PersonCreation.validate
and Validations::PeopleCreation.validate
to validate the two kinds of requests.
Notice that I've assigned these validators to constants in a module. That's a useful pattern in some cases, but it's completely optional. Store them wherever they're most useful in your application. They're just objects.
- Fork it ( http://github.com/Peeja/sullivan/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request