tablo_core/schema/
validation.rs1use email_address::{EmailAddress, Options};
4
5pub trait TypedValue: std::fmt::Display + std::str::FromStr {
7 const NOUN: &'static str;
9
10 const INPUT_TYPE: &'static str = "text";
12
13 fn parse_input(value: &str) -> Option<Self> {
15 value.parse().ok()
16 }
17}
18
19macro_rules! typed_whole_number {
21 ($($ty:ty),* $(,)?) => {
22 $(
23 impl TypedValue for $ty {
24 const NOUN: &'static str = "whole number";
25 }
26 )*
27 };
28}
29
30typed_whole_number!(
31 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
32);
33
34impl TypedValue for bool {
35 const NOUN: &'static str = "yes/no value";
36}
37
38impl TypedValue for f32 {
40 const NOUN: &'static str = "number";
41
42 fn parse_input(value: &str) -> Option<Self> {
43 value
44 .parse::<f32>()
45 .ok()
46 .filter(|parsed| parsed.is_finite())
47 }
48}
49
50impl TypedValue for f64 {
51 const NOUN: &'static str = "number";
52
53 fn parse_input(value: &str) -> Option<Self> {
54 value
55 .parse::<f64>()
56 .ok()
57 .filter(|parsed| parsed.is_finite())
58 }
59}
60
61impl TypedValue for uuid::Uuid {
62 const NOUN: &'static str = "identifier";
63}
64
65impl TypedValue for crate::TenantId {
66 const NOUN: &'static str = "identifier";
67}
68
69impl TypedValue for jiff::Timestamp {
71 const NOUN: &'static str = "timestamp";
72 const INPUT_TYPE: &'static str = "datetime-local";
73
74 fn parse_input(value: &str) -> Option<Self> {
76 if let Ok(parsed) = value.parse::<jiff::Timestamp>() {
77 return Some(parsed);
78 }
79 normalize_datetime_local(value)?.parse().ok()
80 }
81}
82
83fn normalize_datetime_local(value: &str) -> Option<String> {
86 let t = value.find('T')?;
87 let after_t = &value[t + 1..];
88 if after_t.contains('+') || after_t.contains('-') {
89 return None;
90 }
91 if value.ends_with(['Z', 'z']) {
92 return None;
93 }
94 if value.len() == 16 {
95 Some(format!("{value}:00Z"))
96 } else {
97 Some(format!("{value}Z"))
98 }
99}
100
101pub(crate) fn format_timestamp_input(storage: &str) -> String {
104 let trimmed = storage.trim();
105 if trimmed.is_empty() {
106 return String::new();
107 }
108 match trimmed.parse::<jiff::Timestamp>() {
109 Ok(parsed) => parsed.strftime("%Y-%m-%dT%H:%M").to_string(),
110 Err(_) => String::new(),
111 }
112}
113
114const EMAIL_MAX_LENGTH: usize = 254;
116
117pub(crate) fn is_email(value: &str) -> bool {
120 value.len() <= EMAIL_MAX_LENGTH
121 && EmailAddress::parse_with_options(
122 value,
123 Options::default()
124 .with_required_tld()
125 .without_display_text(),
126 )
127 .is_ok()
128}
129
130#[cfg(test)]
131mod tests;