Validation Reference
Validation Reference
Overview
Validators enforce constraints on data being inserted or updated. If validation fails, the operation is rejected with a Validation error.
Key points:
- Validators run after sanitizers
- Validation failure rejects the entire operation
- Multiple validators can be applied to a single field
- Validators are applied on both insert and update
Syntax
The #[validate(...)] attribute adds validation rules to fields:
use ic_dbms_api::prelude::*;
#[derive(Table, ...)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
// Unit struct validator (no parameters)
#[validate(EmailValidator)]
pub email: Text,
// Tuple struct validator (positional parameter)
#[validate(MaxStrlenValidator(100))]
pub name: Text,
}
Built-in Validators
All validators are available in ic_dbms_api::prelude.
String Length Validators
MaxStrlenValidator - Maximum string length
#[validate(MaxStrlenValidator(255))]
pub description: Text, // Max 255 characters
MinStrlenValidator - Minimum string length
#[validate(MinStrlenValidator(8))]
pub password: Text, // At least 8 characters
RangeStrlenValidator - String length within range
#[validate(RangeStrlenValidator(3, 50))]
pub username: Text, // Between 3 and 50 characters
Format Validators
EmailValidator - Valid email format
#[validate(EmailValidator)]
pub email: Text, // Must be valid email
UrlValidator - Valid URL format
#[validate(UrlValidator)]
pub website: Text, // Must be valid URL
PhoneNumberValidator - Valid phone number format
#[validate(PhoneNumberValidator)]
pub phone: Text, // Must be valid phone number
MimeTypeValidator - Valid MIME type format
#[validate(MimeTypeValidator)]
pub content_type: Text, // e.g., "application/json", "image/png"
RgbColorValidator - Valid RGB color format
#[validate(RgbColorValidator)]
pub color: Text, // e.g., "#FF5733", "rgb(255, 87, 51)"
Case Validators
CamelCaseValidator - Must be camelCase
#[validate(CamelCaseValidator)]
pub identifier: Text, // e.g., "myVariableName"
KebabCaseValidator - Must be kebab-case
#[validate(KebabCaseValidator)]
pub slug: Text, // e.g., "my-page-slug"
SnakeCaseValidator - Must be snake_case
#[validate(SnakeCaseValidator)]
pub code: Text, // e.g., "my_constant_name"
Locale Validators
CountryIso639Validator - ISO 639 language code
#[validate(CountryIso639Validator)]
pub language: Text, // e.g., "en", "es", "fr"
CountryIso3166Validator - ISO 3166 country code
#[validate(CountryIso3166Validator)]
pub country: Text, // e.g., "US", "GB", "DE"
Implementing Custom Validators
Create a struct implementing the Validate trait:
use ic_dbms_api::prelude::{Validate, Value, IcDbmsResult, IcDbmsError};
/// Validates that a number is positive
pub struct PositiveValidator;
impl Validate for PositiveValidator {
fn validate(&self, value: &Value) -> IcDbmsResult<()> {
match value {
Value::Int32(n) if n.0 > 0 => Ok(()),
Value::Int64(n) if n.0 > 0 => Ok(()),
Value::Decimal(d) if d.0 > rust_decimal::Decimal::ZERO => Ok(()),
Value::Int32(_) | Value::Int64(_) | Value::Decimal(_) => {
Err(IcDbmsError::Validation("Value must be positive".to_string()))
}
_ => Err(IcDbmsError::Validation("PositiveValidator only applies to numeric types".to_string()))
}
}
}
// Usage
#[derive(Table, ...)]
#[table = "products"]
pub struct Product {
#[primary_key]
pub id: Uint32,
#[validate(PositiveValidator)]
pub price: Decimal,
}
Custom validator with parameters (tuple struct):
/// Validates that a string matches a regex pattern
pub struct RegexValidator(pub &'static str);
impl Validate for RegexValidator {
fn validate(&self, value: &Value) -> IcDbmsResult<()> {
if let Value::Text(text) = value {
let re = regex::Regex::new(self.0).unwrap();
if re.is_match(text.as_str()) {
return Ok(());
}
}
Err(IcDbmsError::Validation(
format!("Value does not match pattern: {}", self.0)
))
}
}
// Usage
#[validate(RegexValidator(r"^[A-Z]{2}-\d{4}$"))]
pub product_code: Text, // Must match "XX-1234" format
Custom validator with named parameters:
/// Validates a number is within a range
pub struct RangeValidator {
pub min: i64,
pub max: i64,
}
impl Validate for RangeValidator {
fn validate(&self, value: &Value) -> IcDbmsResult<()> {
let num = match value {
Value::Int32(n) => n.0 as i64,
Value::Int64(n) => n.0,
_ => return Err(IcDbmsError::Validation("RangeValidator requires integer".to_string())),
};
if num >= self.min && num <= self.max {
Ok(())
} else {
Err(IcDbmsError::Validation(
format!("Value must be between {} and {}", self.min, self.max)
))
}
}
}
// Usage
#[validate(RangeValidator, min = 1, max = 100)]
pub percentage: Int32,
Validation Errors
When validation fails, an IcDbmsError::Validation(String) is returned:
let result = client.insert::<User>(User::table_name(), user, None).await?;
match result {
Ok(()) => println!("Insert successful"),
Err(IcDbmsError::Validation(msg)) => {
println!("Validation failed: {}", msg);
// e.g., "Invalid email format"
// e.g., "String length exceeds maximum of 100"
}
Err(e) => println!("Other error: {:?}", e),
}
Examples
Comprehensive user validation:
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
#[validate(RangeStrlenValidator(2, 50))]
pub name: Text,
#[validate(EmailValidator)]
pub email: Text,
#[validate(MinStrlenValidator(8))]
pub password_hash: Text,
#[validate(PhoneNumberValidator)]
pub phone: Nullable<Text>,
#[validate(UrlValidator)]
pub website: Nullable<Text>,
#[validate(CountryIso3166Validator)]
pub country: Nullable<Text>,
#[validate(CountryIso639Validator)]
pub language: Text,
}
Product validation:
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "products"]
pub struct Product {
#[primary_key]
pub id: Uuid,
#[validate(RangeStrlenValidator(1, 200))]
pub name: Text,
#[validate(MaxStrlenValidator(2000))]
pub description: Text,
#[validate(KebabCaseValidator)]
pub slug: Text,
#[validate(MimeTypeValidator)]
pub image_type: Nullable<Text>,
#[validate(RgbColorValidator)]
pub accent_color: Nullable<Text>,
}
Combined with sanitizers:
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "articles"]
pub struct Article {
#[primary_key]
pub id: Uuid,
// Sanitize first, then validate
#[sanitizer(TrimSanitizer)]
#[validate(MaxStrlenValidator(200))]
pub title: Text,
// Convert to slug format, then validate
#[sanitizer(SlugSanitizer)]
#[validate(KebabCaseValidator)]
pub slug: Text,
#[sanitizer(TrimSanitizer)]
pub content: Text,
}