Contract Generator
The Contract Generator creates validation contracts that define the structure and validation rules for incoming data in your Azu application.
Usage
azu generate contract CONTRACT_NAME [OPTIONS]
Description
Contracts in Azu applications provide a way to validate and structure incoming data from HTTP requests, API calls, or form submissions. They ensure data integrity and provide clear error messages when validation fails.
Options
CONTRACT_NAME
- Name of the contract to generate (required)-d, --description DESCRIPTION
- Description of the contract-f, --fields FIELDS
- Comma-separated list of fields with types and validations-t, --template TEMPLATE
- Template to use (default: basic)-f, --force
- Overwrite existing files-h, --help
- Show help message
Examples
Generate a basic contract
azu generate contract UserContract
This creates:
src/contracts/user_contract.cr
- The contract classspec/contracts/user_contract_spec.cr
- Test file
Generate a contract with fields
azu generate contract UserContract --fields "name:string:required,email:string:required:email,age:integer:min:18"
Generate a contract with description
azu generate contract PostContract --description "Validates blog post creation and updates"
Generated Files
Contract Class (src/contracts/CONTRACT_NAME.cr
)
src/contracts/CONTRACT_NAME.cr
)# <%= @description || @name.underscore.humanize %> contract for data validation
class <%= @name %>Contract < Azu::Contract
# Define your contract fields here
# Example:
# field :name, String, required: true
# field :email, String, required: true, format: /^[^@]+@[^@]+\.[^@]+$/
# field :age, Int32, min: 18, max: 120
# Custom validation methods
# def validate_custom_rule
# # Add custom validation logic
# end
end
Test File (spec/contracts/CONTRACT_NAME_spec.cr
)
spec/contracts/CONTRACT_NAME_spec.cr
)require "../spec_helper"
describe <%= @name %>Contract do
describe "#valid?" do
it "validates required fields" do
contract = <%= @name %>Contract.new
# Add your test cases here
# contract.valid?.should be_true
end
end
end
Contract Patterns
Basic Contract Pattern
class UserContract < Azu::Contract
field :name, String, required: true, min_length: 2, max_length: 50
field :email, String, required: true, format: /^[^@]+@[^@]+\.[^@]+$/
field :age, Int32, min: 18, max: 120
field :bio, String?, max_length: 500
end
Contract with Custom Validations
class RegistrationContract < Azu::Contract
field :email, String, required: true, format: /^[^@]+@[^@]+\.[^@]+$/
field :password, String, required: true, min_length: 8
field :password_confirmation, String, required: true
def validate_password_confirmation
return unless password && password_confirmation
unless password == password_confirmation
errors.add(:password_confirmation, "must match password")
end
end
def validate_unique_email
return unless email
if User.find_by(email: email)
errors.add(:email, "is already taken")
end
end
end
Nested Contract Pattern
class AddressContract < Azu::Contract
field :street, String, required: true
field :city, String, required: true
field :postal_code, String, required: true
end
class UserContract < Azu::Contract
field :name, String, required: true
field :email, String, required: true
field :address, AddressContract
end
Array Contract Pattern
class TagContract < Azu::Contract
field :name, String, required: true, max_length: 20
end
class PostContract < Azu::Contract
field :title, String, required: true, max_length: 200
field :content, String, required: true
field :tags, Array(TagContract), max_size: 10
end
Field Types and Validations
Supported Field Types
class ExampleContract < Azu::Contract
# Basic types
field :string_field, String
field :integer_field, Int32
field :float_field, Float64
field :boolean_field, Bool
field :time_field, Time
# Optional types (can be nil)
field :optional_string, String?
field :optional_integer, Int32?
# Array types
field :string_array, Array(String)
field :integer_array, Array(Int32)
# Nested contracts
field :nested_contract, NestedContract
field :nested_contract_array, Array(NestedContract)
end
Common Validations
class ValidationContract < Azu::Contract
# Required fields
field :required_field, String, required: true
# String validations
field :name, String,
required: true,
min_length: 2,
max_length: 50,
format: /^[a-zA-Z\s]+$/
# Numeric validations
field :age, Int32,
required: true,
min: 0,
max: 150
field :price, Float64,
required: true,
min: 0.0,
max: 10000.0
# Array validations
field :tags, Array(String),
max_size: 10,
min_size: 1
# Custom validation
field :custom_field, String, required: true
end
Using Contracts
In Controllers
class UsersController < ApplicationController
def create
contract = UserContract.new(params.to_h)
if contract.valid?
user = User.create(contract.valid_data)
render json: user, status: :created
else
render json: {errors: contract.errors}, status: :unprocessable_entity
end
end
def update
contract = UserContract.new(params.to_h)
if contract.valid?
user = User.find(params["id"])
user.update(contract.valid_data)
render json: user
else
render json: {errors: contract.errors}, status: :unprocessable_entity
end
end
end
In Services
class UserService
def create_user(data : Hash) : User
contract = UserContract.new(data)
unless contract.valid?
raise InvalidUserDataError.new(contract.errors)
end
User.create(contract.valid_data)
end
end
Accessing Validated Data
contract = UserContract.new(params.to_h)
if contract.valid?
# Access individual fields
name = contract.name
email = contract.email
# Access all valid data as hash
user_data = contract.valid_data
# Access specific field with type safety
age = contract.age.try(&.to_i) || 0
end
Error Handling
Accessing Validation Errors
contract = UserContract.new(params.to_h)
unless contract.valid?
# Get all errors
all_errors = contract.errors
# Get errors for specific field
name_errors = contract.errors_for(:name)
# Check if field has errors
if contract.has_errors_for?(:email)
# Handle email errors
end
# Get first error for field
first_name_error = contract.first_error_for(:name)
end
Custom Error Messages
class CustomContract < Azu::Contract
field :email, String,
required: true,
format: /^[^@]+@[^@]+\.[^@]+$/,
messages: {
required: "Email address is required",
format: "Please provide a valid email address"
}
end
Best Practices
1. Keep Contracts Focused
Each contract should validate a specific use case:
# Good: Separate contracts for different operations
class CreateUserContract < Azu::Contract
field :name, String, required: true
field :email, String, required: true
field :password, String, required: true
end
class UpdateUserContract < Azu::Contract
field :name, String, required: true
field :email, String, required: true
# No password field for updates
end
2. Use Descriptive Field Names
# Good
field :email_address, String, required: true
field :phone_number, String, required: true
# Avoid
field :email, String, required: true
field :phone, String, required: true
3. Implement Custom Validations
class UserContract < Azu::Contract
field :username, String, required: true
def validate_username_format
return unless username
unless username.match(/^[a-zA-Z0-9_]+$/)
errors.add(:username, "can only contain letters, numbers, and underscores")
end
end
def validate_username_availability
return unless username
if User.find_by(username: username)
errors.add(:username, "is already taken")
end
end
end
4. Reuse Common Validations
module CommonValidations
def self.email_field(name = :email)
field name, String,
required: true,
format: /^[^@]+@[^@]+\.[^@]+$/,
messages: {
required: "Email is required",
format: "Invalid email format"
}
end
end
class UserContract < Azu::Contract
include CommonValidations
CommonValidations.email_field
field :name, String, required: true
end
Testing Contracts
Unit Testing
describe UserContract do
describe "#valid?" do
it "is valid with correct data" do
data = {
"name" => "John Doe",
"email" => "john@example.com",
"age" => "25"
}
contract = UserContract.new(data)
contract.valid?.should be_true
end
it "is invalid with missing required fields" do
data = {"name" => "John Doe"}
contract = UserContract.new(data)
contract.valid?.should be_false
contract.errors_for(:email).should contain("is required")
end
it "validates email format" do
data = {
"name" => "John Doe",
"email" => "invalid-email",
"age" => "25"
}
contract = UserContract.new(data)
contract.valid?.should be_false
contract.errors_for(:email).should contain("invalid format")
end
end
describe "#valid_data" do
it "returns cleaned data" do
data = {
"name" => " John Doe ",
"email" => "john@example.com",
"age" => "25"
}
contract = UserContract.new(data)
contract.valid_data["name"].should eq("John Doe")
end
end
end
Integration Testing
describe "Contract integration" do
it "works with controller" do
post "/users", {
"name" => "John Doe",
"email" => "john@example.com"
}
response.status_code.should eq(201)
end
it "returns validation errors" do
post "/users", {
"name" => "",
"email" => "invalid-email"
}
response.status_code.should eq(422)
response.body.should contain("validation errors")
end
end
Related Commands
azu generate endpoint
- Generate API endpointsazu generate model
- Generate data modelsazu generate service
- Generate business logic servicesazu generate middleware
- Generate middleware components
Templates
The contract generator supports different templates:
basic
- Simple contract with basic structureuser
- User registration/update contract templateapi
- API request/response contract templateform
- Form submission contract template
To use a specific template:
azu generate contract ApiRequestContract --template api
Last updated