Subvalid decouples your validation logic from your object structure. With Subvalid you can define different validation rules for different contexts. So rather than defining validation on the object, and having it be "objective", you can define it in a separate class - so it's "subjective". (as in Subjective validation).
Subvalid was extracted from a project at Envato which requires complex validation logic at each stage of an object's life cycle:
- Users upload videos. The videos are validated to make sure an actual video was uploaded (and not someone's university Powerpoint slides), that framerate is good, resolution and codec is acceptable etc. Failure here would reject the file straight away, and tell the user to try again with a new file.
- Next we generate thumbnails, resized preview videos etc. If anything fails validation here, it's a bug (wrong preview video size etc) - and we want to alert developers.
- After the video is uploaded and processed, users would enter metadata: title, description, tags etc. If that fails - we still want to save the item, but just leave it as "incomplete", and allow the user to come back later and complete it. Once this passes, the item is ready, and we submit it for review to our internal review team.
All these steps are done asynchronously, so we need to capture the errors, save them to a different field on the item, and carry on to report results back to the user.
While ActiveModel::Validations is great if you've got simple validation logic, it doesn't cut it for something complex like this. When you have different validation for the same object at each point in it's life cycle, you need something more flexible.
ActiveModel also hooks in pretty deep into ActiveRecord. It's main use case assume you're just wanting to prevent bad data hitting your database - which isn't necessarily always the case.
We needed something more. So Subvalid was born.
And you can have the best of both worlds. Subvalid can exist alongside ActiveModel. ActiveModel::Validations is great for ensuring data consistency, and you can add it to your model classes as normal - and then write Subvalid validator classes in addition to handle more complex nuanced validation logic. Or do it all in Subvalid - up to you.
- Very simple, consistent API
- Validation logic is defined in separate "Validator" classes completely decoupled from business logic
- Multiple validators can be defined for each piece of data in your system to be executed at different points
- Caller is in control. No magic happening under the hood
- Failing validation does not block saving to the database
- Does not add anything at all to business objects. No including modules, no monkey patching, no object extension. Subvalid assumes POROs, but works with anything. A key design goal is to not pollute the objects being validated at all
- Supports nested validation on nested object structures - and nicely handles nested errors.
- DSL and API inspired by ActiveModel::Validations - just simplified and more consistent.
Subvalid is extracted from production code in use at Envato. However, it is undergoing early development, and APIs and features are almost certain to be in flux.
Add this line to your application's Gemfile:
gem 'subvalid'
And then execute:
$ bundle
Or install it yourself as:
$ gem install subvalid
Say you've got some object:
Person = Struct.new(:name)
madlep = Person.new("madlep")
You can validate it with Subvalid like this:
require 'subvalid'
class PersonValidator
include Subvalid::Validator
validates :name, presence: true
end
PersonValidator.validate(madlep).valid? # => true
validate
returns a validation result. You can check if it is #valid?
or if
it has errors
on an attribute
result = PersonValidator.validate(Person.new(nil))
result.valid? # => false
result.errors[:name] # => ["is not present"]
Of course, because Subvalid only cares about duck-types, and not any particular
modelling framework, this validator works equally well with any type of object -
so long as it responds to name
class Person < ActiveRecord::Base
end
madlepAR = Person.create(name: "madlep")
PersonValidator.validate(madlepAR).valid? # => true
And you can validate nested data structures
Video = Struct.new(:title, :length, :author)
class VideoValidator
include Subvalid::Validator
validates :title, presence: true
validates :length, presence: true
validates :author do
validates :name, presence: true
end
end
invalid_video = Video.new(nil, nil, Person.new(nil))
result = VideoValidator.validate(video)
result.to_h # => {:title=>{:errors=>["is not present"]}, :length=>{:errors=>["is not present"]}, :author=>{:name=>{:errors=>["is not present"]}}}
Or you can DRY up your validation code by composing validators together
class VideoValidator
include Subvalid::Validator
validates :title, presence: true
validates :length, presence: true
validates :author, with: PersonValidator
end
Validator execution on specific fields can be run or skipped at validation time
by passing an if
validator proc, which decides if the validation should run
class PersonValidator
include Subvalid::Validator
validates :postcode, presence: true, if: -> (person) { person.country == "US" }
end
You can specify length constraints in different ways:
class PersonValidator
include Subvalid::Validator
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
The possible length constraint options are:
:minimum
- The attribute cannot have less than the specified length.
:maximum
- The attribute cannot have more than the specified length.
:in
(or :within
) - The attribute length must be included in a given interval. The value for this option must be a range.
:is
- The attribute length must be equal to the given value.
The default error messages depend on the type of length validation being performed. You can use the :message
option to specify an error message.
Note that the default error messages are plural. A personalised message should be provided in cases where it is grammatically incorrect, eg, "cannot be shorter than 1 characters".
- github project
- Bug reports and feature requests are via github issues
Subvalid
uses MIT license. See
LICENSE.txt
for
details.
We welcome contribution from everyone. Read more about it in
CODE_OF_CONDUCT.md
For bug fixes, documentation changes, and small features:
- Fork it ( https://github.com/subvalid/subvalid/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 a new Pull Request
For larger new features: Do everything as above, but first also make contact with the project maintainers to be sure your change fits with the project direction and you won't be wasting effort going in the wrong direction