use skp_validator::{
Validate, ValidationContext, ValidationError, ValidationErrors, ValidationResult,
};
use skp_validator_rules::numeric::range::RangeRule;
#[cfg(feature = "regex")]
use skp_validator_rules::string::pattern::PatternRule;
use skp_validator_core::Rule; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct TaxInfo {
country_code: String,
tax_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct TransferRequest {
amount: f64,
currency: String,
recipient_id: String,
is_international: bool,
tax_info: Option<TaxInfo>,
}
impl Validate for TaxInfo {
fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
let mut errors = ValidationErrors::new();
let allowed_countries = ["US", "CA", "GB", "DE"];
if !allowed_countries.contains(&self.country_code.as_str()) {
errors.add_field_error(
"country_code",
ValidationError::new("country_code", "invalid_country", "Country not supported")
);
}
#[cfg(feature = "regex")]
{
let pattern_rule = match self.country_code.as_str() {
"US" => Some(PatternRule::new(r"^\d{9}$").message("US Tax ID must be 9 digits (SSN/EIN)")),
"CA" => Some(PatternRule::new(r"^[A-Z]{2}\d{6}$").message("Canadian Tax ID must be 2 letters + 6 digits")),
_ => None,
};
if let Some(rule) = pattern_rule {
if let Err(e) = rule.validate(&self.tax_id, ctx) {
let msg = e.to_flat_map().values().next().and_then(|v| v.first().cloned())
.unwrap_or_else(|| "Invalid format".to_string());
errors.add_field_error("tax_id", ValidationError::new("tax_id", "pattern", msg));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl Validate for TransferRequest {
fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
let mut errors = ValidationErrors::new();
let tier = ctx.get_meta("user_tier").unwrap_or("basic");
let base_limit = match tier {
"vip" => 100_000.0,
"premium" => 10_000.0,
_ => 1_000.0,
};
let daily_limit = ctx.get_meta("daily_limit")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(base_limit);
let range_rule = RangeRule::<f64>::new().max(daily_limit).message(format!(
"Amount exceeds your daily limit of {} ({})", daily_limit, tier
));
if let Err(mut e) = range_rule.validate(&self.amount, ctx) {
let err = if !e.errors.is_empty() {
e.errors.pop()
} else if let Some(field_errors) = e.fields.get_mut("") {
if let skp_validator::FieldErrors::Simple(vec) = field_errors {
vec.first().cloned()
} else { None }
} else { None };
if let Some(err_obj) = err {
errors.add_field_error("amount", err_obj);
}
}
if self.is_international {
if self.tax_info.is_none() {
errors.add_field_error(
"tax_info",
ValidationError::new("tax_info", "required", "Tax info required for international transfers")
);
}
}
if let Some(ref info) = self.tax_info {
if let Err(nested_errs) = info.validate_with_context(ctx) {
errors.add_nested_errors("tax_info", nested_errs);
}
}
let crypto_enabled = ctx.get_meta("feature_crypto").map(|s| s == "true").unwrap_or(false);
if !crypto_enabled && self.currency == "BTC" {
errors.add_field_error(
"currency",
ValidationError::new("currency", "feature_disabled", "Crypto transfers are disabled")
);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn main() {
println!("--- Test Case 1: Basic User exceeding limit ---");
let req1 = TransferRequest {
amount: 5000.0,
currency: "USD".to_string(),
recipient_id: "user_123".to_string(),
is_international: false,
tax_info: None,
};
let ctx_basic = ValidationContext::new().with_meta("user_tier", "basic");
print_validation("Basic User", &req1, &ctx_basic);
println!("\n--- Test Case 2: VIP User within limit ---");
let req2 = TransferRequest {
amount: 50000.0,
currency: "USD".to_string(),
recipient_id: "user_123".to_string(),
is_international: false,
tax_info: None,
};
let ctx_vip = ValidationContext::new().with_meta("user_tier", "vip");
print_validation("VIP User", &req2, &ctx_vip);
println!("\n--- Test Case 3: International Transfer Missing Tax Info ---");
let req3 = TransferRequest {
amount: 100.0,
currency: "USD".to_string(),
recipient_id: "user_999".to_string(),
is_international: true, tax_info: None,
};
print_validation("Intl Missing Tax", &req3, &ctx_basic);
println!("\n--- Test Case 4: International Transfer Invalid Tax Format (US) ---");
let req4 = TransferRequest {
amount: 100.0,
currency: "USD".to_string(),
recipient_id: "user_999".to_string(),
is_international: true,
tax_info: Some(TaxInfo {
country_code: "US".to_string(),
tax_id: "AB123".to_string(), }),
};
print_validation("Intl Invalid US Tax", &req4, &ctx_basic);
println!("\n--- Test Case 5: Feature Flag (Crypto Blocked) ---");
let req5 = TransferRequest {
amount: 10.0,
currency: "BTC".to_string(),
recipient_id: "wallet_123".to_string(),
is_international: false,
tax_info: None,
};
print_validation("Crypto Blocked", &req5, &ctx_basic);
println!("\n--- Test Case 6: Feature Flag (Crypto Allowed) ---");
let ctx_crypto = ValidationContext::new().with_meta("feature_crypto", "true");
print_validation("Crypto Allowed", &req5, &ctx_crypto);
}
fn print_validation(name: &str, req: &TransferRequest, ctx: &ValidationContext) {
match req.validate_with_context(ctx) {
Ok(_) => println!("✅ {}: Valid", name),
Err(e) => {
println!("❌ {}: Invalid", name);
for (field, msgs) in e.to_flat_map() {
for msg in msgs {
println!(" - {}: {}", field, msg);
}
}
}
}
}