use email_address::{EmailAddress, Options};
pub trait TypedValue: std::fmt::Display + std::str::FromStr {
const NOUN: &'static str;
const INPUT_TYPE: &'static str = "text";
fn parse_input(value: &str) -> Option<Self> {
value.parse().ok()
}
}
macro_rules! typed_whole_number {
($($ty:ty),* $(,)?) => {
$(
impl TypedValue for $ty {
const NOUN: &'static str = "whole number";
}
)*
};
}
typed_whole_number!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
);
impl TypedValue for bool {
const NOUN: &'static str = "yes/no value";
}
impl TypedValue for f32 {
const NOUN: &'static str = "number";
fn parse_input(value: &str) -> Option<Self> {
value
.parse::<f32>()
.ok()
.filter(|parsed| parsed.is_finite())
}
}
impl TypedValue for f64 {
const NOUN: &'static str = "number";
fn parse_input(value: &str) -> Option<Self> {
value
.parse::<f64>()
.ok()
.filter(|parsed| parsed.is_finite())
}
}
impl TypedValue for uuid::Uuid {
const NOUN: &'static str = "identifier";
}
impl TypedValue for crate::TenantId {
const NOUN: &'static str = "identifier";
}
impl TypedValue for jiff::Timestamp {
const NOUN: &'static str = "timestamp";
const INPUT_TYPE: &'static str = "datetime-local";
fn parse_input(value: &str) -> Option<Self> {
if let Ok(parsed) = value.parse::<jiff::Timestamp>() {
return Some(parsed);
}
normalize_datetime_local(value)?.parse().ok()
}
}
fn normalize_datetime_local(value: &str) -> Option<String> {
let t = value.find('T')?;
let after_t = &value[t + 1..];
if after_t.contains('+') || after_t.contains('-') {
return None;
}
if value.ends_with(['Z', 'z']) {
return None;
}
if value.len() == 16 {
Some(format!("{value}:00Z"))
} else {
Some(format!("{value}Z"))
}
}
pub(crate) fn format_timestamp_input(storage: &str) -> String {
let trimmed = storage.trim();
if trimmed.is_empty() {
return String::new();
}
match trimmed.parse::<jiff::Timestamp>() {
Ok(parsed) => parsed.strftime("%Y-%m-%dT%H:%M").to_string(),
Err(_) => String::new(),
}
}
const EMAIL_MAX_LENGTH: usize = 254;
pub(crate) fn is_email(value: &str) -> bool {
value.len() <= EMAIL_MAX_LENGTH
&& EmailAddress::parse_with_options(
value,
Options::default()
.with_required_tld()
.without_display_text(),
)
.is_ok()
}
#[cfg(test)]
mod tests;