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 EmailValidatorThis creates:
- src/validators/email_validator.cr- The validator class
- spec/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 urlGenerated 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
endTest 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
endValidator 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
endValidator 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
endComplex 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
endAsync 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
endUsing 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)
    ]
endIn 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)
endCreating 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
end2. 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
end3. 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
end4. 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
endTesting 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
endIntegration 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
endCommon 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
end2. 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
end3. 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
end4. 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
endRelated Commands
- azu generate contract- Generate validation contracts
- azu generate model- Generate data models
- azu generate service- Generate business logic services
- azu generate endpoint- Generate API endpoints
Templates
The custom validator generator supports different templates:
- basic- Simple validator with basic structure
- format- Format validation template
- range- Range validation template
- uniqueness- Uniqueness validation template
- conditional- Conditional validation template
To use a specific template:
azu generate custom_validator PhoneValidator --template formatLast updated