Validation
Here is an example of validation usage:
class User < Jennifer::Model::Base
mapping({
# ...
login: String
})
validates_length :login, in: 8..16
end
User.create({login: "login"}).valid? # => false
User.create({login: "loginlogin"}).valid? # => true
As you can see, our validation lets us know that our User
is not valid with a too short login attribute. Once we set long enough value User
will not be persisted to the database.
Trigger validation
The following model methods triggers validations in a scope of own execution:
.create
.create!
#validate
#validate!
#valid?
#save
#save_without_transaction
#save!
#update
#update!
NOTE: Bang method version will raise an exception if record is invalid.
NOTE:
#valid?
is an alias for#validate!
.
Jennifer::Query#patch
also invokes validation (and callbacks) for each matched record.
#validate!
method invokes validation callbacks. Be aware: after_validation
callbacks may not be triggered if record is invalid or before_validation
has raised Jennifer::Skip
exception.
Skip validation
Some methods skip validation on invocation:
Model::Base#invalid?
Model::Base#save(skip_validation: true)
Model::Base#update_column
Model::Base#update_columns
QueryBuilder::Query#update
QueryBuilder::Query#increment
QueryBuilder::Query#decrement
NOTE:
#invalid?
method will only check if#errors
is empty.
Validation macros
Please take into account that all validators described below implement singleton pattern - there is only one instance of each of them in application.
validates_acceptance
This macro validates that a given field equals to true or be one of given values. This is useful for validating checkbox value:
class User < Jennifer::Model::Base
mapping(
# ...
)
property terms_of_service = false
property eula : String?
validates_acceptance :terms_of_service
validates_acceptance :eula, accept: %w(true accept yes)
end
By default "1"
and true
is recognized as accepted values, but, as described, this behavior could be override by passing accept
option with array.
validates_confirmation
This validation helps to check if confirmation field was filled with same value as specified.
class User < Jennifer::Model::Base
mapping(
# ...
email: String?,
address: String?
)
property email_confirmation : String?, address_confirmation : String?
validates_confirmation :email
validates_confirmation :address, case_insensitive: true
end
If confirmation is nil - this validation will be skipped. Such behavior allows to normally proceed in places where this validation is not needed (e.g. email confirmation is important only during new user creating).
To make comparison case insensitive - specify second argument as true
.
validates_exclusion
This macro validates that the attribute’s value aren’t included in a given set. This could be any object which responds to #includes?
method.
class Country < Jennifer::Base::Model
mapping(
# ...
code: String
)
validates_exclusion :code, in: %w(AA DD)
end
validates_format
This macro validates that the attribute’s value satisfies given regular expression.
class Contact < Jennifer::Model::Base
mapping(
# ...
street: String
)
validates_format :street, /st\.|street/i
end
validates_inclusion
This macro validates that the attribute’s value are included in the set. This could be any object which responds to #includes?
method.
class User < Jennifer::Base::Model
mapping(
# ...
country_code: String
)
validates_inclusion :code, in: Country::KNOWN_COUNTRIES
end
validates_length
This macro validates the attribute’s value length. There are a lot of options so constraint can be specified in different ways.
class User < Jennifer::Model::Base
mapping(
# ...
)
validates_length :name, minimum: 2
validates_length :login, in: 4..16
validates_length :uid, is: 16
end
The possible constraints are:
minimum
- length can’t be less than specified one,maximum
- length can’t be greater than specified on,in
- length must be included in given intervalis
- length must be same as specified
validates_numericality
This macro validates if given number field satisfies specified constraints.
class Player < Jennifer::Model::Base
mapping(
# ...
points: Float64?
)
validates_numericality :points, greater_than: 0
end
This macro accepts following constraints:
greater_than
greater_than_or_equal_to
equal_to
less_than
less_than_or_equal_to
other_than
odd
even
validates_presence
This macro validates that attribute’s value is not empty. It uses #blank?
method from the core pact of Ifrit.
class User < Jennifer::Model::Base
mapping(
# ...
email: String?
)
validates_presence :email
end
validates_absence
This validates that attribute’s value is blank. It uses #blank?
method from Ifrit as well as presence
validation.
class SuperUser < User
mapping(
# ...
)
validates_absence :title
end
validates_uniqueness
This validates that the attribute’s value is unique right before object gets validated. It doesn’t create any db constraint so it doesn’t totally guaranty that another another application instance creates record with save value in overlapping time. Don’t use this validation for sensitive data. On the other hand this could help in generating readable error messages.
class Country < Jennifer::Model::Base
mapping(
# ...
code: String
)
validates_uniqueness :code
end
NOTE: Be aware that mysql performs case insensitive string comparison.
validates_with
This passes the record to a new instance of given validator class to be validated.
class EnnValidator < Jennifer::Validations::Validator
def validate(record, **opts)
if record.enn!.size < 4 && record.enn![0].downcase == 'a'
record.errors.add(:enn, "Invalid enn")
end
end
end
class Passport < Jennifer::Model::Base
mapping(
# ...
enn: {type: String, primary: true}
)
validates_with EnnValidator
end
validates_with_method
This invokes specified record method to perform validation
class User < Jennifer::Model::Base
mapping(
id: Primary?
)
validates_with_method :thirteen
def thirteen
if id == 13
errors.add(:id, "Can't be 13")
end
end
end
if
validation option
Sometimes it will make sense to validate an object only when a given predicate is satisfied. You can do that by using the :if option, which can take a symbol or an expression. You may use the :if option when you want to specify when the validation should happen.
The symbol value of :if options corresponds to the method name that will get called right before validation happens.
class Player < Jennifer::Model::Base
mapping(
# ...
health: Float64,
live_creature: {type: Bool, default: true, virtual: true}
)
validates_numericality :health, greater_than: 0, if: :live_creature
end
An expression may be used to simulate unless behavior of simple condition without wrapping it into a method.
class Player < Jennifer::Model::Base
mapping(
# ...
health: Float64,
undead: {type: Bool, default: false, virtual: true}
)
validates_numericality :health, greater_than: 0, if: !undead
end
Common validation options
These are common validation options:
allow_blank
This option skip validation if attribute’s value is nil
. All validation methods accepts this except following:
uniqueness
presence
absence
acceptance
confirmation
By default it is set to false
.
if
Sometimes it will make sense to validate an object only when a given predicate is satisfied. You can do that by using the :if option, which can take a symbol or an expression. You may use the :if option when you want to specify when the validation should happen.
The symbol value of :if options corresponds to the method name that will get called right before validation happens.
class Player < Jennifer::Model::Base
mapping(
# ...
health: Float64,
live_creature: {type: Bool, default: true, virtual: true}
)
validates_numericality :health, greater_than: 0, if: :live_creature
end
An expression may be used to simulate unless behavior of simple condition without wrapping it into a method.
class Player < Jennifer::Model::Base
mapping(
# ...
health: Float64,
undead: {type: Bool, default: false, virtual: true}
)
validates_numericality :health, greater_than: 0, if: !undead
end
message
the message
option lets you specify the message that will be added to the errors collection when validation fails. When this option is not used,respective default error message for each validation is used. The message
option accepts a String
, Symbol
or Proc
.
A String
message
value is used as a validation error message as-is.
A Proc
message
value is given two arguments: the object being validated and a field name.
A Symbol
message
is used as a message name and Jennifer will try to find it for model-attribute combination using default message resolving hierarchy. For more information about this read Internationalization section.
class Person < Jennifer::Model::Base
mapping(
id: Primary64,
name: String?,
age: Int32?,
username: String?
)
# Hard-coded message
validates_presence :name, message: "must be given please"
# Message i18n name.
validates_numericality :age, greater_than: 18, message: :invalid
# Proc
validates_uniqueness :username,
message: ->(object : Jennifer::Model::Translation, field : String) do
record = object.as(Person)
"Hey #{record.name}, #{record.attribute(field)} is already taken."
end
end
Custom validation
Custom validators
Custom validators are classes that inherit from Jennifer::Validations::Validator
and implement #validate
method.
class Passport < Jennifer::Model::Base
mapping(
enn: {type: String, primary: true}
)
validates_with EnnValidator
end
class EnnValidator < Jennifer::Validations::Validator
def validate(record)
if record.enn!.size < 4 && record.enn![0].downcase == 'a'
record.errors.add(:enn, "Invalid enn")
end
end
end
If there is any named argument specified after validator class - it will be passed to #validate
method as well.
class Passport < Jennifer::Model::Base
mapping(
enn: {type: String, primary: true}
)
validates_with EnnValidator, length: 6
end
class EnnValidator < Jennifer::Validator
def validate(record, length)
if record.enn!.size < length && record.enn![0].downcase == 'a'
record.errors.add(:enn, "Invalid enn")
end
end
end
To override default singleton behavior of validator define .instance
method this way:
class CustomValidator < Jennifer::Validations::Validator
def self.instance
new
end
def validate(record)
end
end
Accessing errors list
Each record has a container to hold error messages - Accord::ErrorList
. To retrieve it use #errors
method.
By it own #errors
doesn’t trigger validation so first of all you need to perform it explicitly by listed upper methods or using #validate!
method:
user = User.new(login: "login")
user.errors.any? # false
user.validate!
user.errors.any? # true
errors[]
To check whether or not a particular attribute of an object is valid you can use #errors[:attribute]
. It returns an array of error messages for :attribute
. If there is no error - empty array will be returned.
#errors.add
This methods let you add an error message related to a particular attribute. It takes as arguments tre attribute and the error message.
user = User.create(login: "login")
user.errors.add(:login, "Some custom message")
errors.clear!
The #clear!
method is used when all error messages should be removed. It is automatically invoked by #validate!
.
Non-model usage
It is possible to use Jennifer::Model::Errors
for handling errors of any class that includes Jennifer::Model::Translation
and implements .superclass
method.
class Post
include Jennifer::Model::Translation
property title : String?
getter errors
def initialize
@errors = Jennifer::Model::Errors.new(self)
end
def validate
errors.clear
errors.add(:title, :blank) if title.nil?
end
# The following method is needed to be minimally implemented
def self.superclass; end
end
post = Post.new
post.validate
post.errors[:title] # "can't be blank"