systemprompt_identifiers/
email.rs1use crate::error::IdValidationError;
4use crate::{DbValue, ToDbValue};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema)]
10#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
11#[cfg_attr(feature = "sqlx", sqlx(transparent))]
12#[serde(transparent)]
13pub struct Email(String);
14
15impl Email {
16 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
17 let value = value.into();
18 if value.is_empty() {
19 return Err(IdValidationError::empty("Email"));
20 }
21 let parts: Vec<&str> = value.split('@').collect();
22 if parts.len() != 2 {
23 return Err(IdValidationError::invalid(
24 "Email",
25 "must contain exactly one '@' symbol",
26 ));
27 }
28 let local = parts[0];
29 let domain = parts[1];
30 if local.is_empty() {
31 return Err(IdValidationError::invalid(
32 "Email",
33 "local part (before @) cannot be empty",
34 ));
35 }
36 if local.starts_with('.') || local.ends_with('.') {
37 return Err(IdValidationError::invalid(
38 "Email",
39 "local part cannot start or end with '.'",
40 ));
41 }
42 if local.contains("..") {
43 return Err(IdValidationError::invalid(
44 "Email",
45 "local part cannot contain consecutive dots",
46 ));
47 }
48 if local.contains('\n') || local.contains('\r') {
49 return Err(IdValidationError::invalid(
50 "Email",
51 "email cannot contain newline characters",
52 ));
53 }
54 if domain.is_empty() {
55 return Err(IdValidationError::invalid(
56 "Email",
57 "domain part (after @) cannot be empty",
58 ));
59 }
60 if !domain.contains('.') {
61 return Err(IdValidationError::invalid(
62 "Email",
63 "domain must contain at least one '.'",
64 ));
65 }
66 if domain.starts_with('.') || domain.ends_with('.') {
67 return Err(IdValidationError::invalid(
68 "Email",
69 "domain cannot start or end with '.'",
70 ));
71 }
72 if domain.contains("..") {
73 return Err(IdValidationError::invalid(
74 "Email",
75 "domain cannot contain consecutive dots",
76 ));
77 }
78 if let Some(tld) = domain.rsplit('.').next() {
79 if tld.len() < 2 {
80 return Err(IdValidationError::invalid(
81 "Email",
82 "TLD must be at least 2 characters",
83 ));
84 }
85 }
86 Ok(Self(value))
87 }
88
89 #[must_use]
90 #[allow(clippy::expect_used)]
91 pub fn new(value: impl Into<String>) -> Self {
92 Self::try_new(value).expect("Email validation failed")
97 }
98
99 #[must_use]
100 pub fn as_str(&self) -> &str {
101 &self.0
102 }
103
104 #[must_use]
105 pub fn local_part(&self) -> &str {
106 self.0.split('@').next().unwrap_or("")
107 }
108
109 #[must_use]
110 pub fn domain(&self) -> &str {
111 self.0.split('@').nth(1).unwrap_or("")
112 }
113}
114
115impl fmt::Display for Email {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 write!(f, "{}", self.0)
118 }
119}
120
121impl TryFrom<String> for Email {
122 type Error = IdValidationError;
123
124 fn try_from(s: String) -> Result<Self, Self::Error> {
125 Self::try_new(s)
126 }
127}
128
129impl TryFrom<&str> for Email {
130 type Error = IdValidationError;
131
132 fn try_from(s: &str) -> Result<Self, Self::Error> {
133 Self::try_new(s)
134 }
135}
136
137impl std::str::FromStr for Email {
138 type Err = IdValidationError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 Self::try_new(s)
142 }
143}
144
145impl AsRef<str> for Email {
146 fn as_ref(&self) -> &str {
147 &self.0
148 }
149}
150
151impl<'de> Deserialize<'de> for Email {
152 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153 where
154 D: serde::Deserializer<'de>,
155 {
156 let s = String::deserialize(deserializer)?;
157 Self::try_new(s).map_err(serde::de::Error::custom)
158 }
159}
160
161impl ToDbValue for Email {
162 fn to_db_value(&self) -> DbValue {
163 DbValue::String(self.0.clone())
164 }
165}
166
167impl ToDbValue for &Email {
168 fn to_db_value(&self) -> DbValue {
169 DbValue::String(self.0.clone())
170 }
171}