use async_trait::async_trait;
use domainstack::{AsyncValidate, Path, ValidationContext, ValidationError};
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use std::sync::Arc;
#[derive(Debug)]
struct UserRegistration {
username: String,
email: String,
password: String,
}
#[async_trait]
impl AsyncValidate for UserRegistration {
async fn validate_async(&self, ctx: &ValidationContext) -> Result<(), ValidationError> {
let pool = ctx
.get_resource::<SqlitePool>("db")
.expect("Database pool not in context");
let mut errors = ValidationError::default();
if self.username.len() < 3 {
errors.push(
Path::from("username"),
"username_too_short",
"Username must be at least 3 characters",
);
}
if !self.email.contains('@') {
errors.push(Path::from("email"), "invalid_email", "Email must contain @");
}
if self.password.len() < 8 {
errors.push(
Path::from("password"),
"password_too_short",
"Password must be at least 8 characters",
);
}
if errors.is_empty() {
let username_exists: bool =
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)")
.bind(&self.username)
.fetch_one(pool.as_ref())
.await
.unwrap_or(false);
if username_exists {
errors.push(
Path::from("username"),
"username_taken",
format!("Username '{}' is already taken", self.username),
);
}
let email_exists: bool =
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = ?)")
.bind(&self.email)
.fetch_one(pool.as_ref())
.await
.unwrap_or(false);
if email_exists {
errors.push(
Path::from("email"),
"email_taken",
format!("Email '{}' is already registered", self.email),
);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
async fn setup_database(pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(pool)
.await?;
sqlx::query("INSERT OR IGNORE INTO users (username, email, password_hash) VALUES (?, ?, ?)")
.bind("admin")
.bind("admin@example.com")
.bind("$2b$12$hashed_password_here")
.execute(pool)
.await?;
sqlx::query("INSERT OR IGNORE INTO users (username, email, password_hash) VALUES (?, ?, ?)")
.bind("johndoe")
.bind("john@example.com")
.bind("$2b$12$another_hashed_password")
.execute(pool)
.await?;
Ok(())
}
async fn register_user(pool: &SqlitePool, user: &UserRegistration) -> Result<i64, sqlx::Error> {
let result = sqlx::query("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)")
.bind(&user.username)
.bind(&user.email)
.bind(&user.password) .execute(pool)
.await?;
Ok(result.last_insert_rowid())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Async SQLite Validation Example ===\n");
println!("📦 Setting up SQLite database...");
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect("sqlite::memory:")
.await?;
setup_database(&pool).await?;
println!("[ok] Database initialized with sample users\n");
let ctx = ValidationContext::new().with_resource("db", Arc::new(pool.clone()));
println!("Example 1: Valid new user registration");
let user1 = UserRegistration {
username: "newuser".to_string(),
email: "newuser@example.com".to_string(),
password: "securepass123".to_string(),
};
match user1.validate_async(&ctx).await {
Ok(()) => {
let user_id = register_user(&pool, &user1).await?;
println!("[ok] User registered successfully (ID: {})", user_id);
println!(" Username: {}", user1.username);
println!(" Email: {}\n", user1.email);
}
Err(e) => println!("[error] Validation failed:\n{}\n", e),
}
println!("Example 2: Username already exists in database");
let user2 = UserRegistration {
username: "admin".to_string(), email: "new@example.com".to_string(),
password: "password123".to_string(),
};
match user2.validate_async(&ctx).await {
Ok(()) => println!("[ok] User registered successfully\n"),
Err(e) => println!("[error] Validation failed:\n{}\n", e),
}
println!("Example 3: Email already exists in database");
let user3 = UserRegistration {
username: "newuser2".to_string(),
email: "admin@example.com".to_string(), password: "password123".to_string(),
};
match user3.validate_async(&ctx).await {
Ok(()) => println!("[ok] User registered successfully\n"),
Err(e) => println!("[error] Validation failed:\n{}\n", e),
}
println!("Example 4: Multiple validation errors (sync + async)");
let user4 = UserRegistration {
username: "ab".to_string(), email: "invalid-email".to_string(), password: "short".to_string(), };
match user4.validate_async(&ctx).await {
Ok(()) => println!("[ok] User registered successfully\n"),
Err(e) => println!("[error] Validation failed:\n{}\n", e),
}
println!("Example 5: Both username and email already taken");
let user5 = UserRegistration {
username: "admin".to_string(), email: "john@example.com".to_string(), password: "password123".to_string(),
};
match user5.validate_async(&ctx).await {
Ok(()) => println!("[ok] User registered successfully\n"),
Err(e) => println!("[error] Validation failed:\n{}\n", e),
}
println!("=== Users in Database ===");
let users: Vec<(String, String)> =
sqlx::query_as("SELECT username, email FROM users ORDER BY created_at")
.fetch_all(&pool)
.await?;
for (i, (username, email)) in users.iter().enumerate() {
println!("{}. {} <{}>", i + 1, username, email);
}
println!("\n=== Example completed ===");
Ok(())
}