Custom Validator Generator
The Custom Validator Generator creates reusable validation components that can be used across multiple contracts and models in your Azu application.
Usage
azu generate custom_validator VALIDATOR_NAME [OPTIONS]
Description
Custom validators in Azu applications provide a way to create reusable validation logic that can be shared across different contracts and models. They encapsulate complex validation rules and can be easily tested and maintained.
Options
VALIDATOR_NAME
- Name of the validator to generate (required)-d, --description DESCRIPTION
- Description of the validator-t, --template TEMPLATE
- Template to use (default: basic)-f, --force
- Overwrite existing files-h, --help
- Show help message
Examples
Generate a basic custom validator
azu generate custom_validator EmailValidator
This creates:
src/validators/email_validator.cr
- The validator classspec/validators/email_validator_spec.cr
- Test file
Generate a validator with description
azu generate custom_validator PhoneValidator --description "Validates phone numbers in various formats"
Generate specific validator types
azu generate custom_validator PasswordValidator --template password
azu generate custom_validator UrlValidator --template url
Generated Files
Validator Class (src/validators/VALIDATOR_NAME.cr
)
src/validators/VALIDATOR_NAME.cr
)# <%= @description || @name.underscore.humanize %> custom validator
class <%= @name %>Validator < Azu::Validator
def initialize
end
def validate(value : String, context : Azu::ValidationContext) : Bool
# Add your validation logic here
# Example:
# return false if value.nil?
# return value.match(/^[^@]+@[^@]+\.[^@]+$/) != nil
true
end
def error_message : String
"<%= @name.underscore.humanize %> is invalid"
end
end
Test File (spec/validators/VALIDATOR_NAME_spec.cr
)
spec/validators/VALIDATOR_NAME_spec.cr
)require "../spec_helper"
describe <%= @name %>Validator do
describe "#validate" do
it "validates correctly" do
validator = <%= @name %>Validator.new
context = Azu::ValidationContext.new
# Add your test cases here
# validator.validate("test", context).should be_true
end
end
end
Validator Patterns
Basic Validator Pattern
class EmailValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
value.match(/^[^@]+@[^@]+\.[^@]+$/) != nil
end
def error_message : String
"must be a valid email address"
end
end
Validator with Options
class LengthValidator < Azu::Validator
def initialize(@min_length : Int32? = nil, @max_length : Int32? = nil)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return true if value.nil?
length = value.size
if @min_length && length < @min_length
return false
end
if @max_length && length > @max_length
return false
end
true
end
def error_message : String
if @min_length && @max_length
"must be between #{@min_length} and #{@max_length} characters"
elsif @min_length
"must be at least #{@min_length} characters"
elsif @max_length
"must be no more than #{@max_length} characters"
else
"length is invalid"
end
end
end
Complex Validator Pattern
class PasswordValidator < Azu::Validator
def initialize(
@min_length : Int32 = 8,
@require_uppercase : Bool = true,
@require_lowercase : Bool = true,
@require_numbers : Bool = true,
@require_special : Bool = false
)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.size < @min_length
if @require_uppercase && !value.match(/[A-Z]/)
return false
end
if @require_lowercase && !value.match(/[a-z]/)
return false
end
if @require_numbers && !value.match(/\d/)
return false
end
if @require_special && !value.match(/[!@#$%^&*(),.?":{}|<>]/)
return false
end
true
end
def error_message : String
requirements = [] of String
requirements << "at least #{@min_length} characters"
requirements << "uppercase letter" if @require_uppercase
requirements << "lowercase letter" if @require_lowercase
requirements << "number" if @require_numbers
requirements << "special character" if @require_special
"must contain #{requirements.join(", ")}"
end
end
Async Validator Pattern
class UniqueEmailValidator < Azu::Validator
def initialize(@model_class : Class)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return true if value.nil? || value.empty?
# Check if email already exists in database
existing_user = @model_class.find_by(email: value)
# If updating, exclude current record
if context.record_id
existing_user = nil if existing_user.try(&.id) == context.record_id
end
existing_user.nil?
end
def error_message : String
"email is already taken"
end
end
Using Custom Validators
In Contracts
class UserContract < Azu::Contract
field :email, String,
required: true,
validators: [EmailValidator.new]
field :password, String,
required: true,
validators: [PasswordValidator.new(min_length: 8, require_special: true)]
field :username, String,
required: true,
validators: [
LengthValidator.new(min_length: 3, max_length: 20),
UniqueUsernameValidator.new(User)
]
end
In Models
class User < CQL::Model
table :users
column :email, String
column :password, String
column :username, String
validates :email, presence: true, validator: EmailValidator.new
validates :password, presence: true, validator: PasswordValidator.new
validates :username, presence: true, validator: UniqueUsernameValidator.new(User)
end
Creating Validator Instances
# Basic usage
email_validator = EmailValidator.new
# With options
password_validator = PasswordValidator.new(
min_length: 10,
require_uppercase: true,
require_lowercase: true,
require_numbers: true,
require_special: true
)
# With model reference
unique_email_validator = UniqueEmailValidator.new(User)
Best Practices
1. Keep Validators Focused
Each validator should validate one specific thing:
# Good: Focused on email format
class EmailValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
value.match(/^[^@]+@[^@]+\.[^@]+$/) != nil
end
end
# Good: Focused on uniqueness
class UniqueEmailValidator < Azu::Validator
def initialize(@model_class : Class)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
# Check uniqueness logic
end
end
2. Provide Clear Error Messages
class PhoneValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
value.match(/^\+?[\d\s\-\(\)]+$/) != nil
end
def error_message : String
"must be a valid phone number (e.g., +1-555-123-4567)"
end
end
3. Handle Edge Cases
class UrlValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return true if value.nil? || value.empty? # Allow optional URLs
begin
URI.parse(value)
true
rescue URI::Error
false
end
end
def error_message : String
"must be a valid URL"
end
end
4. Use Type Safety
class AgeValidator < Azu::Validator
def initialize(@min_age : Int32 = 0, @max_age : Int32 = 150)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
age = value.to_i?
return false unless age
age >= @min_age && age <= @max_age
end
def error_message : String
"must be between #{@min_age} and #{@max_age} years old"
end
end
Testing Validators
Unit Testing
describe EmailValidator do
describe "#validate" do
it "validates correct email addresses" do
validator = EmailValidator.new
context = Azu::ValidationContext.new
validator.validate("user@example.com", context).should be_true
validator.validate("test.email+tag@domain.co.uk", context).should be_true
end
it "rejects invalid email addresses" do
validator = EmailValidator.new
context = Azu::ValidationContext.new
validator.validate("invalid-email", context).should be_false
validator.validate("user@", context).should be_false
validator.validate("@domain.com", context).should be_false
validator.validate("", context).should be_false
validator.validate(nil, context).should be_false
end
end
describe "#error_message" do
it "returns appropriate error message" do
validator = EmailValidator.new
validator.error_message.should eq("must be a valid email address")
end
end
end
Integration Testing
describe "Validator integration" do
it "works with contracts" do
contract = UserContract.new({
"email" => "invalid-email",
"password" => "weak"
})
contract.valid?.should be_false
contract.errors_for(:email).should contain("must be a valid email address")
contract.errors_for(:password).should contain("must contain")
end
end
Common Validator Types
1. Format Validators
Validate data format:
class PhoneValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
value.match(/^\+?[\d\s\-\(\)]+$/) != nil
end
end
class PostalCodeValidator < Azu::Validator
def validate(value : String, context : Azu::ValidationContext) : Bool
return false if value.nil? || value.empty?
value.match(/^\d{5}(-\d{4})?$/) != nil
end
end
2. Range Validators
Validate numeric ranges:
class RangeValidator < Azu::Validator
def initialize(@min : Float64? = nil, @max : Float64? = nil)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return true if value.nil? || value.empty?
number = value.to_f?
return false unless number
if @min && number < @min
return false
end
if @max && number > @max
return false
end
true
end
end
3. Uniqueness Validators
Validate database uniqueness:
class UniqueValidator < Azu::Validator
def initialize(@model_class : Class, @column : String)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return true if value.nil? || value.empty?
query = @model_class.where({@column => value})
if context.record_id
query = query.where.not({id: context.record_id})
end
query.first.nil?
end
end
4. Conditional Validators
Validate based on conditions:
class ConditionalValidator < Azu::Validator
def initialize(@condition : Proc(Azu::ValidationContext, Bool), @validator : Azu::Validator)
end
def validate(value : String, context : Azu::ValidationContext) : Bool
return true unless @condition.call(context)
@validator.validate(value, context)
end
def error_message : String
@validator.error_message
end
end
Related Commands
azu generate contract
- Generate validation contractsazu generate model
- Generate data modelsazu generate service
- Generate business logic servicesazu generate endpoint
- Generate API endpoints
Templates
The custom validator generator supports different templates:
basic
- Simple validator with basic structureformat
- Format validation templaterange
- Range validation templateuniqueness
- Uniqueness validation templateconditional
- Conditional validation template
To use a specific template:
azu generate custom_validator PhoneValidator --template format
Last updated