Validations
CQL provides a comprehensive validation system that ensures data integrity before records are saved to the database. The validation system is built on a predicate-based approach that leverages Crystal's type system for compile-time safety.
Basic Validation Syntax
Define validations using the validate
macro with field names and validation predicates:
class User
include CQL::ActiveRecord::Model(Int32)
db_context UserDB, :users
property id : Int32?
property name : String
property email : String
property age : Int32 = 0
property password : String?
@[DB::Field(ignore: true)]
property password_confirmation : String?
# Define validations with predicates and custom messages
validate :name, presence: true, size: 2..50, message: "Name is invalid"
validate :email, required: true, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, message: "Email format is invalid"
validate :age, gt: 0, lt: 120, message: "Age must be between a reasonable range"
validate :password_confirmation, presence: true, message: "Password confirmation is required"
def initialize(@name : String, @email : String, @age : Int32 = 0, @password : String? = nil, @password_confirmation : String? = nil)
end
end
Available Validation Predicates
CQL provides a rich set of built-in validation predicates:
Presence and Required
presence: true
: Ensures the field is not nil and not emptyrequired: true
: Ensures the field is not nil
validate :name, presence: true # Must not be nil or empty
validate :user_id, required: true # Must not be nil (but can be empty)
Numeric Comparisons
gt: value
: Greater thangte: value
: Greater than or equal tolt: value
: Less thanlte: value
: Less than or equal toeq: value
: Equal to
validate :age, gt: 0, lt: 150 # Age between 1 and 149
validate :score, gte: 0, lte: 100 # Score between 0 and 100
validate :priority, eq: 1 # Exact value match
Size Validations
size: number
: Exact sizesize: range
: Size within range
validate :username, size: 3..20 # Between 3 and 20 characters
validate :zip_code, size: 5 # Exactly 5 characters
Pattern Matching
match: regex
: Must match regular expression
validate :email, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validate :phone, match: /\A\d{3}-\d{3}-\d{4}\z/
Inclusion and Exclusion
in: array
: Value must be in the arrayin: range
: Value must be in the rangeexclude: array
: Value must not be in the arrayexclude: range
: Value must not be in the range
validate :status, in: ["active", "inactive", "pending"]
validate :rating, in: 1..5
validate :username, exclude: ["admin", "root", "system"]
Custom Messages
Provide custom error messages for better user experience:
class Product
include CQL::ActiveRecord::Model(Int32)
db_context StoreDB, :products
property id : Int32?
property name : String
property price : Float64
property category : String
validate :name, presence: true, message: "Product name cannot be blank"
validate :price, gt: 0, message: "Price must be greater than zero"
validate :category, in: ["electronics", "books", "clothing"], message: "Invalid product category"
def initialize(@name : String, @price : Float64, @category : String)
end
end
Working with Validation Errors
Checking if a Record is Valid
user = User.new("", "invalid-email", -5)
# Check validity
if user.valid?
puts "User is valid"
else
puts "User has validation errors"
end
Accessing Validation Errors
user = User.new("", "invalid-email", -5)
unless user.valid?
errors = user.errors
# Get all error messages
error_messages = errors.map(&.message)
puts error_messages
# => ["Name is invalid", "Email format is invalid", "Age must be between a reasonable range"]
# Access individual errors
errors.each do |error|
puts "Field: #{error.field}, Message: #{error.message}"
end
end
Validating with Context
You can validate with specific contexts for different scenarios:
class User
include CQL::ActiveRecord::Model(Int32)
# Validations can be context-specific
validate :password, presence: true, on: :create
validate :current_password, presence: true, on: :update
end
user = User.new("John", "john@example.com")
# Validate for specific context
user.valid?(:create) # Checks password presence
user.valid?(:update) # Checks current_password presence
user.valid? # Runs all validations regardless of context
Validation Exceptions
Force validation and raise an exception if invalid:
user = User.new("", "invalid-email", -5)
begin
user.validate! # Raises ValidationError if invalid
rescue CQL::ActiveRecord::Validations::ValidationError => e
puts "Validation failed: #{e.message}"
# Message contains comma-separated error messages
end
Custom Validators
Create custom validators for complex validation logic:
class PasswordValidator < CQL::ActiveRecord::Validations::CustomValidator
def valid? : Array(CQL::ActiveRecord::Validations::Error)
errors = [] of CQL::ActiveRecord::Validations::Error
password = @record.password
# Check minimum length
unless password && password.size >= 8
errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must be at least 8 characters")
end
# Check for mixed case
unless password && password.matches?(/[a-z]/) && password.matches?(/[A-Z]/)
errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must contain both uppercase and lowercase letters")
end
# Check for numbers
unless password && password.matches?(/\d/)
errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must contain at least one number")
end
errors
end
end
class User
include CQL::ActiveRecord::Model(Int32)
db_context UserDB, :users
property id : Int32?
property name : String
property email : String
property password : String?
# Use custom validator
use PasswordValidator
validate :name, presence: true, size: 2..50, message: "Name must be between 2 and 50 characters"
def initialize(@name : String, @email : String, @password : String? = nil)
end
end
Validation Best Practices
1. Use Appropriate Predicates
# Good - specific predicates
validate :age, gt: 0, lt: 120
validate :email, required: true, match: EMAIL_REGEX
# Avoid - overly complex custom validators for simple cases
validate :age, custom: :validate_age_range
2. Provide Clear Error Messages
# Good - descriptive messages
validate :email, required: true, message: "Email address is required for account creation"
# Avoid - generic messages
validate :email, required: true, message: "Invalid"
3. Use Context-Specific Validations
class User
# Validations that only apply during creation
validate :password, presence: true, on: :create
# Validations that only apply during updates
validate :current_password, presence: true, on: :update
# Validations that always apply
validate :email, required: true, match: EMAIL_REGEX
end
4. Test Validations Thoroughly
describe User do
it "validates required fields" do
user = User.new("", "", nil)
user.valid?.should be_false
user.errors.map(&.field).should contain(:name)
user.errors.map(&.field).should contain(:email)
end
it "validates email format" do
user = User.new("John", "invalid-email", "password")
user.valid?.should be_false
user.errors.map(&.message).should contain("Email format is invalid")
end
it "allows valid data" do
user = User.new("John Doe", "john@example.com", "password123")
user.valid?.should be_true
end
end
Related Features
Active Record Models - How to define models with validations
Callbacks - Lifecycle hooks that work with validations
CRUD Operations - How validations integrate with save operations
Error Handling - Working with validation errors
Last updated
Was this helpful?