stepflow_data/value/
email_value.rs1use std::borrow::{Borrow, Cow};
2use std::str::FromStr;
3use super::{Value, BaseValue, InvalidValue};
4
5
6#[derive(Debug, PartialEq, Clone)]
10pub struct EmailValue {
11 val: Cow<'static, str>,
12}
13
14impl EmailValue {
15 pub fn try_new<STR>(val: STR) -> Result<Self, InvalidValue>
16 where STR: Into<Cow<'static, str>>
17 {
18 let val = val.into();
19 Self::validate(&val)?;
20 Ok(Self { val })
21 }
22
23 pub fn validate(val: &Cow<'static, str>) -> Result<(), InvalidValue> {
24 if val.is_empty() {
25 return Err(InvalidValue::Empty);
26 }
27
28 if extract_login(val).is_none() {
29 return Err(InvalidValue::BadFormat)
30 }
31
32 Ok(())
33 }
34
35 pub fn val(&self) -> &str {
36 self.val.borrow()
37 }
38
39 pub fn boxed(self) -> Box<dyn Value> {
40 Box::new(self)
41 }
42}
43
44fn is_valid_email_local_part_char(c: char) -> bool {
45 if c.is_alphanumeric() {
46 return true;
47 }
48 match c {
49 '!' | '#' | '$' | '%' | '&' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' | '`' | '{' | '|' | '}' | '~' => true,
50 _ => false
51 }
52}
53fn extract_login(input: &str) -> Option<&str> {
54 #[derive(PartialEq, Debug)]
55 enum ExtractState {
56 LoginAnyLocalPartChar, LoginAnyLocalPartCharAndDot,
58 Domain
59 }
60
61 let mut end_range = 0;
62 let mut state = ExtractState::LoginAnyLocalPartChar; let mut login: &str = "";
64 for c in input.chars() {
65 if c.is_whitespace() {
67 return None;
68 }
69 end_range += 1;
70
71 state = match state {
72 ExtractState::LoginAnyLocalPartChar |
73 ExtractState::LoginAnyLocalPartCharAndDot => {
74 if is_valid_email_local_part_char(c) {
75 ExtractState::LoginAnyLocalPartCharAndDot
76 } else if state == ExtractState::LoginAnyLocalPartCharAndDot && c == '.' {
77 ExtractState::LoginAnyLocalPartChar
78 } else if c == '@' {
79 login = input.get(0..end_range-1)?;
80 if login.chars().last()? == '.' {
81 return None;
83 }
84 ExtractState::Domain
85 } else {
86 return None;
87 }
88 }
89 ExtractState::Domain => {
90 match c {
91 '@' => return None,
92 _ => ExtractState::Domain,
93 }
94 }
95 }
96 }
97
98 if login.is_empty() {
99 None
101 } else {
102 Some(login)
103 }
104}
105
106define_value_impl!(EmailValue);
107
108impl FromStr for EmailValue {
109 type Err = InvalidValue;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 EmailValue::try_new(s.to_owned())
113 }
114}
115
116
117#[cfg(test)]
118mod tests {
119 use super::super::InvalidValue;
120 use super::{ extract_login, EmailValue };
121
122 #[test]
123 fn test_extract_valid_email() {
124 let emails = vec![
127 ("email@example.com", "email"),
129 ("firstname.lastname@example.com", "firstname.lastname"),
130 ("email@subdomain.example.com", "email"),
131 ("firstname+lastname@example.com", "firstname+lastname"),
132 ("email@123.123.123.123", "email"),
133 ("email@[123.123.123.123]", "email"),
134 ("1234567890@example.com", "1234567890"),
136 ("email@example-one.com", "email"),
137 ("_______@example.com", "_______"),
138 ("email@example.name", "email"),
139 ("email@example.museum", "email"),
140 ("email@example.co.jp", "email"),
141 ("firstname-lastname@example.com", "firstname-lastname"),
142 ];
147 for (email, login) in emails {
148 println!("Checking GOOD {}", email);
149 let extracted_login = extract_login(email).unwrap();
150 assert_eq!(extracted_login, login);
151 }
152 }
153
154 #[test]
155 fn test_extract_invalid_email() {
156 let bad_emails = vec![
158 "plainaddress",
159 "#@%^%#$@#$@#.com",
160 "@example.com",
161 "Joe Smith <email@example.com>",
162 "email.example.com",
163 "email@example@example.com",
164 ".email@example.com",
165 "email.@example.com",
166 "email..email@example.com",
167 "あいうえお@example.com",
168 "email@example.com (Joe Smith)",
169 "Abc..123@example.com",
175
176 "”(),:;<>[\\]@example.com",
178 "just”not”right@example.com",
179 "this\\ is\"really\"not\\allowed@example.com",
180 ];
181 for bad_email in bad_emails {
182 println!("Checking BAD {}", bad_email);
183 assert_eq!(extract_login(bad_email), None);
184 }
185 }
186
187 #[test]
188 fn test_good_email() {
189 let email = EmailValue::try_new("a@b.com").unwrap();
190 assert_eq!(email.val(), "a@b.com");
191 }
192
193 #[test]
194 fn test_bad_email() {
195 let email_result = EmailValue::try_new("");
196 assert_eq!(email_result, Err(InvalidValue::Empty));
197
198 let email_result = EmailValue::try_new("ab.com");
199 assert_eq!(email_result, Err(InvalidValue::BadFormat));
200 }
201
202 #[test]
203 fn test_fromstr() {
204 assert!(matches!("".parse::<EmailValue>(), Err(_)));
205 assert!(matches!("notemail".parse::<EmailValue>(), Err(_)));
206 assert_eq!("valid@email.com".parse::<EmailValue>().unwrap(), EmailValue::try_new("valid@email.com").unwrap());
207 }
208}