1use chrono::{DateTime, Utc};
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::sync::OnceLock;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(untagged)]
12pub enum FieldValue {
13 String(String),
14 Integer(i64),
15 Float(f64),
16 Boolean(bool),
17 Uuid(Uuid),
18 DateTime(DateTime<Utc>),
19 Null,
20}
21
22impl FieldValue {
23 pub fn as_string(&self) -> Option<&str> {
25 match self {
26 FieldValue::String(s) => Some(s),
27 _ => None,
28 }
29 }
30
31 pub fn as_integer(&self) -> Option<i64> {
33 match self {
34 FieldValue::Integer(i) => Some(*i),
35 _ => None,
36 }
37 }
38
39 pub fn as_uuid(&self) -> Option<Uuid> {
41 match self {
42 FieldValue::Uuid(u) => Some(*u),
43 _ => None,
44 }
45 }
46
47 pub fn is_null(&self) -> bool {
49 matches!(self, FieldValue::Null)
50 }
51}
52
53#[derive(Debug, Clone)]
55pub enum FieldFormat {
56 Email,
57 Uuid,
58 Url,
59 Phone,
60 Custom(Regex),
61}
62
63impl FieldFormat {
64 pub fn validate(&self, value: &FieldValue) -> bool {
66 let string_value = match value.as_string() {
67 Some(s) => s,
68 None => return false,
69 };
70
71 match self {
72 FieldFormat::Email => Self::is_valid_email(string_value),
73 FieldFormat::Uuid => Uuid::parse_str(string_value).is_ok(),
74 FieldFormat::Url => Self::is_valid_url(string_value),
75 FieldFormat::Phone => Self::is_valid_phone(string_value),
76 FieldFormat::Custom(regex) => regex.is_match(string_value),
77 }
78 }
79
80 fn is_valid_email(email: &str) -> bool {
81 static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
82 let regex = EMAIL_REGEX.get_or_init(|| {
83 Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
84 });
85 regex.is_match(email)
86 }
87
88 fn is_valid_url(url: &str) -> bool {
89 static URL_REGEX: OnceLock<Regex> = OnceLock::new();
90 let regex = URL_REGEX.get_or_init(|| Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap());
91 regex.is_match(url)
92 }
93
94 fn is_valid_phone(phone: &str) -> bool {
95 static PHONE_REGEX: OnceLock<Regex> = OnceLock::new();
96 let regex = PHONE_REGEX.get_or_init(|| {
97 Regex::new(r"^\+?[1-9]\d{7,14}$").unwrap()
99 });
100 regex.is_match(phone)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_field_value_string() {
110 let value = FieldValue::String("test".to_string());
111 assert_eq!(value.as_string(), Some("test"));
112 assert_eq!(value.as_integer(), None);
113 assert!(!value.is_null());
114 }
115
116 #[test]
117 fn test_field_value_integer() {
118 let value = FieldValue::Integer(42);
119 assert_eq!(value.as_integer(), Some(42));
120 assert_eq!(value.as_string(), None);
121 }
122
123 #[test]
124 fn test_field_value_null() {
125 let value = FieldValue::Null;
126 assert!(value.is_null());
127 assert_eq!(value.as_string(), None);
128 }
129
130 #[test]
131 fn test_email_validation() {
132 let format = FieldFormat::Email;
133
134 assert!(format.validate(&FieldValue::String("test@example.com".to_string())));
135 assert!(format.validate(&FieldValue::String(
136 "user.name+tag@example.co.uk".to_string()
137 )));
138 assert!(!format.validate(&FieldValue::String("invalid-email".to_string())));
139 assert!(!format.validate(&FieldValue::String("@example.com".to_string())));
140 }
141
142 #[test]
143 fn test_uuid_validation() {
144 let format = FieldFormat::Uuid;
145 let valid_uuid = Uuid::new_v4().to_string();
146
147 assert!(format.validate(&FieldValue::String(valid_uuid)));
148 assert!(!format.validate(&FieldValue::String("not-a-uuid".to_string())));
149 }
150
151 #[test]
152 fn test_url_validation() {
153 let format = FieldFormat::Url;
154
155 assert!(format.validate(&FieldValue::String("https://example.com".to_string())));
156 assert!(format.validate(&FieldValue::String(
157 "http://test.com/path?query=1".to_string()
158 )));
159 assert!(!format.validate(&FieldValue::String("not a url".to_string())));
160 }
161
162 #[test]
163 fn test_phone_validation() {
164 let format = FieldFormat::Phone;
165
166 assert!(format.validate(&FieldValue::String("+33612345678".to_string())));
167 assert!(format.validate(&FieldValue::String("33612345678".to_string())));
168 assert!(!format.validate(&FieldValue::String("123".to_string())));
169 }
170
171 #[test]
172 fn test_custom_regex_validation() {
173 let format = FieldFormat::Custom(Regex::new(r"^[A-Z]{3}\d{3}$").unwrap());
174
175 assert!(format.validate(&FieldValue::String("ABC123".to_string())));
176 assert!(!format.validate(&FieldValue::String("abc123".to_string())));
177 assert!(!format.validate(&FieldValue::String("ABCD123".to_string())));
178 }
179}