Lotus::Validations
Validations mixins for objects
Status
Contact
- Home page: http://lotusrb.org
- Mailing List: http://lotusrb.org/mailing-list
- API Doc: http://rdoc.info/gems/lotus-validations
- Bugs/Issues: https://github.com/lotus/validations/issues
- Support: http://stackoverflow.com/questions/tagged/lotus-ruby
- Chat: https://gitter.im/lotus/chat
Rubies
Lotus::Validations supports Ruby (MRI) 2+, JRuby 9k+ & Rubinius 2.3+
Installation
Add this line to your application's Gemfile:
gem 'lotus-validations'
And then execute:
$ bundle
Or install it yourself as:
$ gem install lotus-validations
Usage
Lotus::Validations
is a set of lightweight validations for Ruby objects.
Attributes
The framework allows you to define attributes for each object.
It defines an initializer, whose attributes can be passed as a hash. All unknown values are ignored, which is useful for whitelisting attributes.
require 'lotus/validations'
class Person
include Lotus::Validations
attribute :name, presence: true
attribute :email, presence: true
end
person = Person.new(name: 'Luca', email: '[email protected]', age: 32)
person.name # => "Luca"
person.email # => "[email protected]"
person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
Blank Values
The framework will treat as valid any blank attributes, without presence
, for both format
and size
predicates.
require 'lotus/validations'
class Person
include Lotus::Validations
attribute :name, type: String, size: 5..45
attribute :email, type: String, size: 20..80, format: /@/
attribute :skills, type: Array, size: 1..3
attribute :keys, type: Hash, size: 1..3
end
Person.new.valid? # < true
Person.new(name: '').valid? # < true
Person.new(skills: '').valid? # < true
Person.new(skills: ['ruby', 'lotus']).valid? # < true
Person.new(skills: []).valid? # < false
Person.new(keys: {}).valid? # < false
Person.new(keys: {a: :b}, skills: []).valid? # < false
If you want to disable this behaviour, please, refer to presence.
Validations
If you prefer Lotus::Validations to only define validations, but not attributes, you can use the following alternative syntax.
require 'lotus/validations'
class Person
include Lotus::Validations
attr_accessor :name, :email
# Custom initializer
def initialize(attributes = {})
@name, @email = attributes.values_at(:name, :email)
end
validates :name, presence: true
validates :email, presence: true
end
person = Person.new(name: 'Luca', email: '[email protected]')
person.name # => "Luca"
person.email # => "[email protected]"
This is a bit more verbose, but offers a great level of flexibility for your Ruby objects. It also allows to use Lotus::Validations in combination with other frameworks.
Coercions
If a Ruby class is passed to the :type
option, the given value is coerced, accordingly.
Standard coercions
require 'lotus/validations'
class Person
include Lotus::Validations
attribute :fav_number, type: Integer
end
person = Person.new(fav_number: '23')
person.valid?
person.fav_number # => 23
Allowed types are:
Array
BigDecimal
Boolean
Date
DateTime
Float
Hash
Integer
Pathname
Set
String
Symbol
Time
Custom coercions
If a user defined class is specified, it can be freely used for coercion purposes. The only limitation is that the constructor should have arity of 1.
require 'lotus/validations'
class FavNumber
def initialize(number)
@number = number
end
end
class BirthDate
end
class Person
include Lotus::Validations
attribute :fav_number, type: FavNumber
attribute :date, type: BirthDate
end
person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
person.valid?
person.fav_number # => #<FavNumber:0x007ffc644bba00 @number="23">
person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
Validations
Each attribute definition can receive a set of options to define one or more validations.
Validations are triggered when you invoke #valid?
.
Acceptance
An attribute is valid if its value is truthy.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :terms_of_service, acceptance: true
end
signup = Signup.new(terms_of_service: '1')
signup.valid? # => true
signup = Signup.new(terms_of_service: 'true')
signup.valid? # => true
signup = Signup.new(terms_of_service: '')
signup.valid? # => false
signup = Signup.new(terms_of_service: '0')
signup.valid? # => false
Confirmation
An attribute is valid if its value and the value of a corresponding attribute is valid.
By convention, if you have a password
attribute, the validation looks for password_confirmation
.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :password, confirmation: true
end
signup = Signup.new(password: 'secret', password_confirmation: 'secret')
signup.valid? # => true
signup = Signup.new(password: 'secret', password_confirmation: 'x')
signup.valid? # => false
Exclusion
An attribute is valid, if the value isn't excluded from the value described by the validator.
The validator value can be anything that responds to #include?
.
In Ruby, this includes most of the core objects: String
, Enumerable
(Array
, Hash
,
Range
, Set
).
See also Inclusion.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :music, exclusion: ['pop']
end
signup = Signup.new(music: 'rock')
signup.valid? # => true
signup = Signup.new(music: 'pop')
signup.valid? # => false
Format
An attribute is valid if it matches the given Regular Expression.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :name, format: /\A[a-zA-Z]+\z/
end
signup = Signup.new(name: 'Luca')
signup.valid? # => true
signup = Signup.new(name: '23')
signup.valid? # => false
Inclusion
An attribute is valid, if the value provided is included in the validator's value.
The validator value can be anything that responds to #include?
.
In Ruby, this includes most of the core objects: like String
, Enumerable
(Array
, Hash
,
Range
, Set
).
See also Exclusion.
require 'prime'
require 'lotus/validations'
class PrimeNumbers
def initialize(limit)
@numbers = Prime.each(limit).to_a
end
def include?(number)
@numbers.include?(number)
end
end
class Signup
include Lotus::Validations
attribute :age, inclusion: 18..99
attribute :fav_number, inclusion: PrimeNumbers.new(100)
end
signup = Signup.new(age: 32)
signup.valid? # => true
signup = Signup.new(age: 17)
signup.valid? # => false
signup = Signup.new(fav_number: 23)
signup.valid? # => true
signup = Signup.new(fav_number: 8)
signup.valid? # => false
Presence
An attribute is valid if present.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :name, presence: true
end
signup = Signup.new(name: 'Luca')
signup.valid? # => true
signup = Signup.new(name: '')
signup.valid? # => false
signup = Signup.new(name: nil)
signup.valid? # => false
Size
An attribute is valid if its #size
falls within the described value.
require 'lotus/validations'
class Signup
MEGABYTE = 1024 ** 2
include Lotus::Validations
attribute :ssn, size: 11 # exact match
attribute :password, size: 8..64 # range
attribute :avatar, size: 1..(5 * MEGABYTE)
end
signup = Signup.new(password: 'a-very-long-password')
signup.valid? # => true
signup = Signup.new(password: 'short')
signup.valid? # => false
Note that in the example above you are able to validate the weight of the file,
because Ruby's File
and Tempfile
both respond to #size
.
Uniqueness
Uniqueness validations aren't implemented because this library doesn't deal with persistence. The other reason is that this isn't an effective way to ensure uniqueness of a value in a database.
Please read more at: The Perils of Uniqueness Validations.
Nested validations
Nested validations are handled with a nested block syntax.
class ShippingDetails
include Lotus::Validations
attribute :full_name, presence: true
attribute :address do
attribute :street, presence: true
attribute :city, presence: true
attribute :country, presence: true
attribute :postal_code, presence: true, format: /.../
end
end
validator = ShippingDetails.new
validator.valid? # => false
Bulk operations on errors are guaranteed by #each
.
This method yields a flattened collection of errors.
validator.errors.each do |error|
error.name
# => on the first iteration it returns "full_name"
# => the second time it returns "address.street" and so on..
end
Errors for a specific attribute can be accessed via #for
.
error = validator.errors.for('full_name').first
error.name # => "full_name"
error.attribute_name # => "full_name"
error = validator.errors.for('address.street').first
error.name # => "address.street"
error.attribute_name # => "street"
Composable validations
Validations can be reused via composition:
require 'lotus/validations'
module NameValidations
include Lotus::Validations
attribute :name, presence: true
end
module EmailValidations
include Lotus::Validations
attribute :email, presence: true, format: /.../
end
module PasswordValidations
include Lotus::Validations
# We validate only the presence here
attribute :password, presence: true
end
module CommonValidations
include EmailValidations
include PasswordValidations
end
# A valid signup requires:
# * name (presence)
# * email (presence and format)
# * password (presence and confirmation)
class Signup
include NameValidations
include CommonValidations
# We decorate PasswordValidations behavior, by requiring the confirmation too.
# This additional validation is active only in this case.
attribute :password, confirmation: true
end
# A valid signin requires:
# * email (presence)
# * password (presence)
class Signin
include CommonValidations
end
# A valid "forgot password" requires:
# * email (presence)
class ForgotPassword
include EmailValidations
end
Complete example
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :first_name, presence: true
attribute :last_name, presence: true
attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
attribute :password, presence: true, confirmation: true, size: 8..64
end
Errors
When you invoke #valid?
, validation errors are available at #errors
.
It's a set of errors grouped by attribute. Each error contains the name of the
invalid attribute, the failed validation, the expected value, and the current one.
require 'lotus/validations'
class Signup
include Lotus::Validations
attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
attribute :age, size: 18..99
end
signup = Signup.new(email: '[email protected]')
signup.valid? # => true
signup = Signup.new(email: '', age: 17)
signup.valid? # => false
signup.errors
# => #<Lotus::Validations::Errors:0x007fe00ced9b78
# @errors={
# :email=>[
# #<Lotus::Validations::Error:0x007fe00cee3290 @attribute=:email, @validation=:presence, @expected=true, @actual="">,
# #<Lotus::Validations::Error:0x007fe00cee31f0 @attribute=:email, @validation=:format, @expected=/\A(.*)@(.*)\.(.*)\z/, @actual="">
# ],
# :age=>[
# #<Lotus::Validations::Error:0x007fe00cee30d8 @attribute=:age, @validation=:size, @expected=18..99, @actual=17>
# ]
# }>
Lotus::Entity
Integration with Lotus::Entity
is straight forward.
require 'lotus/model'
require 'lotus/validations'
class Product
include Lotus::Entity
include Lotus::Validations
attribute :name, type: String, presence: true
attribute :price, type: Integer, presence: true
end
product = Product.new(name: 'Book', price: '100')
product.valid? # => true
product.name # => "Book"
product.price # => 100
Contributing
- Fork it ( https://github.com/lotus/lotus-validations/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
Copyright
Copyright © 2014-2016 Luca Guidi – Released under MIT License