use cel::{
Context, ExecutionError, ResolveResult,
extractors::This,
objects::{Opaque, OptionalValue, Value},
};
use std::{fmt, sync::Arc};
#[derive(Debug, Clone, PartialEq, Eq)]
enum FormatKind {
Dns1123Label,
Dns1123Subdomain,
Dns1035Label,
Dns1035LabelPrefix,
Dns1123LabelPrefix,
Dns1123SubdomainPrefix,
QualifiedName,
LabelValue,
Uri,
Uuid,
Byte,
Date,
DateTime,
}
impl fmt::Display for FormatKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
FormatKind::Dns1123Label => "dns1123Label",
FormatKind::Dns1123Subdomain => "dns1123Subdomain",
FormatKind::Dns1035Label => "dns1035Label",
FormatKind::Dns1035LabelPrefix => "dns1035LabelPrefix",
FormatKind::Dns1123LabelPrefix => "dns1123LabelPrefix",
FormatKind::Dns1123SubdomainPrefix => "dns1123SubdomainPrefix",
FormatKind::QualifiedName => "qualifiedName",
FormatKind::LabelValue => "labelValue",
FormatKind::Uri => "uri",
FormatKind::Uuid => "uuid",
FormatKind::Byte => "byte",
FormatKind::Date => "date",
FormatKind::DateTime => "datetime",
};
write!(f, "{name}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct KubeFormat(FormatKind);
impl Opaque for KubeFormat {
fn runtime_type_name(&self) -> &str {
"kubernetes.Format"
}
}
pub fn register(ctx: &mut Context<'_>) {
ctx.add_function("format.dns1123Label", format_dns1123_label);
ctx.add_function("format.dns1123Subdomain", format_dns1123_subdomain);
ctx.add_function("format.dns1035Label", format_dns1035_label);
ctx.add_function("format.dns1035LabelPrefix", format_dns1035_label_prefix);
ctx.add_function("format.dns1123LabelPrefix", format_dns1123_label_prefix);
ctx.add_function("format.dns1123SubdomainPrefix", format_dns1123_subdomain_prefix);
ctx.add_function("format.qualifiedName", format_qualified_name);
ctx.add_function("format.labelValue", format_label_value);
ctx.add_function("format.uri", format_uri);
ctx.add_function("format.uuid", format_uuid);
ctx.add_function("format.byte", format_byte);
ctx.add_function("format.date", format_date);
ctx.add_function("format.datetime", format_datetime);
ctx.add_function("format.named", format_named);
ctx.add_function("validate", format_validate);
}
macro_rules! format_factory {
($fn_name:ident, $kind:ident) => {
fn $fn_name() -> ResolveResult {
Ok(Value::Opaque(Arc::new(KubeFormat(FormatKind::$kind))))
}
};
}
format_factory!(format_dns1123_label, Dns1123Label);
format_factory!(format_dns1123_subdomain, Dns1123Subdomain);
format_factory!(format_dns1035_label, Dns1035Label);
format_factory!(format_dns1035_label_prefix, Dns1035LabelPrefix);
format_factory!(format_dns1123_label_prefix, Dns1123LabelPrefix);
format_factory!(format_dns1123_subdomain_prefix, Dns1123SubdomainPrefix);
format_factory!(format_qualified_name, QualifiedName);
format_factory!(format_label_value, LabelValue);
format_factory!(format_uri, Uri);
format_factory!(format_uuid, Uuid);
format_factory!(format_byte, Byte);
format_factory!(format_date, Date);
format_factory!(format_datetime, DateTime);
fn format_named(name: Arc<String>) -> ResolveResult {
let kind = match name.as_str() {
"dns1123Label" => FormatKind::Dns1123Label,
"dns1123Subdomain" => FormatKind::Dns1123Subdomain,
"dns1035Label" => FormatKind::Dns1035Label,
"dns1035LabelPrefix" => FormatKind::Dns1035LabelPrefix,
"dns1123LabelPrefix" => FormatKind::Dns1123LabelPrefix,
"dns1123SubdomainPrefix" => FormatKind::Dns1123SubdomainPrefix,
"qualifiedName" => FormatKind::QualifiedName,
"labelValue" => FormatKind::LabelValue,
"uri" => FormatKind::Uri,
"uuid" => FormatKind::Uuid,
"byte" => FormatKind::Byte,
"date" => FormatKind::Date,
"datetime" => FormatKind::DateTime,
_ => return Ok(Value::Null),
};
Ok(Value::Opaque(Arc::new(KubeFormat(kind))))
}
pub(crate) fn format_validate(This(this): This<Value>, s: Arc<String>) -> ResolveResult {
let fmt = match &this {
Value::Opaque(o) => o
.downcast_ref::<KubeFormat>()
.ok_or_else(|| ExecutionError::function_error("validate", "expected Format type"))?,
_ => {
return Err(ExecutionError::function_error("validate", "expected Format type"));
}
};
let errors = validate_format(&fmt.0, &s);
if errors.is_empty() {
Ok(Value::Opaque(Arc::new(OptionalValue::none())))
} else {
let list: Vec<Value> = errors.into_iter().map(|e| Value::String(Arc::new(e))).collect();
Ok(Value::Opaque(Arc::new(OptionalValue::of(Value::List(Arc::new(
list,
))))))
}
}
fn validate_format(kind: &FormatKind, s: &str) -> Vec<String> {
match kind {
FormatKind::Dns1123Label => validate_dns1123_label(s),
FormatKind::Dns1123Subdomain => validate_dns1123_subdomain(s),
FormatKind::Dns1035Label => validate_dns1035_label(s),
FormatKind::Dns1035LabelPrefix => validate_dns1035_label_prefix(s),
FormatKind::Dns1123LabelPrefix => validate_dns1123_label_prefix(s),
FormatKind::Dns1123SubdomainPrefix => validate_dns1123_subdomain_prefix(s),
FormatKind::QualifiedName => validate_qualified_name(s),
FormatKind::LabelValue => validate_label_value(s),
FormatKind::Uri => validate_uri(s),
FormatKind::Uuid => validate_uuid(s),
FormatKind::Byte => validate_byte(s),
FormatKind::Date => validate_date(s),
FormatKind::DateTime => validate_datetime(s),
}
}
fn validate_dns1123_label(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_dns1123_label_char_set(s) {
errors.push("must consist of lower case alphanumeric characters or '-'".to_string());
}
if s.starts_with('-') || s.ends_with('-') {
errors.push("must start and end with an alphanumeric character".to_string());
}
errors
}
fn is_dns1123_label_char_set(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
fn validate_dns1035_label(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_dns1035_char_set(s) {
errors.push("must consist of lower case alphanumeric characters or '-'".to_string());
}
if !s.starts_with(|c: char| c.is_ascii_lowercase()) {
errors.push("must start with a lowercase alphabetic character".to_string());
}
if s.ends_with('-') {
errors.push("must end with an alphanumeric character".to_string());
}
errors
}
fn is_dns1035_char_set(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
fn validate_dns1035_label_prefix(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_dns1035_char_set(s) {
errors.push("must consist of lower case alphanumeric characters or '-'".to_string());
}
if !s.starts_with(|c: char| c.is_ascii_lowercase()) {
errors.push("must start with a lowercase alphabetic character".to_string());
}
errors
}
fn validate_dns1123_subdomain(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 253 {
errors.push(format!("must be no more than 253 characters (is {})", s.len()));
}
for part in s.split('.') {
let part_errors = validate_dns1123_label(part);
errors.extend(part_errors);
}
errors
}
fn validate_dns1123_label_prefix(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_dns1123_label_char_set(s) {
errors.push("must consist of lower case alphanumeric characters or '-'".to_string());
}
if s.starts_with('-') {
errors.push("must start with an alphanumeric character".to_string());
}
errors
}
fn validate_dns1123_subdomain_prefix(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 253 {
errors.push(format!("must be no more than 253 characters (is {})", s.len()));
}
let parts: Vec<&str> = s.split('.').collect();
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
let part_errors = validate_dns1123_label_prefix(part);
errors.extend(part_errors);
} else {
let part_errors = validate_dns1123_label(part);
errors.extend(part_errors);
}
}
errors
}
fn validate_qualified_name(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
let (prefix, name) = if let Some(slash_pos) = s.rfind('/') {
let prefix = &s[..slash_pos];
let name = &s[slash_pos + 1..];
if prefix.is_empty() {
errors.push("prefix must be non-empty".to_string());
} else {
let prefix_errors = validate_dns1123_subdomain(prefix);
for e in prefix_errors {
errors.push(format!("prefix: {e}"));
}
}
(Some(prefix), name)
} else {
(None, s)
};
let name_errors = validate_qualified_name_local(name);
if prefix.is_some() {
for e in name_errors {
errors.push(format!("name: {e}"));
}
} else {
errors.extend(name_errors);
}
errors
}
fn validate_qualified_name_local(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_qualified_name_char_set(s) {
errors.push("must consist of alphanumeric characters, '-', '_', or '.'".to_string());
}
if !s.starts_with(|c: char| c.is_ascii_alphanumeric()) {
errors.push("must start with an alphanumeric character".to_string());
}
if !s.ends_with(|c: char| c.is_ascii_alphanumeric()) {
errors.push("must end with an alphanumeric character".to_string());
}
errors
}
fn is_qualified_name_char_set(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
fn validate_label_value(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
return errors;
}
if s.len() > 63 {
errors.push(format!("must be no more than 63 characters (is {})", s.len()));
}
if !is_label_value_char_set(s) {
errors.push("must consist of alphanumeric characters, '-', '_', or '.'".to_string());
}
if !s.starts_with(|c: char| c.is_ascii_alphanumeric()) {
errors.push("must start with an alphanumeric character".to_string());
}
if !s.ends_with(|c: char| c.is_ascii_alphanumeric()) {
errors.push("must end with an alphanumeric character".to_string());
}
errors
}
fn is_label_value_char_set(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
fn validate_uri(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be non-empty".to_string());
return errors;
}
let scheme_end = s.find(':');
match scheme_end {
None => {
errors.push("must have a scheme (e.g., 'https:')".to_string());
}
Some(pos) => {
let scheme = &s[..pos];
if scheme.is_empty()
|| !scheme.starts_with(|c: char| c.is_ascii_alphabetic())
|| !scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
{
errors.push("invalid scheme".to_string());
}
}
}
errors
}
fn validate_uuid(s: &str) -> Vec<String> {
let mut errors = Vec::new();
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 5 {
errors
.push("must be in the form 8-4-4-4-12 (e.g., xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)".to_string());
return errors;
}
let expected_lens = [8, 4, 4, 4, 12];
let mut valid = true;
for (part, &expected_len) in parts.iter().zip(expected_lens.iter()) {
if part.len() != expected_len || !part.chars().all(|c| c.is_ascii_hexdigit()) {
valid = false;
break;
}
}
if !valid {
errors
.push("must be in the form 8-4-4-4-12 (e.g., xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)".to_string());
}
errors
}
fn validate_byte(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
return errors;
}
let stripped = s.trim_end_matches('=');
let valid = stripped
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '-' || c == '_');
if !valid {
errors.push("must be a valid base64 encoded string".to_string());
return errors;
}
let padding_count = s.len() - stripped.len();
if padding_count > 2 {
errors.push("must be a valid base64 encoded string".to_string());
return errors;
}
let remainder = stripped.len() % 4;
if remainder == 1 {
errors.push("must be a valid base64 encoded string".to_string());
return errors;
}
if padding_count > 0 && !s.len().is_multiple_of(4) {
errors.push("must be a valid base64 encoded string".to_string());
}
errors
}
fn validate_date(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.len() != 10 {
errors.push("must be a date in YYYY-MM-DD format".to_string());
return errors;
}
let bytes = s.as_bytes();
if bytes[4] != b'-' || bytes[7] != b'-' {
errors.push("must be a date in YYYY-MM-DD format".to_string());
return errors;
}
let year_str = &s[0..4];
let month_str = &s[5..7];
let day_str = &s[8..10];
let Ok(year) = year_str.parse::<u32>() else {
errors.push("must be a date in YYYY-MM-DD format".to_string());
return errors;
};
let Ok(month) = month_str.parse::<u32>() else {
errors.push("must be a date in YYYY-MM-DD format".to_string());
return errors;
};
let Ok(day) = day_str.parse::<u32>() else {
errors.push("must be a date in YYYY-MM-DD format".to_string());
return errors;
};
if !(1..=12).contains(&month) {
errors.push("must be a valid date".to_string());
return errors;
}
let max_day = days_in_month(year, month);
if day < 1 || day > max_day {
errors.push("must be a valid date".to_string());
}
errors
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
fn is_leap_year(year: u32) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
fn validate_datetime(s: &str) -> Vec<String> {
let mut errors = Vec::new();
if s.is_empty() {
errors.push("must be a valid RFC 3339 date-time".to_string());
return errors;
}
let t_pos = s.find(['T', 't']);
let Some(t_pos) = t_pos else {
errors.push("must be a valid RFC 3339 date-time".to_string());
return errors;
};
let date_part = &s[..t_pos];
let date_errors = validate_date(date_part);
if !date_errors.is_empty() {
errors.push("must be a valid RFC 3339 date-time".to_string());
return errors;
}
let time_part = &s[t_pos + 1..];
if time_part.is_empty() {
errors.push("must be a valid RFC 3339 date-time".to_string());
return errors;
}
let (time_str, tz_str) = find_timezone(time_part);
if tz_str.is_empty() {
errors.push("must have a timezone".to_string());
return errors;
}
if !validate_time_str(time_str) {
errors.push("must be a valid RFC 3339 date-time".to_string());
return errors;
}
if !validate_timezone(tz_str) {
errors.push("must be a valid RFC 3339 date-time".to_string());
}
errors
}
fn find_timezone(time_part: &str) -> (&str, &str) {
if let Some(pos) = time_part.rfind(['Z', 'z']) {
return (&time_part[..pos], &time_part[pos..]);
}
for (i, ch) in time_part.char_indices().rev() {
if (ch == '+' || ch == '-') && i >= 2 {
return (&time_part[..i], &time_part[i..]);
}
}
(time_part, "")
}
fn validate_time_str(s: &str) -> bool {
if s.len() < 8 {
return false;
}
let bytes = s.as_bytes();
if bytes[2] != b':' || bytes[5] != b':' {
return false;
}
let Ok(hour) = s[0..2].parse::<u32>() else {
return false;
};
let Ok(min) = s[3..5].parse::<u32>() else {
return false;
};
let Ok(sec) = s[6..8].parse::<u32>() else {
return false;
};
if hour > 23 || min > 59 || sec > 60 {
return false;
}
if s.len() > 8 {
if bytes[8] != b'.' {
return false;
}
let frac = &s[9..];
if frac.is_empty() || !frac.chars().all(|c| c.is_ascii_digit()) {
return false;
}
}
true
}
fn validate_timezone(tz: &str) -> bool {
if tz == "Z" || tz == "z" {
return true;
}
if tz.len() != 6 {
return false;
}
let bytes = tz.as_bytes();
if bytes[0] != b'+' && bytes[0] != b'-' {
return false;
}
if bytes[3] != b':' {
return false;
}
let Ok(hour) = tz[1..3].parse::<u32>() else {
return false;
};
let Ok(min) = tz[4..6].parse::<u32>() else {
return false;
};
hour <= 23 && min <= 59
}
#[cfg(test)]
mod tests {
use super::*;
use cel::Program;
fn eval(expr: &str) -> Value {
let mut ctx = Context::default();
register(&mut ctx);
Program::compile(expr).unwrap().execute(&ctx).unwrap()
}
fn assert_valid(expr: &str) {
assert_eq!(
eval(&format!("{expr}.hasValue()")),
Value::Bool(false),
"expected valid (optional.none) for {expr}"
);
}
fn assert_invalid(expr: &str) {
assert_eq!(
eval(&format!("{expr}.hasValue()")),
Value::Bool(true),
"expected invalid (optional.of) for {expr}"
);
}
#[test]
fn test_dns1123_label_valid() {
assert_valid("format.dns1123Label().validate('my-name')");
assert_valid("format.dns1123Label().validate('a')");
assert_valid("format.dns1123Label().validate('abc123')");
assert_valid("format.dns1123Label().validate('a-b')");
}
#[test]
fn test_dns1123_label_invalid() {
assert_invalid("format.dns1123Label().validate('')");
assert_invalid("format.dns1123Label().validate('-start')");
assert_invalid("format.dns1123Label().validate('end-')");
assert_invalid("format.dns1123Label().validate('UPPER')");
assert_invalid("format.dns1123Label().validate('has.dot')");
let long = "a".repeat(64);
assert_invalid(&format!("format.dns1123Label().validate('{long}')"));
}
#[test]
fn test_dns1123_label_max_length() {
let max = "a".repeat(63);
assert_valid(&format!("format.dns1123Label().validate('{max}')"));
}
#[test]
fn test_dns1035_label_valid() {
assert_valid("format.dns1035Label().validate('my-name')");
assert_valid("format.dns1035Label().validate('a')");
assert_valid("format.dns1035Label().validate('abc')");
}
#[test]
fn test_dns1035_label_invalid() {
assert_invalid("format.dns1035Label().validate('')");
assert_invalid("format.dns1035Label().validate('1start')"); assert_invalid("format.dns1035Label().validate('end-')");
assert_invalid("format.dns1035Label().validate('UPPER')");
}
#[test]
fn test_dns1123_subdomain_valid() {
assert_valid("format.dns1123Subdomain().validate('example.com')");
assert_valid("format.dns1123Subdomain().validate('my-app.example.com')");
assert_valid("format.dns1123Subdomain().validate('a')");
}
#[test]
fn test_dns1123_subdomain_invalid() {
assert_invalid("format.dns1123Subdomain().validate('')");
assert_invalid("format.dns1123Subdomain().validate('.leading.dot')");
assert_invalid("format.dns1123Subdomain().validate('trailing.dot.')");
let long = format!("{}.{}", "a".repeat(63), "b".repeat(63 * 3));
if long.len() > 253 {
assert_invalid(&format!("format.dns1123Subdomain().validate('{long}')"));
}
}
#[test]
fn test_dns1123_label_prefix_valid() {
assert_valid("format.dns1123LabelPrefix().validate('my-name')");
assert_valid("format.dns1123LabelPrefix().validate('my-name-')"); assert_valid("format.dns1123LabelPrefix().validate('a')");
}
#[test]
fn test_dns1123_label_prefix_invalid() {
assert_invalid("format.dns1123LabelPrefix().validate('')");
assert_invalid("format.dns1123LabelPrefix().validate('-start')");
assert_invalid("format.dns1123LabelPrefix().validate('UPPER')");
}
#[test]
fn test_dns1035_label_prefix_valid() {
assert_valid("format.dns1035LabelPrefix().validate('my-prefix-')");
assert_valid("format.dns1035LabelPrefix().validate('')"); assert_valid("format.dns1035LabelPrefix().validate('a')");
assert_valid("format.dns1035LabelPrefix().validate('ok-')");
}
#[test]
fn test_dns1035_label_prefix_invalid() {
assert_invalid("format.dns1035LabelPrefix().validate('1start-')"); assert_invalid("format.dns1035LabelPrefix().validate('UPPER')"); }
#[test]
fn test_dns1035_label_prefix_via_named() {
assert_valid("format.named('dns1035LabelPrefix').validate('ok-')");
}
#[test]
fn test_dns1123_subdomain_prefix_valid() {
assert_valid("format.dns1123SubdomainPrefix().validate('example.com')");
assert_valid("format.dns1123SubdomainPrefix().validate('my-app.example-')"); assert_valid("format.dns1123SubdomainPrefix().validate('a')");
}
#[test]
fn test_dns1123_subdomain_prefix_invalid() {
assert_invalid("format.dns1123SubdomainPrefix().validate('')");
assert_invalid("format.dns1123SubdomainPrefix().validate('bad-.example')");
}
#[test]
fn test_qualified_name_valid() {
assert_valid("format.qualifiedName().validate('my-name')");
assert_valid("format.qualifiedName().validate('example.com/my-name')");
assert_valid("format.qualifiedName().validate('my.name')");
assert_valid("format.qualifiedName().validate('my_name')");
assert_valid("format.qualifiedName().validate('A-Za-z0')");
}
#[test]
fn test_qualified_name_invalid() {
assert_invalid("format.qualifiedName().validate('')");
assert_invalid("format.qualifiedName().validate('/name')"); assert_invalid("format.qualifiedName().validate('prefix/')"); assert_invalid("format.qualifiedName().validate('.bad/name')"); assert_invalid("format.qualifiedName().validate('prefix/.bad')"); }
#[test]
fn test_label_value_valid() {
assert_valid("format.labelValue().validate('')"); assert_valid("format.labelValue().validate('a')");
assert_valid("format.labelValue().validate('my-value')");
assert_valid("format.labelValue().validate('my.value')");
assert_valid("format.labelValue().validate('my_value')");
assert_valid("format.labelValue().validate('MyValue')");
}
#[test]
fn test_label_value_invalid() {
assert_invalid("format.labelValue().validate('-start')");
assert_invalid("format.labelValue().validate('end-')");
assert_invalid("format.labelValue().validate('has space')");
let long = "a".repeat(64);
assert_invalid(&format!("format.labelValue().validate('{long}')"));
}
#[test]
fn test_uri_valid() {
assert_valid("format.uri().validate('https://example.com')");
assert_valid("format.uri().validate('http://example.com/path')");
assert_valid("format.uri().validate('ftp://files.example.com')");
assert_valid("format.uri().validate('urn:isbn:0451450523')");
assert_valid("format.uri().validate('mailto:user@example.com')");
}
#[test]
fn test_uri_invalid() {
assert_invalid("format.uri().validate('')");
assert_invalid("format.uri().validate('no-scheme')");
assert_invalid("format.uri().validate('://missing-scheme')");
}
#[test]
fn test_uuid_valid() {
assert_valid("format.uuid().validate('550e8400-e29b-41d4-a716-446655440000')");
assert_valid("format.uuid().validate('550E8400-E29B-41D4-A716-446655440000')"); }
#[test]
fn test_uuid_invalid() {
assert_invalid("format.uuid().validate('')");
assert_invalid("format.uuid().validate('not-a-uuid')");
assert_invalid("format.uuid().validate('550e8400-e29b-41d4-a716')"); assert_invalid("format.uuid().validate('550e8400-e29b-41d4-a716-44665544000g')"); }
#[test]
fn test_byte_valid() {
assert_valid("format.byte().validate('')"); assert_valid("format.byte().validate('aGVsbG8=')"); assert_valid("format.byte().validate('aGVsbG8')"); assert_valid("format.byte().validate('YQ==')"); assert_valid("format.byte().validate('YWI=')"); assert_valid("format.byte().validate('aGVsbG8-')"); assert_valid("format.byte().validate('aGVsbG8_')"); }
#[test]
fn test_byte_invalid() {
assert_invalid("format.byte().validate('not valid!')");
assert_invalid("format.byte().validate('abc===')"); }
#[test]
fn test_date_valid() {
assert_valid("format.date().validate('2024-01-15')");
assert_valid("format.date().validate('2024-02-29')"); assert_valid("format.date().validate('2024-12-31')");
}
#[test]
fn test_date_invalid() {
assert_invalid("format.date().validate('')");
assert_invalid("format.date().validate('2024-13-01')"); assert_invalid("format.date().validate('2023-02-29')"); assert_invalid("format.date().validate('2024-1-1')"); assert_invalid("format.date().validate('not-a-date')");
}
#[test]
fn test_datetime_valid() {
assert_valid("format.datetime().validate('2024-01-15T10:30:00Z')");
assert_valid("format.datetime().validate('2024-01-15T10:30:00+09:00')");
assert_valid("format.datetime().validate('2024-01-15T10:30:00-05:00')");
assert_valid("format.datetime().validate('2024-01-15T10:30:00.123Z')");
assert_valid("format.datetime().validate('2024-01-15t10:30:00z')"); }
#[test]
fn test_datetime_invalid() {
assert_invalid("format.datetime().validate('')");
assert_invalid("format.datetime().validate('2024-01-15')"); assert_invalid("format.datetime().validate('2024-01-15T10:30:00')"); assert_invalid("format.datetime().validate('not-a-datetime')");
assert_invalid("format.datetime().validate('2024-01-15T25:00:00Z')"); }
#[test]
fn test_format_named_known() {
assert_valid("format.named('dns1123Label').validate('my-name')");
assert_valid("format.named('uuid').validate('550e8400-e29b-41d4-a716-446655440000')");
}
#[test]
fn test_format_named_unknown() {
assert_eq!(eval("format.named('unknown')"), Value::Null);
}
#[test]
fn test_validate_returns_optional() {
assert_eq!(
eval("format.dns1123Label().validate('valid-name').hasValue()"),
Value::Bool(false)
);
assert_eq!(
eval("format.dns1123Label().validate('').hasValue()"),
Value::Bool(true)
);
}
#[test]
fn test_validate_has_value_pattern() {
assert_eq!(
eval("!format.dns1123Label().validate('valid').hasValue()"),
Value::Bool(true)
);
assert_eq!(
eval("!format.dns1123Label().validate('').hasValue()"),
Value::Bool(false)
);
}
#[test]
fn test_validate_error_list_accessible() {
assert_eq!(
eval("format.dns1123Label().validate('').value().size() > 0"),
Value::Bool(true)
);
}
}