#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub code: &'static str,
pub message: String,
}
impl ValidationError {
#[must_use]
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ValidationError {}
pub fn validate_email(s: &str) -> Result<(), ValidationError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
let (local, domain) = match trimmed.split_once('@') {
Some(parts) => parts,
None => {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
))
}
};
if local.is_empty() || domain.is_empty() {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
if domain.contains('@') {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
if !domain.contains('.') || domain.starts_with('.') || domain.ends_with('.') {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
if domain.contains("..") || local.contains("..") {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
Ok(())
}
#[must_use]
pub fn is_email(s: &str) -> bool {
validate_email(s).is_ok()
}
pub fn validate_url(s: &str) -> Result<(), ValidationError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(ValidationError::new("invalid_url", "Enter a valid URL."));
}
let rest = if let Some(r) = trimmed.strip_prefix("https://") {
r
} else if let Some(r) = trimmed.strip_prefix("http://") {
r
} else {
return Err(ValidationError::new("invalid_url", "Enter a valid URL."));
};
let host_end = rest
.find(|c: char| c == '/' || c == '?' || c == '#')
.unwrap_or(rest.len());
let host = &rest[..host_end];
if host.is_empty() || host.starts_with(':') {
return Err(ValidationError::new("invalid_url", "Enter a valid URL."));
}
let hostname = host.split(':').next().unwrap_or(host);
if hostname.is_empty() {
return Err(ValidationError::new("invalid_url", "Enter a valid URL."));
}
Ok(())
}
#[must_use]
pub fn is_url(s: &str) -> bool {
validate_url(s).is_ok()
}
pub fn validate_slug(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new(
"invalid_slug",
"Enter a valid slug consisting of letters, numbers, underscores or hyphens.",
));
}
for ch in s.chars() {
let ok = ch.is_ascii_alphanumeric() || ch == '_' || ch == '-';
if !ok {
return Err(ValidationError::new(
"invalid_slug",
"Enter a valid slug consisting of letters, numbers, underscores or hyphens.",
));
}
}
Ok(())
}
#[must_use]
pub fn is_slug(s: &str) -> bool {
validate_slug(s).is_ok()
}
pub fn validate_unicode_slug(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new(
"invalid_unicode_slug",
"Enter a valid slug consisting of Unicode letters, numbers, underscores or hyphens.",
));
}
for ch in s.chars() {
let ok = ch.is_alphanumeric() || ch == '_' || ch == '-';
if !ok {
return Err(ValidationError::new(
"invalid_unicode_slug",
"Enter a valid slug consisting of Unicode letters, numbers, underscores or hyphens.",
));
}
}
Ok(())
}
#[must_use]
pub fn is_unicode_slug(s: &str) -> bool {
validate_unicode_slug(s).is_ok()
}
pub fn validate_prohibit_null_characters(s: &str) -> Result<(), ValidationError> {
if s.contains('\0') {
return Err(ValidationError::new(
"null_characters_not_allowed",
"Null characters are not allowed.",
));
}
Ok(())
}
pub fn validate_phone_e164(s: &str) -> Result<(), ValidationError> {
let rest = match s.strip_prefix('+') {
Some(r) => r,
None => {
return Err(ValidationError::new(
"invalid_phone",
"Enter a phone number in E.164 format (e.g. +14155552671).",
));
}
};
let len = rest.len();
if !(1..=15).contains(&len) {
return Err(ValidationError::new(
"invalid_phone",
"Enter a phone number in E.164 format (e.g. +14155552671).",
));
}
if !rest.chars().all(|c| c.is_ascii_digit()) {
return Err(ValidationError::new(
"invalid_phone",
"Enter a phone number in E.164 format (e.g. +14155552671).",
));
}
Ok(())
}
#[must_use]
pub fn is_phone_e164(s: &str) -> bool {
validate_phone_e164(s).is_ok()
}
pub fn validate_hex_color(s: &str) -> Result<(), ValidationError> {
let rest = match s.strip_prefix('#') {
Some(r) => r,
None => {
return Err(ValidationError::new(
"invalid_hex_color",
"Enter a hex color like `#fff` or `#ffaa00`.",
));
}
};
if !matches!(rest.len(), 3 | 4 | 6 | 8) {
return Err(ValidationError::new(
"invalid_hex_color",
"Enter a hex color like `#fff` or `#ffaa00`.",
));
}
if !rest.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(ValidationError::new(
"invalid_hex_color",
"Enter a hex color like `#fff` or `#ffaa00`.",
));
}
Ok(())
}
#[must_use]
pub fn is_hex_color(s: &str) -> bool {
validate_hex_color(s).is_ok()
}
pub fn validate_uuid(s: &str) -> Result<(), ValidationError> {
uuid::Uuid::parse_str(s)
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_uuid", "Enter a valid UUID."))
}
#[must_use]
pub fn is_uuid(s: &str) -> bool {
validate_uuid(s).is_ok()
}
pub fn validate_iso_date(s: &str) -> Result<(), ValidationError> {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_iso_date", "Enter a date in YYYY-MM-DD format."))
}
pub fn validate_iso_time(s: &str) -> Result<(), ValidationError> {
chrono::NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_iso_time", "Enter a time in HH:MM:SS format."))
}
pub fn validate_iso_datetime(s: &str) -> Result<(), ValidationError> {
chrono::DateTime::parse_from_rfc3339(s)
.map(|_| ())
.map_err(|_| {
ValidationError::new(
"invalid_iso_datetime",
"Enter a datetime in RFC 3339 format (e.g. 2026-01-15T14:30:00Z).",
)
})
}
pub fn validate_alphanumeric(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new(
"not_alphanumeric",
"Enter only letters and digits.",
));
}
if !s.chars().all(|c| c.is_ascii_alphanumeric()) {
return Err(ValidationError::new(
"not_alphanumeric",
"Enter only letters and digits.",
));
}
Ok(())
}
pub fn validate_numeric(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new("not_numeric", "Enter only digits."));
}
if !s.chars().all(|c| c.is_ascii_digit()) {
return Err(ValidationError::new("not_numeric", "Enter only digits."));
}
Ok(())
}
pub fn validate_alpha(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new("not_alpha", "Enter only letters."));
}
if !s.chars().all(|c| c.is_ascii_alphabetic()) {
return Err(ValidationError::new("not_alpha", "Enter only letters."));
}
Ok(())
}
pub fn validate_creditcard_luhn(s: &str) -> Result<(), ValidationError> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace() && *c != '-')
.collect();
if !cleaned.chars().all(|c| c.is_ascii_digit()) {
return Err(ValidationError::new(
"invalid_card_number",
"Enter a valid credit card number.",
));
}
if !(12..=19).contains(&cleaned.len()) {
return Err(ValidationError::new(
"invalid_card_number",
"Enter a valid credit card number.",
));
}
let mut sum = 0u32;
let mut double = false;
for ch in cleaned.chars().rev() {
let mut d = ch.to_digit(10).expect("digit-only by check above");
if double {
d *= 2;
if d >= 10 {
d -= 9;
}
}
sum += d;
double = !double;
}
if sum % 10 != 0 {
return Err(ValidationError::new(
"invalid_card_number",
"Enter a valid credit card number.",
));
}
Ok(())
}
pub fn validate_isbn(s: &str) -> Result<(), ValidationError> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace() && *c != '-')
.collect();
match cleaned.len() {
10 => validate_isbn_10(&cleaned),
13 => validate_isbn_13(&cleaned),
_ => Err(ValidationError::new(
"invalid_isbn",
"Enter a valid ISBN-10 or ISBN-13.",
)),
}
}
fn validate_isbn_10(s: &str) -> Result<(), ValidationError> {
let chars: Vec<char> = s.chars().collect();
let mut sum = 0u32;
for (i, &ch) in chars.iter().enumerate() {
let digit = if i == 9 && (ch == 'X' || ch == 'x') {
10
} else if let Some(d) = ch.to_digit(10) {
d
} else {
return Err(ValidationError::new(
"invalid_isbn",
"Enter a valid ISBN-10 or ISBN-13.",
));
};
sum += digit * (u32::try_from(i).unwrap_or(0) + 1);
}
if sum % 11 != 0 {
return Err(ValidationError::new(
"invalid_isbn",
"Enter a valid ISBN-10 or ISBN-13.",
));
}
Ok(())
}
fn validate_isbn_13(s: &str) -> Result<(), ValidationError> {
if !s.chars().all(|c| c.is_ascii_digit()) {
return Err(ValidationError::new(
"invalid_isbn",
"Enter a valid ISBN-10 or ISBN-13.",
));
}
let mut sum = 0u32;
for (i, ch) in s.chars().enumerate() {
let d = ch.to_digit(10).expect("digit-only by check above");
let weight = if i.is_multiple_of(2) { 1 } else { 3 };
sum += d * weight;
}
if sum % 10 != 0 {
return Err(ValidationError::new(
"invalid_isbn",
"Enter a valid ISBN-10 or ISBN-13.",
));
}
Ok(())
}
pub fn validate_hostname(s: &str) -> Result<(), ValidationError> {
if s.is_empty() || s.len() > 253 {
return Err(ValidationError::new(
"invalid_hostname",
"Enter a valid hostname.",
));
}
if s.starts_with('.') || s.ends_with('.') {
return Err(ValidationError::new(
"invalid_hostname",
"Enter a valid hostname.",
));
}
for label in s.split('.') {
if label.is_empty() || label.len() > 63 {
return Err(ValidationError::new(
"invalid_hostname",
"Enter a valid hostname.",
));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(ValidationError::new(
"invalid_hostname",
"Enter a valid hostname.",
));
}
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(ValidationError::new(
"invalid_hostname",
"Enter a valid hostname.",
));
}
}
Ok(())
}
pub fn validate_iban(s: &str) -> Result<(), ValidationError> {
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
if !(5..=34).contains(&cleaned.len()) {
return Err(ValidationError::new("invalid_iban", "Enter a valid IBAN."));
}
let bytes = cleaned.as_bytes();
if !bytes[0].is_ascii_uppercase()
|| !bytes[1].is_ascii_uppercase()
|| !bytes[2].is_ascii_digit()
|| !bytes[3].is_ascii_digit()
{
return Err(ValidationError::new("invalid_iban", "Enter a valid IBAN."));
}
if !bytes[4..]
.iter()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
{
return Err(ValidationError::new("invalid_iban", "Enter a valid IBAN."));
}
let rearranged: String = cleaned[4..]
.chars()
.chain(cleaned[..4].chars())
.flat_map(|c| {
if c.is_ascii_digit() {
vec![c]
} else {
let n = c as u32 - 'A' as u32 + 10;
n.to_string().chars().collect()
}
})
.collect();
let mut remainder: u64 = 0;
for ch in rearranged.chars() {
let d = ch.to_digit(10).expect("digit-only after letter map");
remainder = (remainder * 10 + u64::from(d)) % 97;
}
if remainder != 1 {
return Err(ValidationError::new("invalid_iban", "Enter a valid IBAN."));
}
Ok(())
}
pub fn validate_mac_address(s: &str) -> Result<(), ValidationError> {
if s.len() != 17 {
return Err(ValidationError::new(
"invalid_mac_address",
"Enter a valid MAC address (e.g. 00:1A:2B:3C:4D:5E).",
));
}
let sep = s.as_bytes()[2] as char;
if sep != ':' && sep != '-' {
return Err(ValidationError::new(
"invalid_mac_address",
"Enter a valid MAC address (e.g. 00:1A:2B:3C:4D:5E).",
));
}
let parts: Vec<&str> = s.split(sep).collect();
if parts.len() != 6 {
return Err(ValidationError::new(
"invalid_mac_address",
"Enter a valid MAC address (e.g. 00:1A:2B:3C:4D:5E).",
));
}
for part in parts {
if part.len() != 2 || !part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(ValidationError::new(
"invalid_mac_address",
"Enter a valid MAC address (e.g. 00:1A:2B:3C:4D:5E).",
));
}
}
Ok(())
}
pub fn validate_base64(s: &str) -> Result<(), ValidationError> {
validate_base64_impl(s, false)
}
pub fn validate_base64_urlsafe(s: &str) -> Result<(), ValidationError> {
validate_base64_impl(s, true)
}
fn validate_base64_impl(s: &str, urlsafe: bool) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::new(
"invalid_base64",
"Enter a valid base64 string.",
));
}
let pad = s.bytes().rev().take_while(|b| *b == b'=').count();
if pad > 2 {
return Err(ValidationError::new(
"invalid_base64",
"Enter a valid base64 string.",
));
}
let body = &s[..s.len() - pad];
for ch in body.chars() {
let ok = ch.is_ascii_alphanumeric()
|| (if urlsafe {
ch == '-' || ch == '_'
} else {
ch == '+' || ch == '/'
});
if !ok {
return Err(ValidationError::new(
"invalid_base64",
"Enter a valid base64 string.",
));
}
}
if (pad > 0 || !urlsafe) && !s.len().is_multiple_of(4) {
return Err(ValidationError::new(
"invalid_base64",
"Enter a valid base64 string.",
));
}
Ok(())
}
pub fn validate_jwt_shape(s: &str) -> Result<(), ValidationError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(ValidationError::new(
"invalid_jwt",
"Enter a valid JWT (header.payload.signature).",
));
}
for part in &parts {
if part.is_empty() {
return Err(ValidationError::new(
"invalid_jwt",
"Enter a valid JWT (header.payload.signature).",
));
}
validate_base64_urlsafe(part).map_err(|_| {
ValidationError::new(
"invalid_jwt",
"Enter a valid JWT (header.payload.signature).",
)
})?;
}
Ok(())
}
pub fn validate_semver(s: &str) -> Result<(), ValidationError> {
let bad = || ValidationError::new("invalid_semver", "Enter a valid semver (e.g. 1.2.3).");
let (core_pre, build) = match s.split_once('+') {
Some((cp, b)) => (cp, Some(b)),
None => (s, None),
};
let (core, pre) = match core_pre.split_once('-') {
Some((c, p)) => (c, Some(p)),
None => (core_pre, None),
};
let core_parts: Vec<&str> = core.split('.').collect();
if core_parts.len() != 3 {
return Err(bad());
}
for part in core_parts {
if !is_valid_numeric_id(part) {
return Err(bad());
}
}
if let Some(p) = pre {
if !is_valid_semver_id_list(p, false) {
return Err(bad());
}
}
if let Some(b) = build {
if !is_valid_semver_id_list(b, true) {
return Err(bad());
}
}
Ok(())
}
fn is_valid_numeric_id(s: &str) -> bool {
if s.is_empty() {
return false;
}
if !s.chars().all(|c| c.is_ascii_digit()) {
return false;
}
!(s.len() > 1 && s.starts_with('0'))
}
fn is_valid_semver_id_list(s: &str, allow_numeric_leading_zero: bool) -> bool {
if s.is_empty() {
return false;
}
for part in s.split('.') {
if part.is_empty() {
return false;
}
if !part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return false;
}
if !allow_numeric_leading_zero
&& part.chars().all(|c| c.is_ascii_digit())
&& part.len() > 1
&& part.starts_with('0')
{
return false;
}
}
true
}
pub fn validate_country_code(s: &str) -> Result<(), ValidationError> {
if s.len() != 2 || !s.chars().all(|c| c.is_ascii_uppercase()) {
return Err(ValidationError::new(
"invalid_country_code",
"Enter a 2-letter ISO 3166-1 country code (e.g. US, GB).",
));
}
Ok(())
}
pub fn validate_currency_code(s: &str) -> Result<(), ValidationError> {
if s.len() != 3 || !s.chars().all(|c| c.is_ascii_uppercase()) {
return Err(ValidationError::new(
"invalid_currency_code",
"Enter a 3-letter ISO 4217 currency code (e.g. USD, EUR).",
));
}
Ok(())
}
pub fn validate_language_tag(s: &str) -> Result<(), ValidationError> {
let bad = || {
ValidationError::new(
"invalid_language_tag",
"Enter a valid language tag (e.g. en, en-US, zh-Hans-CN).",
)
};
let parts: Vec<&str> = s.split('-').collect();
if parts.is_empty() || parts.len() > 3 {
return Err(bad());
}
let lang = parts[0];
if !(2..=3).contains(&lang.len()) || !lang.chars().all(|c| c.is_ascii_lowercase()) {
return Err(bad());
}
let mut idx = 1;
if idx < parts.len() {
let p = parts[idx];
if p.len() == 4 && is_script_subtag(p) {
idx += 1;
}
}
if idx < parts.len() {
let p = parts[idx];
let is_alpha2 = p.len() == 2 && p.chars().all(|c| c.is_ascii_uppercase());
let is_num3 = p.len() == 3 && p.chars().all(|c| c.is_ascii_digit());
if !is_alpha2 && !is_num3 {
return Err(bad());
}
idx += 1;
}
if idx != parts.len() {
return Err(bad());
}
Ok(())
}
fn is_script_subtag(s: &str) -> bool {
if s.len() != 4 {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_ascii_uppercase() {
return false;
}
chars.all(|c| c.is_ascii_lowercase())
}
pub fn validate_postal_code_us(s: &str) -> Result<(), ValidationError> {
let bad = || {
ValidationError::new(
"invalid_postal_code",
"Enter a valid US ZIP code (12345 or 12345-6789).",
)
};
match s.split_once('-') {
Some((first, second)) => {
if first.len() != 5 || second.len() != 4 {
return Err(bad());
}
if !first.chars().all(|c| c.is_ascii_digit())
|| !second.chars().all(|c| c.is_ascii_digit())
{
return Err(bad());
}
Ok(())
}
None => {
if s.len() != 5 || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(bad());
}
Ok(())
}
}
}
pub fn validate_postal_code_ca(s: &str) -> Result<(), ValidationError> {
let bad = || {
ValidationError::new(
"invalid_postal_code",
"Enter a valid Canadian postal code (A1A 1A1).",
)
};
if s.len() != 7 {
return Err(bad());
}
let bytes = s.as_bytes();
let is_uppercase_letter = |b: u8| b.is_ascii_uppercase();
let is_digit = |b: u8| b.is_ascii_digit();
if !is_uppercase_letter(bytes[0])
|| !is_digit(bytes[1])
|| !is_uppercase_letter(bytes[2])
|| bytes[3] != b' '
|| !is_digit(bytes[4])
|| !is_uppercase_letter(bytes[5])
|| !is_digit(bytes[6])
{
return Err(bad());
}
Ok(())
}
pub fn validate_postal_code_uk(s: &str) -> Result<(), ValidationError> {
let bad = || {
ValidationError::new(
"invalid_postal_code",
"Enter a valid UK postcode (e.g. SW1A 1AA).",
)
};
let (outward, inward) = s.split_once(' ').ok_or_else(bad)?;
if inward.len() != 3 {
return Err(bad());
}
let inward_bytes = inward.as_bytes();
if !inward_bytes[0].is_ascii_digit()
|| !inward_bytes[1].is_ascii_uppercase()
|| !inward_bytes[2].is_ascii_uppercase()
{
return Err(bad());
}
if !(2..=4).contains(&outward.len()) {
return Err(bad());
}
let outward_bytes = outward.as_bytes();
if !outward_bytes[0].is_ascii_uppercase() {
return Err(bad());
}
if !outward
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
{
return Err(bad());
}
Ok(())
}
pub fn validate_min_length(s: &str, min: usize) -> Result<(), ValidationError> {
let len = s.chars().count();
if len < min {
return Err(ValidationError::new(
"min_length",
format!("Ensure this value has at least {min} characters (it has {len})."),
));
}
Ok(())
}
pub fn validate_max_length(s: &str, max: usize) -> Result<(), ValidationError> {
let len = s.chars().count();
if len > max {
return Err(ValidationError::new(
"max_length",
format!("Ensure this value has at most {max} characters (it has {len})."),
));
}
Ok(())
}
pub fn validate_min_value(n: i64, min: i64) -> Result<(), ValidationError> {
if n < min {
return Err(ValidationError::new(
"min_value",
format!("Ensure this value is greater than or equal to {min}."),
));
}
Ok(())
}
pub fn validate_max_value(n: i64, max: i64) -> Result<(), ValidationError> {
if n > max {
return Err(ValidationError::new(
"max_value",
format!("Ensure this value is less than or equal to {max}."),
));
}
Ok(())
}
pub fn validate_min_value_f64(n: f64, min: f64) -> Result<(), ValidationError> {
if n.is_nan() || n < min {
return Err(ValidationError::new(
"min_value",
format!("Ensure this value is greater than or equal to {min}."),
));
}
Ok(())
}
pub fn validate_max_value_f64(n: f64, max: f64) -> Result<(), ValidationError> {
if n.is_nan() || n > max {
return Err(ValidationError::new(
"max_value",
format!("Ensure this value is less than or equal to {max}."),
));
}
Ok(())
}
pub fn validate_integer(s: &str) -> Result<(), ValidationError> {
if s != s.trim() || s.is_empty() {
return Err(ValidationError::new(
"invalid_integer",
"Enter a valid integer.",
));
}
s.parse::<i64>()
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_integer", "Enter a valid integer."))
}
pub fn validate_decimal(
s: &str,
max_digits: Option<usize>,
decimal_places: Option<usize>,
) -> Result<(), ValidationError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(ValidationError::new("invalid_decimal", "Enter a number."));
}
let unsigned = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
let (int_part, frac_part) = match unsigned.split_once('.') {
Some((a, b)) => (a, b),
None => (unsigned, ""),
};
if int_part.is_empty() && frac_part.is_empty() {
return Err(ValidationError::new("invalid_decimal", "Enter a number."));
}
if !int_part.chars().all(|c| c.is_ascii_digit())
|| !frac_part.chars().all(|c| c.is_ascii_digit())
{
return Err(ValidationError::new("invalid_decimal", "Enter a number."));
}
let int_digits = int_part.trim_start_matches('0').len();
let frac_digits = frac_part.len();
if let Some(places) = decimal_places {
if frac_digits > places {
return Err(ValidationError::new(
"max_decimal_places",
format!(
"Ensure there are no more than {places} decimal places (got {frac_digits})."
),
));
}
}
if let Some(total) = max_digits {
let total_digits = int_digits + frac_digits;
if total_digits > total {
return Err(ValidationError::new(
"max_digits",
format!(
"Ensure there are no more than {total} digits in total (got {total_digits})."
),
));
}
}
Ok(())
}
pub fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
use std::str::FromStr as _;
std::net::Ipv4Addr::from_str(s)
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_ipv4_address", "Enter a valid IPv4 address."))
}
pub fn validate_ipv6_address(s: &str) -> Result<(), ValidationError> {
use std::str::FromStr as _;
std::net::Ipv6Addr::from_str(s)
.map(|_| ())
.map_err(|_| ValidationError::new("invalid_ipv6_address", "Enter a valid IPv6 address."))
}
pub fn validate_email_list(s: &str) -> Result<(), ValidationError> {
if s.trim().is_empty() {
return Err(ValidationError::new(
"invalid_email",
"Enter at least one email address.",
));
}
for part in s.split(',') {
let entry = part.trim();
if entry.is_empty() {
return Err(ValidationError::new(
"invalid_email",
"Enter a valid email address.",
));
}
validate_email(entry)?;
}
Ok(())
}
pub fn validate_comma_separated_integer_list(s: &str) -> Result<(), ValidationError> {
if s.trim().is_empty() {
return Err(ValidationError::new(
"invalid_comma_separated_integer_list",
"Enter only digits separated by commas.",
));
}
for part in s.split(',') {
let part = part.trim();
if part.is_empty() || part.parse::<i64>().is_err() {
return Err(ValidationError::new(
"invalid_comma_separated_integer_list",
"Enter only digits separated by commas.",
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn email_accepts_common_shapes() {
assert!(validate_email("alice@example.com").is_ok());
assert!(validate_email("a.b+tag@example.co.uk").is_ok());
assert!(validate_email("nested.dots+plus_underscore-hyphen@sub.example.org").is_ok());
}
#[test]
fn email_rejects_missing_at() {
let e = validate_email("alice.example.com").unwrap_err();
assert_eq!(e.code, "invalid_email");
}
#[test]
fn email_rejects_empty_local_or_domain() {
assert!(validate_email("@example.com").is_err());
assert!(validate_email("alice@").is_err());
}
#[test]
fn email_rejects_no_dot_in_domain() {
assert!(validate_email("alice@localhost").is_err());
}
#[test]
fn email_rejects_two_at_signs() {
assert!(validate_email("a@b@c.com").is_err());
}
#[test]
fn email_rejects_consecutive_dots() {
assert!(validate_email("a..b@example.com").is_err());
assert!(validate_email("a@example..com").is_err());
}
#[test]
fn email_rejects_empty_and_whitespace_only() {
assert!(validate_email("").is_err());
assert!(validate_email(" ").is_err());
}
#[test]
fn is_email_is_a_thin_boolean_wrapper() {
assert!(is_email("a@b.com"));
assert!(!is_email("not an email"));
}
#[test]
fn url_accepts_http_and_https() {
assert!(validate_url("http://example.com").is_ok());
assert!(validate_url("https://example.com").is_ok());
}
#[test]
fn url_accepts_paths_query_fragment() {
assert!(validate_url("https://example.com/path?q=1#frag").is_ok());
}
#[test]
fn url_accepts_port() {
assert!(validate_url("http://example.com:8080/api").is_ok());
}
#[test]
fn url_rejects_no_scheme() {
assert!(validate_url("example.com").is_err());
}
#[test]
fn url_rejects_unknown_scheme() {
assert!(validate_url("ftp://example.com").is_err());
}
#[test]
fn url_rejects_empty_host() {
assert!(validate_url("https://").is_err());
assert!(validate_url("https:///path").is_err());
}
#[test]
fn url_rejects_empty_string() {
assert!(validate_url("").is_err());
}
#[test]
fn slug_accepts_alnum_underscore_hyphen() {
assert!(validate_slug("hello-world_42").is_ok());
assert!(validate_slug("just-letters").is_ok());
assert!(validate_slug("123").is_ok());
}
#[test]
fn slug_rejects_spaces_and_punctuation() {
assert!(validate_slug("hello world").is_err());
assert!(validate_slug("hello!").is_err());
assert!(validate_slug("a.b").is_err());
}
#[test]
fn slug_rejects_empty() {
assert!(validate_slug("").is_err());
}
#[test]
fn slug_rejects_non_ascii_letters() {
assert!(validate_slug("café").is_err());
}
#[test]
fn min_length_uses_char_count_not_byte_count() {
assert!(validate_min_length("éé", 2).is_ok());
assert!(validate_min_length("é", 2).is_err());
}
#[test]
fn max_length_uses_char_count_not_byte_count() {
assert!(validate_max_length("éé", 2).is_ok());
assert!(validate_max_length("ééé", 2).is_err());
}
#[test]
fn min_length_at_boundary_is_ok() {
assert!(validate_min_length("abc", 3).is_ok());
assert!(validate_min_length("ab", 3).is_err());
}
#[test]
fn min_and_max_value_bounds_are_inclusive() {
assert!(validate_min_value(5, 5).is_ok());
assert!(validate_max_value(5, 5).is_ok());
assert!(validate_min_value(4, 5).is_err());
assert!(validate_max_value(6, 5).is_err());
}
#[test]
fn min_and_max_value_f64_bounds_are_inclusive() {
assert!(validate_min_value_f64(5.0, 5.0).is_ok());
assert!(validate_max_value_f64(5.0, 5.0).is_ok());
assert!(validate_min_value_f64(4.999, 5.0).is_err());
assert!(validate_max_value_f64(5.001, 5.0).is_err());
}
#[test]
fn min_and_max_value_f64_reject_nan() {
assert!(validate_min_value_f64(f64::NAN, 0.0).is_err());
assert!(validate_max_value_f64(f64::NAN, 100.0).is_err());
}
#[test]
fn min_and_max_value_f64_handle_infinities() {
assert!(validate_min_value_f64(f64::INFINITY, 5.0).is_ok());
assert!(validate_max_value_f64(f64::INFINITY, 5.0).is_err());
assert!(validate_min_value_f64(f64::NEG_INFINITY, 5.0).is_err());
assert!(validate_max_value_f64(f64::NEG_INFINITY, 5.0).is_ok());
}
#[test]
fn validation_error_display_renders_message() {
let e = ValidationError::new("invalid_email", "Bad email.");
assert_eq!(format!("{e}"), "Bad email.");
assert_eq!(e.code, "invalid_email");
}
#[test]
fn integer_accepts_positive_negative_zero() {
assert!(validate_integer("0").is_ok());
assert!(validate_integer("42").is_ok());
assert!(validate_integer("-7").is_ok());
assert!(validate_integer("+1").is_ok());
}
#[test]
fn integer_rejects_decimals_and_letters() {
assert!(validate_integer("3.14").is_err());
assert!(validate_integer("abc").is_err());
assert!(validate_integer("12abc").is_err());
}
#[test]
fn integer_rejects_surrounding_whitespace() {
assert!(validate_integer(" 42").is_err());
assert!(validate_integer("42 ").is_err());
assert!(validate_integer("").is_err());
}
#[test]
fn decimal_accepts_well_formed_numbers() {
assert!(validate_decimal("12.34", None, None).is_ok());
assert!(validate_decimal("-12.34", None, None).is_ok());
assert!(validate_decimal("+0.5", None, None).is_ok());
assert!(validate_decimal(".5", None, None).is_ok());
assert!(validate_decimal("5.", None, None).is_ok());
assert!(validate_decimal("100", None, None).is_ok());
}
#[test]
fn decimal_rejects_non_numeric() {
assert!(validate_decimal("abc", None, None).is_err());
assert!(validate_decimal("12.3.4", None, None).is_err());
assert!(validate_decimal(".", None, None).is_err());
assert!(validate_decimal("", None, None).is_err());
}
#[test]
fn decimal_enforces_max_decimal_places() {
let e = validate_decimal("12.345", None, Some(2)).unwrap_err();
assert_eq!(e.code, "max_decimal_places");
assert!(validate_decimal("12.34", None, Some(2)).is_ok());
}
#[test]
fn decimal_enforces_max_total_digits() {
let e = validate_decimal("12345.6", Some(5), None).unwrap_err();
assert_eq!(e.code, "max_digits");
assert!(validate_decimal("1234.5", Some(5), None).is_ok());
}
#[test]
fn decimal_max_digits_ignores_leading_zeros() {
assert!(validate_decimal("007.5", Some(2), None).is_ok());
}
#[test]
fn ipv4_accepts_dotted_quad() {
assert!(validate_ipv4_address("127.0.0.1").is_ok());
assert!(validate_ipv4_address("0.0.0.0").is_ok());
assert!(validate_ipv4_address("255.255.255.255").is_ok());
}
#[test]
fn ipv4_rejects_malformed_or_out_of_range() {
assert!(validate_ipv4_address("256.0.0.1").is_err());
assert!(validate_ipv4_address("not.an.ip.addr").is_err());
assert!(validate_ipv4_address("1.2.3").is_err());
assert!(validate_ipv4_address("").is_err());
}
#[test]
fn ipv6_accepts_full_and_shorthand() {
assert!(validate_ipv6_address("2001:db8::1").is_ok());
assert!(validate_ipv6_address("::1").is_ok());
assert!(validate_ipv6_address("fe80::1234:5678:9abc:def0").is_ok());
}
#[test]
fn ipv6_rejects_v4_addresses_and_garbage() {
assert!(validate_ipv6_address("127.0.0.1").is_err());
assert!(validate_ipv6_address("zzz").is_err());
assert!(validate_ipv6_address("").is_err());
}
#[test]
fn comma_list_accepts_clean_form() {
assert!(validate_comma_separated_integer_list("1,2,3").is_ok());
assert!(validate_comma_separated_integer_list("42").is_ok());
assert!(validate_comma_separated_integer_list("-1,0,1").is_ok());
}
#[test]
fn comma_list_tolerates_inner_whitespace() {
assert!(validate_comma_separated_integer_list("1, 2, 3").is_ok());
}
#[test]
fn comma_list_rejects_empty_and_non_integers() {
assert!(validate_comma_separated_integer_list("").is_err());
assert!(validate_comma_separated_integer_list("1,abc,3").is_err());
assert!(validate_comma_separated_integer_list("1,,3").is_err());
}
#[test]
fn unicode_slug_accepts_non_ascii_letters() {
assert!(validate_unicode_slug("café-au-lait").is_ok());
assert!(validate_unicode_slug("日本語").is_ok());
assert!(validate_unicode_slug("Привет_мир").is_ok());
}
#[test]
fn unicode_slug_still_rejects_punctuation_and_spaces() {
assert!(validate_unicode_slug("hello world").is_err());
assert!(validate_unicode_slug("hello!").is_err());
assert!(validate_unicode_slug("a.b").is_err());
}
#[test]
fn unicode_slug_rejects_empty() {
assert!(validate_unicode_slug("").is_err());
}
#[test]
fn prohibit_null_accepts_strings_without_nul() {
assert!(validate_prohibit_null_characters("hello").is_ok());
assert!(validate_prohibit_null_characters("").is_ok());
assert!(validate_prohibit_null_characters("non-printable\x01ok").is_ok());
}
#[test]
fn prohibit_null_rejects_strings_containing_nul() {
let e = validate_prohibit_null_characters("hello\0world").unwrap_err();
assert_eq!(e.code, "null_characters_not_allowed");
}
#[test]
fn email_list_accepts_single_email() {
assert!(validate_email_list("alice@example.com").is_ok());
}
#[test]
fn email_list_accepts_multiple_with_whitespace() {
assert!(validate_email_list("a@b.com, c@d.com,e@f.com").is_ok());
}
#[test]
fn email_list_rejects_empty_string() {
assert!(validate_email_list("").is_err());
assert!(validate_email_list(" ").is_err());
}
#[test]
fn email_list_rejects_empty_entries() {
assert!(validate_email_list("a@b.com,,c@d.com").is_err());
assert!(validate_email_list("a@b.com, ,c@d.com").is_err());
}
#[test]
fn email_list_rejects_invalid_entry() {
let e = validate_email_list("a@b.com,not-an-email,c@d.com").unwrap_err();
assert_eq!(e.code, "invalid_email");
}
#[test]
fn phone_e164_accepts_typical_examples() {
assert!(validate_phone_e164("+14155552671").is_ok());
assert!(validate_phone_e164("+442012345678").is_ok());
assert!(validate_phone_e164("+919876543210").is_ok());
}
#[test]
fn phone_e164_accepts_minimum_length_of_one_digit() {
assert!(validate_phone_e164("+1").is_ok());
}
#[test]
fn phone_e164_accepts_maximum_length_of_fifteen_digits() {
assert!(validate_phone_e164("+123456789012345").is_ok());
}
#[test]
fn phone_e164_rejects_missing_plus() {
let e = validate_phone_e164("14155552671").unwrap_err();
assert_eq!(e.code, "invalid_phone");
}
#[test]
fn phone_e164_rejects_too_many_digits() {
assert!(validate_phone_e164("+1234567890123456").is_err());
}
#[test]
fn phone_e164_rejects_zero_digits_after_plus() {
assert!(validate_phone_e164("+").is_err());
}
#[test]
fn phone_e164_rejects_separators_and_letters() {
assert!(validate_phone_e164("+1-415-555-2671").is_err());
assert!(validate_phone_e164("+1 (415) 555-2671").is_err());
assert!(validate_phone_e164("+1abc4155552671").is_err());
}
#[test]
fn phone_e164_rejects_empty() {
assert!(validate_phone_e164("").is_err());
}
#[test]
fn is_phone_e164_is_thin_boolean_wrapper() {
assert!(is_phone_e164("+14155552671"));
assert!(!is_phone_e164("14155552671"));
}
#[test]
fn hex_color_accepts_rgb_shorthand() {
assert!(validate_hex_color("#fff").is_ok());
assert!(validate_hex_color("#000").is_ok());
assert!(validate_hex_color("#fA0").is_ok());
}
#[test]
fn hex_color_accepts_full_rrggbb() {
assert!(validate_hex_color("#ffffff").is_ok());
assert!(validate_hex_color("#FFAA00").is_ok());
}
#[test]
fn hex_color_accepts_alpha_variants() {
assert!(validate_hex_color("#fff8").is_ok());
assert!(validate_hex_color("#FFAA00CC").is_ok());
}
#[test]
fn hex_color_rejects_missing_hash() {
assert!(validate_hex_color("fff").is_err());
}
#[test]
fn hex_color_rejects_non_hex_chars() {
let e = validate_hex_color("#ffffg0").unwrap_err();
assert_eq!(e.code, "invalid_hex_color");
}
#[test]
fn hex_color_rejects_wrong_length() {
assert!(validate_hex_color("#f").is_err());
assert!(validate_hex_color("#ff").is_err());
assert!(validate_hex_color("#fffff").is_err());
assert!(validate_hex_color("#fffffff").is_err());
}
#[test]
fn hex_color_rejects_empty_and_hash_only() {
assert!(validate_hex_color("").is_err());
assert!(validate_hex_color("#").is_err());
}
#[test]
fn is_hex_color_is_thin_boolean_wrapper() {
assert!(is_hex_color("#fff"));
assert!(!is_hex_color("fff"));
}
#[test]
fn uuid_accepts_hyphenated_form() {
assert!(validate_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
}
#[test]
fn uuid_accepts_simple_form_no_hyphens() {
assert!(validate_uuid("550e8400e29b41d4a716446655440000").is_ok());
}
#[test]
fn uuid_accepts_urn_prefix() {
assert!(validate_uuid("urn:uuid:550e8400-e29b-41d4-a716-446655440000").is_ok());
}
#[test]
fn uuid_accepts_braced_form() {
assert!(validate_uuid("{550e8400-e29b-41d4-a716-446655440000}").is_ok());
}
#[test]
fn uuid_rejects_garbage() {
let e = validate_uuid("not-a-uuid").unwrap_err();
assert_eq!(e.code, "invalid_uuid");
}
#[test]
fn uuid_rejects_wrong_length() {
assert!(validate_uuid("550e8400e29b41d4a71644665544000").is_err());
}
#[test]
fn uuid_rejects_non_hex() {
assert!(validate_uuid("550e8400-e29b-41d4-a716-44665544000g").is_err());
}
#[test]
fn is_uuid_is_thin_boolean_wrapper() {
assert!(is_uuid("550e8400-e29b-41d4-a716-446655440000"));
assert!(!is_uuid("nope"));
}
#[test]
fn iso_date_accepts_well_formed() {
assert!(validate_iso_date("2026-01-15").is_ok());
assert!(validate_iso_date("1970-01-01").is_ok());
assert!(validate_iso_date("9999-12-31").is_ok());
}
#[test]
fn iso_date_rejects_out_of_range() {
assert!(validate_iso_date("2026-02-30").is_err()); assert!(validate_iso_date("2026-13-01").is_err()); assert!(validate_iso_date("2026-00-01").is_err()); assert!(validate_iso_date("2026-01-32").is_err()); }
#[test]
fn iso_date_rejects_wrong_format() {
assert!(validate_iso_date("01/15/2026").is_err()); assert!(validate_iso_date("2026-01-15T00:00:00").is_err()); assert!(validate_iso_date("").is_err());
}
#[test]
fn iso_time_accepts_well_formed() {
assert!(validate_iso_time("14:30:00").is_ok());
assert!(validate_iso_time("00:00:00").is_ok());
assert!(validate_iso_time("23:59:59").is_ok());
}
#[test]
fn iso_time_accepts_fractional_seconds() {
assert!(validate_iso_time("14:30:00.123").is_ok());
assert!(validate_iso_time("14:30:00.123456").is_ok());
}
#[test]
fn iso_time_rejects_out_of_range() {
assert!(validate_iso_time("24:00:00").is_err()); assert!(validate_iso_time("14:60:00").is_err()); }
#[test]
fn iso_time_rejects_wrong_format() {
assert!(validate_iso_time("2:30 PM").is_err());
assert!(validate_iso_time("14:30").is_err()); assert!(validate_iso_time("").is_err());
}
#[test]
fn iso_datetime_accepts_z_offset() {
assert!(validate_iso_datetime("2026-01-15T14:30:00Z").is_ok());
assert!(validate_iso_datetime("2026-01-15T14:30:00.123Z").is_ok());
}
#[test]
fn iso_datetime_accepts_explicit_offset() {
assert!(validate_iso_datetime("2026-01-15T14:30:00+02:00").is_ok());
assert!(validate_iso_datetime("2026-01-15T14:30:00-05:00").is_ok());
}
#[test]
fn iso_datetime_rejects_naive_datetime() {
assert!(validate_iso_datetime("2026-01-15T14:30:00").is_err());
}
#[test]
fn iso_datetime_rejects_garbage() {
let e = validate_iso_datetime("not a date").unwrap_err();
assert_eq!(e.code, "invalid_iso_datetime");
}
#[test]
fn alphanumeric_accepts_letters_and_digits() {
assert!(validate_alphanumeric("abc123").is_ok());
assert!(validate_alphanumeric("ABC").is_ok());
assert!(validate_alphanumeric("9").is_ok());
}
#[test]
fn alphanumeric_rejects_punctuation_spaces_and_empty() {
assert!(validate_alphanumeric("abc 123").is_err());
assert!(validate_alphanumeric("abc-123").is_err());
assert!(validate_alphanumeric("abc!").is_err());
assert!(validate_alphanumeric("").is_err());
}
#[test]
fn alphanumeric_rejects_non_ascii_letters() {
assert!(validate_alphanumeric("café").is_err());
}
#[test]
fn numeric_accepts_digits_only() {
assert!(validate_numeric("123").is_ok());
assert!(validate_numeric("0").is_ok());
}
#[test]
fn numeric_rejects_signs_decimal_letters_empty() {
assert!(validate_numeric("-1").is_err());
assert!(validate_numeric("3.14").is_err());
assert!(validate_numeric("12a").is_err());
assert!(validate_numeric("").is_err());
}
#[test]
fn alpha_accepts_letters_only() {
assert!(validate_alpha("Alice").is_ok());
assert!(validate_alpha("Z").is_ok());
}
#[test]
fn alpha_rejects_digits_punctuation_empty() {
assert!(validate_alpha("abc1").is_err());
assert!(validate_alpha("a b").is_err());
assert!(validate_alpha("a-b").is_err());
assert!(validate_alpha("").is_err());
}
#[test]
fn luhn_accepts_known_valid_pans() {
assert!(validate_creditcard_luhn("4111111111111111").is_ok());
assert!(validate_creditcard_luhn("5555555555554444").is_ok());
assert!(validate_creditcard_luhn("378282246310005").is_ok());
assert!(validate_creditcard_luhn("6011111111111117").is_ok());
}
#[test]
fn luhn_strips_spaces_and_hyphens() {
assert!(validate_creditcard_luhn("4111 1111 1111 1111").is_ok());
assert!(validate_creditcard_luhn("4111-1111-1111-1111").is_ok());
assert!(validate_creditcard_luhn(" 4111-1111 1111-1111 ").is_ok());
}
#[test]
fn luhn_rejects_wrong_checksum() {
let e = validate_creditcard_luhn("4111111111111112").unwrap_err();
assert_eq!(e.code, "invalid_card_number");
}
#[test]
fn luhn_rejects_non_digit_chars() {
assert!(validate_creditcard_luhn("4111-1111-1111-abcd").is_err());
}
#[test]
fn luhn_rejects_too_short_or_too_long() {
assert!(validate_creditcard_luhn("41111111111").is_err());
assert!(validate_creditcard_luhn("41111111111111111111").is_err());
}
#[test]
fn luhn_rejects_empty_and_whitespace_only() {
assert!(validate_creditcard_luhn("").is_err());
assert!(validate_creditcard_luhn(" ").is_err());
}
#[test]
fn isbn10_accepts_real_books() {
assert!(validate_isbn("0131103628").is_ok());
assert!(validate_isbn("080442957X").is_ok());
assert!(validate_isbn("080442957x").is_ok());
}
#[test]
fn isbn13_accepts_real_books() {
assert!(validate_isbn("9780131103627").is_ok());
}
#[test]
fn isbn_strips_spaces_and_hyphens() {
assert!(validate_isbn("0-13-110362-8").is_ok());
assert!(validate_isbn("978-0-13-110362-7").is_ok());
assert!(validate_isbn(" 978 0 13 110362 7 ").is_ok());
}
#[test]
fn isbn_rejects_wrong_checksum() {
let e = validate_isbn("0131103627").unwrap_err();
assert_eq!(e.code, "invalid_isbn");
assert!(validate_isbn("9780131103620").is_err());
}
#[test]
fn isbn_rejects_wrong_length() {
assert!(validate_isbn("01311036280").is_err());
assert!(validate_isbn("97801311036270").is_err());
assert!(validate_isbn("").is_err());
}
#[test]
fn isbn_rejects_non_digit_chars() {
assert!(validate_isbn("01311a3628").is_err());
assert!(validate_isbn("9780131103X27").is_err());
assert!(validate_isbn("X131103628").is_err());
}
#[test]
fn hostname_accepts_common_shapes() {
assert!(validate_hostname("example.com").is_ok());
assert!(validate_hostname("sub.example.co.uk").is_ok());
assert!(validate_hostname("localhost").is_ok()); assert!(validate_hostname("api-v1.example.com").is_ok()); assert!(validate_hostname("123.example.com").is_ok()); }
#[test]
fn hostname_rejects_leading_or_trailing_hyphen() {
assert!(validate_hostname("-bad.example.com").is_err());
assert!(validate_hostname("example-.com").is_err());
assert!(validate_hostname("sub.-bad.com").is_err());
}
#[test]
fn hostname_rejects_leading_or_trailing_dot() {
assert!(validate_hostname(".example.com").is_err());
assert!(validate_hostname("example.com.").is_err());
}
#[test]
fn hostname_rejects_empty_label_between_dots() {
assert!(validate_hostname("example..com").is_err());
}
#[test]
fn hostname_rejects_invalid_chars() {
assert!(validate_hostname("example.com/path").is_err());
assert!(validate_hostname("ex_ample.com").is_err()); assert!(validate_hostname("ex ample.com").is_err());
assert!(validate_hostname("café.com").is_err()); }
#[test]
fn hostname_rejects_oversize_label() {
let long_label: String = "a".repeat(64);
assert!(validate_hostname(&format!("{long_label}.com")).is_err());
let max_label: String = "a".repeat(63);
assert!(validate_hostname(&format!("{max_label}.com")).is_ok());
}
#[test]
fn hostname_rejects_oversize_total() {
let label = "a".repeat(63);
let too_long = format!("{label}.{label}.{label}.{label}xx"); assert!(validate_hostname(&too_long).is_err());
}
#[test]
fn hostname_rejects_empty() {
assert!(validate_hostname("").is_err());
}
#[test]
fn iban_accepts_known_valid_examples() {
assert!(validate_iban("GB82WEST12345698765432").is_ok());
assert!(validate_iban("DE89370400440532013000").is_ok());
assert!(validate_iban("FR1420041010050500013M02606").is_ok());
assert!(validate_iban("NO9386011117947").is_ok());
}
#[test]
fn iban_strips_spaces() {
assert!(validate_iban("GB82 WEST 1234 5698 7654 32").is_ok());
}
#[test]
fn iban_rejects_wrong_checksum() {
let e = validate_iban("GB82WEST12345698765431").unwrap_err();
assert_eq!(e.code, "invalid_iban");
}
#[test]
fn iban_rejects_wrong_format() {
assert!(validate_iban("gb82WEST12345698765432").is_err());
assert!(validate_iban("1B82WEST12345698765432").is_err());
assert!(validate_iban("GBAB12345678901234567890").is_err());
assert!(validate_iban("GB82WEST!2345698765432").is_err());
}
#[test]
fn iban_rejects_out_of_range_length() {
assert!(validate_iban("GB82").is_err());
let too_long = format!("GB82{}", "X".repeat(31));
assert!(validate_iban(&too_long).is_err());
}
#[test]
fn iban_rejects_empty_and_whitespace_only() {
assert!(validate_iban("").is_err());
assert!(validate_iban(" ").is_err());
}
#[test]
fn mac_accepts_colon_separated() {
assert!(validate_mac_address("00:1A:2B:3C:4D:5E").is_ok());
assert!(validate_mac_address("FF:FF:FF:FF:FF:FF").is_ok());
assert!(validate_mac_address("00:00:00:00:00:00").is_ok());
}
#[test]
fn mac_accepts_hyphen_separated() {
assert!(validate_mac_address("00-1A-2B-3C-4D-5E").is_ok());
}
#[test]
fn mac_accepts_lowercase_hex() {
assert!(validate_mac_address("00:1a:2b:3c:4d:5e").is_ok());
assert!(validate_mac_address("ff:ff:ff:ff:ff:ff").is_ok());
}
#[test]
fn mac_rejects_no_separators() {
assert!(validate_mac_address("001A2B3C4D5E").is_err());
}
#[test]
fn mac_rejects_mixed_separators() {
assert!(validate_mac_address("00:1A:2B-3C:4D:5E").is_err());
assert!(validate_mac_address("00-1A:2B:3C:4D:5E").is_err());
}
#[test]
fn mac_rejects_non_hex_chars() {
assert!(validate_mac_address("00:1A:2B:3C:4D:5Z").is_err());
assert!(validate_mac_address("00:1G:2B:3C:4D:5E").is_err());
}
#[test]
fn mac_rejects_wrong_octet_length() {
assert!(validate_mac_address("0:1A:2B:3C:4D:5E").is_err());
assert!(validate_mac_address("000:1A:2B:3C:4D:5E").is_err());
}
#[test]
fn mac_rejects_wrong_total_length() {
assert!(validate_mac_address("").is_err());
assert!(validate_mac_address("00:1A:2B:3C:4D").is_err()); assert!(validate_mac_address("00:1A:2B:3C:4D:5E:6F").is_err()); }
#[test]
fn base64_accepts_standard_alphabet() {
assert!(validate_base64("SGVsbG8=").is_ok());
assert!(validate_base64("TWFueSBoYW5kcw==").is_ok());
assert!(validate_base64("abcd").is_ok());
}
#[test]
fn base64_accepts_plus_and_slash() {
assert!(validate_base64("AB+/").is_ok());
}
#[test]
fn base64_rejects_urlsafe_chars() {
assert!(validate_base64("AB-_").is_err());
}
#[test]
fn base64_rejects_bad_padding_count() {
assert!(validate_base64("AB===").is_err());
}
#[test]
fn base64_rejects_non_multiple_of_4() {
assert!(validate_base64("ABCDE=").is_err()); }
#[test]
fn base64_rejects_empty() {
assert!(validate_base64("").is_err());
assert!(validate_base64_urlsafe("").is_err());
}
#[test]
fn base64_urlsafe_accepts_dash_and_underscore() {
assert!(validate_base64_urlsafe("AB-_").is_ok());
assert!(validate_base64_urlsafe("SGVsbG8=").is_ok()); }
#[test]
fn base64_urlsafe_rejects_plus_and_slash() {
assert!(validate_base64_urlsafe("AB+/").is_err());
}
#[test]
fn base64_urlsafe_accepts_unpadded() {
assert!(validate_base64_urlsafe("ABCDE").is_ok());
}
#[test]
fn jwt_shape_accepts_valid_three_segments() {
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.\
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
assert!(validate_jwt_shape(jwt).is_ok());
}
#[test]
fn jwt_shape_rejects_wrong_segment_count() {
assert!(validate_jwt_shape("abc.def").is_err()); assert!(validate_jwt_shape("a.b.c.d").is_err()); assert!(validate_jwt_shape("no-dots").is_err());
}
#[test]
fn jwt_shape_rejects_empty_segments() {
assert!(validate_jwt_shape(".payload.sig").is_err());
assert!(validate_jwt_shape("header..sig").is_err());
assert!(validate_jwt_shape("header.payload.").is_err());
}
#[test]
fn jwt_shape_rejects_non_urlsafe_chars_in_segment() {
assert!(validate_jwt_shape("abc.de+f.ghi").is_err());
assert!(validate_jwt_shape("abc.def.gh/i").is_err());
}
#[test]
fn jwt_shape_rejects_empty() {
assert!(validate_jwt_shape("").is_err());
}
#[test]
fn semver_accepts_canonical_form() {
assert!(validate_semver("1.0.0").is_ok());
assert!(validate_semver("0.0.1").is_ok());
assert!(validate_semver("10.20.30").is_ok());
}
#[test]
fn semver_accepts_pre_release() {
assert!(validate_semver("1.0.0-alpha").is_ok());
assert!(validate_semver("1.0.0-alpha.1").is_ok());
assert!(validate_semver("1.0.0-0.3.7").is_ok());
assert!(validate_semver("1.0.0-x-y-z.--").is_ok());
}
#[test]
fn semver_accepts_build_metadata() {
assert!(validate_semver("1.0.0+20130313144700").is_ok());
assert!(validate_semver("1.0.0+exp.sha.5114f85").is_ok());
assert!(validate_semver("1.0.0+007").is_ok());
}
#[test]
fn semver_accepts_full_form() {
assert!(validate_semver("1.0.0-rc.1+build.42").is_ok());
}
#[test]
fn semver_rejects_missing_core_parts() {
assert!(validate_semver("1").is_err());
assert!(validate_semver("1.0").is_err());
assert!(validate_semver("1.0.0.0").is_err());
assert!(validate_semver("").is_err());
}
#[test]
fn semver_rejects_leading_zero_in_core() {
assert!(validate_semver("01.0.0").is_err());
assert!(validate_semver("1.02.0").is_err());
assert!(validate_semver("1.0.03").is_err());
assert!(validate_semver("0.0.0").is_ok());
}
#[test]
fn semver_rejects_leading_zero_in_numeric_prerelease_id() {
assert!(validate_semver("1.0.0-01").is_err());
assert!(validate_semver("1.0.0-alpha.01").is_err());
}
#[test]
fn semver_rejects_empty_prerelease_or_build() {
assert!(validate_semver("1.0.0-").is_err());
assert!(validate_semver("1.0.0+").is_err());
assert!(validate_semver("1.0.0-alpha..1").is_err());
}
#[test]
fn semver_rejects_invalid_chars() {
assert!(validate_semver("1.0.0-alpha_1").is_err()); assert!(validate_semver("1.0.0-alpha 1").is_err()); assert!(validate_semver("v1.0.0").is_err()); }
#[test]
fn country_code_accepts_two_uppercase_letters() {
assert!(validate_country_code("US").is_ok());
assert!(validate_country_code("GB").is_ok());
assert!(validate_country_code("DE").is_ok());
assert!(validate_country_code("JP").is_ok());
assert!(validate_country_code("ZZ").is_ok());
}
#[test]
fn country_code_rejects_wrong_length() {
assert!(validate_country_code("U").is_err());
assert!(validate_country_code("USA").is_err()); assert!(validate_country_code("").is_err());
}
#[test]
fn country_code_rejects_lowercase_or_non_letters() {
assert!(validate_country_code("us").is_err());
assert!(validate_country_code("Us").is_err());
assert!(validate_country_code("U1").is_err());
assert!(validate_country_code("U-").is_err());
}
#[test]
fn currency_code_accepts_three_uppercase_letters() {
assert!(validate_currency_code("USD").is_ok());
assert!(validate_currency_code("EUR").is_ok());
assert!(validate_currency_code("GBP").is_ok());
assert!(validate_currency_code("JPY").is_ok());
assert!(validate_currency_code("XXX").is_ok()); }
#[test]
fn currency_code_rejects_wrong_length() {
assert!(validate_currency_code("US").is_err());
assert!(validate_currency_code("USDD").is_err());
assert!(validate_currency_code("").is_err());
}
#[test]
fn currency_code_rejects_lowercase_or_non_letters() {
assert!(validate_currency_code("usd").is_err());
assert!(validate_currency_code("UsD").is_err());
assert!(validate_currency_code("U5D").is_err());
}
#[test]
fn language_tag_accepts_bare_lang() {
assert!(validate_language_tag("en").is_ok());
assert!(validate_language_tag("fr").is_ok());
assert!(validate_language_tag("zh").is_ok());
assert!(validate_language_tag("eng").is_ok());
}
#[test]
fn language_tag_accepts_lang_with_region() {
assert!(validate_language_tag("en-US").is_ok());
assert!(validate_language_tag("fr-CA").is_ok());
assert!(validate_language_tag("pt-BR").is_ok());
assert!(validate_language_tag("es-419").is_ok());
}
#[test]
fn language_tag_accepts_lang_with_script() {
assert!(validate_language_tag("zh-Hans").is_ok());
assert!(validate_language_tag("sr-Cyrl").is_ok());
assert!(validate_language_tag("zh-Hans-CN").is_ok());
}
#[test]
fn language_tag_rejects_uppercase_lang() {
assert!(validate_language_tag("EN").is_err());
assert!(validate_language_tag("En").is_err());
}
#[test]
fn language_tag_rejects_wrong_region_case() {
assert!(validate_language_tag("en-us").is_err());
assert!(validate_language_tag("en-Us").is_err());
}
#[test]
fn language_tag_rejects_wrong_lang_length() {
assert!(validate_language_tag("e").is_err());
assert!(validate_language_tag("english").is_err());
}
#[test]
fn language_tag_rejects_too_many_parts() {
assert!(validate_language_tag("en-US-x-something").is_err());
}
#[test]
fn language_tag_rejects_empty_or_garbage() {
assert!(validate_language_tag("").is_err());
assert!(validate_language_tag("not a tag").is_err());
assert!(validate_language_tag("en_US").is_err()); }
#[test]
fn postal_code_us_accepts_five_digit_zip() {
assert!(validate_postal_code_us("94110").is_ok());
assert!(validate_postal_code_us("00501").is_ok()); assert!(validate_postal_code_us("99950").is_ok());
}
#[test]
fn postal_code_us_accepts_zip_plus_four() {
assert!(validate_postal_code_us("12345-6789").is_ok());
assert!(validate_postal_code_us("94110-0001").is_ok());
}
#[test]
fn postal_code_us_rejects_wrong_length() {
assert!(validate_postal_code_us("1234").is_err()); assert!(validate_postal_code_us("123456").is_err()); assert!(validate_postal_code_us("123456789").is_err()); }
#[test]
fn postal_code_us_rejects_bad_plus_four_shape() {
assert!(validate_postal_code_us("94110-").is_err()); assert!(validate_postal_code_us("9411-12345").is_err()); assert!(validate_postal_code_us("94110-12").is_err()); assert!(validate_postal_code_us("94110-123A").is_err()); }
#[test]
fn postal_code_us_rejects_non_digit_in_base() {
assert!(validate_postal_code_us("9411A").is_err());
assert!(validate_postal_code_us("").is_err());
}
#[test]
fn postal_code_ca_accepts_canonical_shape() {
assert!(validate_postal_code_ca("K1A 0B1").is_ok()); assert!(validate_postal_code_ca("M5W 1E6").is_ok()); assert!(validate_postal_code_ca("V6B 4Y8").is_ok()); }
#[test]
fn postal_code_ca_rejects_lowercase_letters() {
assert!(validate_postal_code_ca("k1a 0b1").is_err());
assert!(validate_postal_code_ca("K1a 0B1").is_err());
}
#[test]
fn postal_code_ca_rejects_missing_space_or_wrong_separator() {
assert!(validate_postal_code_ca("K1A0B1").is_err()); assert!(validate_postal_code_ca("K1A-0B1").is_err()); assert!(validate_postal_code_ca("K1A 0B1").is_err()); }
#[test]
fn postal_code_ca_rejects_wrong_pattern() {
assert!(validate_postal_code_ca("1AB 0B1").is_err()); assert!(validate_postal_code_ca("AAA 0B1").is_err()); assert!(validate_postal_code_ca("K1A 000").is_err()); assert!(validate_postal_code_ca("").is_err());
}
#[test]
fn postal_code_uk_accepts_canonical_shapes() {
assert!(validate_postal_code_uk("M1 1AA").is_ok()); assert!(validate_postal_code_uk("B33 8TH").is_ok()); assert!(validate_postal_code_uk("CR2 6XH").is_ok()); assert!(validate_postal_code_uk("DN55 1PT").is_ok()); assert!(validate_postal_code_uk("W1A 1AA").is_ok()); assert!(validate_postal_code_uk("EC1A 1BB").is_ok()); assert!(validate_postal_code_uk("SW1A 1AA").is_ok()); }
#[test]
fn postal_code_uk_rejects_missing_space() {
assert!(validate_postal_code_uk("SW1A1AA").is_err());
}
#[test]
fn postal_code_uk_rejects_lowercase() {
assert!(validate_postal_code_uk("sw1a 1aa").is_err());
}
#[test]
fn postal_code_uk_rejects_wrong_inward_length() {
assert!(validate_postal_code_uk("M1 1A").is_err());
assert!(validate_postal_code_uk("M1 1AAA").is_err());
}
#[test]
fn postal_code_uk_rejects_wrong_inward_pattern() {
assert!(validate_postal_code_uk("M1 AAA").is_err()); assert!(validate_postal_code_uk("M1 123").is_err()); }
#[test]
fn postal_code_uk_rejects_outward_starting_with_digit() {
assert!(validate_postal_code_uk("1A 1AA").is_err());
}
#[test]
fn postal_code_uk_rejects_empty() {
assert!(validate_postal_code_uk("").is_err());
}
}