use skp_validator::Validate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct ECommerceOrder {
#[validate(uuid)]
pub id: String,
#[validate(custom(function = "validate_date", message = "Invalid date format"))]
pub date: String,
#[validate(nested)]
pub user: UserProfile,
#[validate(dive)]
pub items: Vec<OrderItem>,
#[validate(nested)]
pub shipping_address: Address,
#[validate(nested)]
pub billing_address: Address,
#[validate(range(min = 0.0, message = "Total must be positive"))]
pub total: f64,
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct UserProfile {
#[validate(length(min = 3, max = 50, message = "Name must be 3-50 chars"))]
pub name: String,
#[validate(email(message = "Invalid email"))]
pub email: String,
#[validate(phone(message = "Invalid phone"))]
pub phone: String,
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct OrderItem {
#[validate(uuid(message = "Invalid product ID"))]
pub product_id: String,
#[validate(range(min = 1, message = "Quantity must be at least 1"))]
pub quantity: u32,
#[validate(range(min = 0.01, message = "Price must be positive"))]
pub price: f64,
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct Address {
#[validate(length(min = 5, message = "Street address too short"))]
pub street: String,
#[validate(length(min = 2, message = "City name too short"))]
pub city: String,
#[validate(pattern(regex = "^\\d{5}(-\\d{4})?$", message = "Does not match the required format"))]
pub zip: String,
#[validate(allowed_values(values = ["US", "CA", "UK"], message = "Invalid country"))]
pub country: String,
}
fn validate_date(date: &str) -> Result<(), skp_validator::ValidationError> {
if date.len() == 10 && date.chars().nth(4) == Some('-') && date.chars().nth(7) == Some('-') {
Ok(())
} else {
Err(skp_validator::ValidationError::new("date", "date_format", "Invalid date format"))
}
}
#[test]
fn test_valid_complex_order() {
let order = ECommerceOrder {
id: "123e4567-e89b-12d3-a456-426614174000".to_string(), date: "2023-10-27".to_string(),
user: UserProfile {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
phone: "+16502530000".to_string(), },
items: vec![
OrderItem {
product_id: "123e4567-e89b-12d3-a456-426614174001".to_string(),
quantity: 2,
price: 19.99,
},
OrderItem {
product_id: "123e4567-e89b-12d3-a456-426614174002".to_string(),
quantity: 1,
price: 5.00,
}
],
shipping_address: Address {
street: "123 Main St".to_string(),
city: "New York".to_string(),
zip: "10001".to_string(),
country: "US".to_string(),
},
billing_address: Address {
street: "123 Main St".to_string(),
city: "New York".to_string(),
zip: "10001".to_string(),
country: "US".to_string(),
},
total: 44.98,
};
let result = order.validate();
if let Err(e) = &result {
println!("Validation failed: {}", e);
for (field, errs) in e.field_errors() {
println!("{}: {:?}", field, errs);
}
}
assert!(result.is_ok());
}
#[test]
fn test_mixed_failures_deeply_nested() {
let order = ECommerceOrder {
id: "invalid-uuid".to_string(),
date: "2023/10/27".to_string(), user: UserProfile {
name: "Jo".to_string(), email: "invalid-email".to_string(),
phone: "123".to_string(), },
items: vec![
OrderItem {
product_id: "bad-id".to_string(),
quantity: 0, price: -1.0, }
],
shipping_address: Address {
street: "Tiny".to_string(), city: "NY".to_string(),
zip: "ABCDE".to_string(), country: "FR".to_string(), },
billing_address: Address {
street: "123 Correct St".to_string(),
city: "London".to_string(),
zip: "12345".to_string(),
country: "UK".to_string(),
},
total: -100.0, };
let result = order.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let flat_errors = errors.to_flat_map();
println!("Validation Errors: {:#?}", flat_errors);
assert!(flat_errors.contains_key("id"));
assert_eq!(flat_errors["id"], vec!["Invalid UUID"]);
assert!(flat_errors.contains_key("date"));
assert_eq!(flat_errors["date"], vec!["Invalid date format"]);
assert!(flat_errors.contains_key("total"));
assert!(flat_errors.contains_key("user.name"));
assert!(flat_errors.contains_key("user.email"));
assert!(flat_errors.contains_key("user.phone"));
assert!(flat_errors.contains_key("items[0].product_id"));
assert!(flat_errors.contains_key("items[0].quantity"));
assert!(flat_errors.contains_key("items[0].price"));
assert!(flat_errors.contains_key("shipping_address.street"));
assert!(flat_errors.contains_key("shipping_address.zip"));
assert!(flat_errors.contains_key("shipping_address.country"));
assert!(!flat_errors.contains_key("billing_address.zip"));
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct RecursiveNode {
#[validate(length(min = 1, message = "Name required"))]
pub name: String,
#[validate(dive)]
pub children: Vec<RecursiveNode>,
}
#[test]
fn test_recursive_validation() {
let node = RecursiveNode {
name: "root".to_string(),
children: vec![
RecursiveNode {
name: "child1".to_string(),
children: vec![],
},
RecursiveNode {
name: "".to_string(), children: vec![
RecursiveNode {
name: "subchild".to_string(),
children: vec![],
}
],
}
]
};
let result = node.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let flat = errors.to_flat_map();
println!("Recursive errors: {:#?}", flat);
assert!(flat.contains_key("children[1].name"));
assert_eq!(flat["children[1].name"], vec!["Name required"]);
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct ConfigMap {
#[validate(length(min = 3))]
pub env: String,
#[validate(dive)]
pub settings: std::collections::HashMap<String, ConfigItem>,
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq)]
pub struct ConfigItem {
#[validate(range(min = 0, message = "Value must be positive"))]
pub value: i32,
}
#[test]
fn test_hashmap_validation() {
use std::collections::HashMap;
let mut settings = HashMap::new();
settings.insert("timeout".to_string(), ConfigItem { value: 30 });
settings.insert("retry".to_string(), ConfigItem { value: -1 });
let config = ConfigMap {
env: "prod".to_string(),
settings,
};
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let flat = errors.to_flat_map();
println!("Map errors: {:#?}", flat);
assert!(flat.contains_key("settings[retry].value"));
assert_eq!(flat["settings[retry].value"], vec!["Value must be positive"]);
}