PORO Validator
PoroValidator is a lightweight validation library for your Plain Old Ruby Objects (hence PoroValidator).
I always believed that validation is a seperate concern and should not be defined in the object that you are going to be validating. This validator library aims to seperate the validation to a seperate concern giving it great flexibility and scalability.
It is framework agnostic and can be used on any plain old ruby object/entities.
ActiveRecord/ActiveModel::Validations
While ActiveModel::Validations is great if you've got simple validation logic, it doesn't cut it for complex validations needs. When you have different validation for the same object at each point in it's life cycle, you need something more flexible.
The problem with ActiveModel::Validations is that it hooks pretty deep into ActiveRecord. The main use case for ActiveModel::Validations is to prevent bad data hitting your database - which isn't always the case (sometimes we want to allow bad data to go through). PoroValidator decouples your validation logic from your object structure. With PoroValidator you can define different validation rules for different contexts. So instead of objective validation where the validation is defined in the object you want to validate we define it in a seperate class making it subjective.
Features
- Familiar, simple and consistent API.
- Framework agnostic, all you need is a PORO.
- Validate Hash objects! Good for params, json objects what have you!
- Validation logic is decoupled from business logic by creating seperate validator classes which allows easy testing for the validator class.
- No magic, caller is in control.
- Invalid validations does not necessitate inability to persist to database.
- Conditional Validations via
:ifand:unless. - Nested validations for nested object structures - nicely handling nested errors.
- Composable validations by reusing existing validators.
- Create custom validators.
- Overrideable messages either via the
:messageoption or through the configuration. - Easily test validator classes.
Installation
Add this line to your application's Gemfile:
gem 'poro_validator'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install poro_validator
Usage
Creating and using a validator
# Create a validator
class CustomerValidator
include PoroValidator.validator
validates :last_name, presence: true
validates :first_name, presence: true
validates :age, numeric: { min: 18 }
end
validator = CustomerValidator.new
# Validate entity
customer = CustomerDetail.new
validator.valid?(customer) # => false
validator.errors. # => ["last name is not present", "..."]
# Validate hash
customer = {}
validator.valid?(customer) # => false
validator.errors. # => ["last name is not present", "..."]
Validators
Default Validators
- exclusion_validator
- float_validator
- format_validator
- inclusion_validator
- integer_validator
- length_validator
- numeric_validator
- presence_validator
- with_validator
Exclusion Validator
Option:
in:responds to an Array, Range or Set
validates :foo, exclusion: 5..10
validates :boo, exclusion: [1,2,3,4,5]
validates :zoo, exclusion: { in: [1,2,3,4,5] }
validates :moo, exclusion: 5..10, if: proc { false }
Float Validator
validates :foo, float: true
validates :boo, float: true, if: proc { false }
Format Validator
Option
withpass in regex or string
validates :foo, format: /[a-z]/
validates :boo, format: { with: /[a-z]/ }
Inclusion Validator
Option
validates :foo, inclusion: 1..10
validates :boo, inclusion: { in: [1,2,3,4,5] }
validates :zoo, inclusion: { in: 1..10 }
Integer Validator
validates :foo, integer: true
validates :boo, integer: true, if: proc { false }
Length Validator
Value must be either a string or can be casted as one
Options:
extremum:min and maxmin:minimummax:maximum
validates :foo, length: 1..10
validates :boo, length: { extremum: 1 }
validates :zoo, length: { min: 10, max: 20 }
validates :moo, length: { min: 10 }
validates :goo, length: { max: 10 }
validates :loo, length: 1..10, if: proc { false }
Numeric Validator
Value must be either an integer or can be casted as one
Options:
extremum:min and maxmin:minimummax:maximum
validates :foo, numeric: 1..10
validates :boo, numeric: { extremum: 5 }
validates :zoo, numeric: { min: 10, max: 20 }
validates :moo, numeric: { min: 10 }
validates :goo, numeric: { max: 20 }
validates :loo, numeric: 1..10, if: proc { false }
Presence Validator
validates :foo, presence: true
validates :boo, presence: true, if: proc { false }
With Validator
Options:
:withrequires an existing validator class to be passed
validates :foo, with: ExistingValidator
Custom Validators
Creating validators is easy! Just follow the example below!
module PoroValidator
module Validators
class CustomValidator < BaseClass
def validate(attribute, value, )
= [:message] || "custom validator message"
# your validation logic code goes here..
# add error message if validation fails
errors.add(attribute, , )
end
end
end
end
Note when creating custom validators
You can either define the error message in your validator like shown above or define it via the PoroValidator.configuration
Error Messages
#configure
The message configuration object, allows you to change the default error message produced by each validator. The message must be in the form of a lambda or Proc, and may or may not receive an argument. Use the example below for reference when customizing messages.
PoroValidator.configure do |config|
config..set(:numeric, lambda { "is not a number" })
config..set(:presence, lambda { "is not present" })
config..set(:inclusion, lambda { |set| "not in the set: #{set.inspect}")
...
end
#on method
The on method is used to acccess the error messages related to a key or attribute/method.
unnested validations
Pass in either a symbol or a string
validator.errors.on(:attribute) || validator.errors.on("attribute")
nested validations
Pass in a nested hash structure reflective of the object that was validated
validator.errors.on({address: :line1})
validator.errors.on({address: {city: :locality}})
validator.errors.on({address: {country: {coordinates: {planent: :name}}}})
Conditional Validations
You can pass in conditional
class CustomerValidator
include PoroValidator.validator
validates :last_name, presence: { unless: :foo_condition }
validates :first_name, presence: { if: lambda { true } }
validates :dob, presence: { if: :method_in_the_validator_class }
validates :age, presence: { if: :entity_method }
validates :address do
validates :line1, presence: { if: 'entity.nested_entity.method' }
end
Nested Validations
# validator
class CustomerDetailValidator
include PoroValidator.validator
validates :customer_id, presence: true
validates :customer do
validates :first_name, presence: true
validates :last_name, presence: true
end
validates :address do
validates :line1, presence: true
validates :line2, presence: true
validates :city, presence: true
validates :country do
validates :iso_code, presence: true
validates :short_name, presence: true
validates :coordinates do
validates :longtitude, presence: true
validates :latitude, presence:true
validates :planet do
validates :name, presence: true
end
end
end
end
end
entity = CustomerDetailEntity.new
validator = CustomerDetailValidator.new
validator.valid?(entity)
validator.errors. # => [
"customer_id is not present",
"{:customer=>:first_name} is not present",
"{:customer=>:last_name} is not present",
"{:address=>{:country=>{:coordinates=>:longtitude}}} is not present",
"{:address=>{:country=>{:coordinates=>:latitude}}} is not present",
"{:address=>{:country=>{:coordinates=>{:planet=>:name}}}} is not present"
]
Composable Validations
# Create a validators
class CustomerValidator
include PoroValidator.validator
validates :last_name, presence: true
validates :first_name, presence: true
validates :age, numeric: { min: 18 }
end
class AddressValidator
include PoroValidator.validator
validates :line1, presence: true
validates :lin2, presence: true
validates :city, presence: true
validates :state, presence: true
validates :zip_code, format: /[0-9]/
end
# Create another validator and use the existing validator class as an option
class CustomerValidator
include PoroValidator.validator
validates :customer, with: CustomerValidator
validates :address, with: AddressValidator
end
# Create an entities
class CustomerDetailEntity
attr_accessor :customer
attr_accessor :address
end
customer_detail = CustomerDetailEntity.new
# Validate entity
validator = CustomerValidator.new
validator.valid?(customer_detail) # => false
validator.errors. # => [
"customer" => "last name is not present", ".."
"address" => "line1 is not present", ".."
]
Maintainers
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/magicalbanana/poro_validator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Copyright
Copyright (c) 2015 Kareem Gan