1use std::fmt;
4
5#[derive(Clone, Debug)]
6pub struct User {
7 pub names: Vec<String>,
8 pub emails: Vec<String>,
9 group_email: Option<String>,
10}
11
12impl User {
13 pub fn new(name: impl Into<String>, email: impl Into<String>) -> Self {
14 User {
15 names: vec![name.into()],
16 emails: vec![email.into()],
17 group_email: None,
18 }
19 }
20
21 pub fn parse(s: &str) -> Result<Self, ParseError> {
23 let s = s.trim();
24 if let Some(open) = s.find('<') {
25 if let Some(close) = s.find('>') {
26 if close > open && close == s.len() - 1 {
27 let name = s[..open].trim().to_string();
28 let email = s[open + 1..close].trim().to_string();
29 if !name.is_empty() && !email.is_empty() {
30 return Ok(User::new(name, email));
31 }
32 }
33 }
34 }
35 Err(ParseError {
36 input: s.to_string(),
37 })
38 }
39
40 pub fn none() -> User {
41 User {
42 names: vec![],
43 emails: vec![],
44 group_email: None,
45 }
46 }
47
48 pub fn name(&self) -> String {
49 if self.is_none() {
50 return "(none)".to_string();
51 }
52 self.names.join(" and ")
53 }
54
55 pub fn email(&self) -> String {
56 if self.emails.is_empty() {
57 return String::new();
58 }
59 if self.emails.len() == 1 {
60 return self.emails[0].clone();
61 }
62 let group = self.group_email.as_deref().unwrap_or("dev@example.com");
63 let group_prefix = group.split('@').next().unwrap_or("dev");
64 let group_domain = group.split('@').nth(1).unwrap_or("example.com");
65 let prefixes: Vec<&str> = self
66 .emails
67 .iter()
68 .map(|e| e.split('@').next().unwrap_or(""))
69 .collect();
70 format!("{}+{}@{}", group_prefix, prefixes.join("+"), group_domain)
71 }
72
73 pub fn initials(&self) -> String {
74 self.names
75 .join(" ")
76 .split_whitespace()
77 .filter_map(|w| w.chars().next())
78 .collect::<String>()
79 .to_lowercase()
80 }
81
82 pub fn is_none(&self) -> bool {
83 self.names.is_empty() && self.emails.is_empty()
84 }
85
86 pub fn combine(mut self, other: &User, group_email: &str) -> User {
87 if self.is_none() {
88 return other.clone();
89 }
90 if other.is_none() {
91 return self;
92 }
93 self.names.extend(other.names.clone());
94 self.emails.extend(other.emails.clone());
95 self.group_email = Some(group_email.to_string());
96 self
97 }
98}
99
100impl PartialEq for User {
101 fn eq(&self, other: &Self) -> bool {
102 self.name() == other.name() && self.email() == other.email()
103 }
104}
105impl Eq for User {}
106
107impl fmt::Display for User {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "{} <{}>", self.name(), self.email())
110 }
111}
112
113#[derive(Debug)]
114pub struct ParseError {
115 pub input: String,
116}
117
118impl fmt::Display for ParseError {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(
121 f,
122 "Couldn't parse '{}' as user (expected user in format: 'Jane Doe <jane@example.com>')",
123 self.input
124 )
125 }
126}
127
128impl std::error::Error for ParseError {}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn parse_valid_user() {
136 let u = User::parse("Jane Doe <jane@example.com>").unwrap();
137 assert_eq!(u.name(), "Jane Doe");
138 assert_eq!(u.email(), "jane@example.com");
139 assert_eq!(u.initials(), "jd");
140 }
141
142 #[test]
143 fn parse_with_extra_spaces() {
144 let u = User::parse(" Jane Doe < jane@example.com > ").unwrap();
145 assert_eq!(u.name(), "Jane Doe");
146 assert_eq!(u.email(), "jane@example.com");
147 }
148
149 #[test]
150 fn parse_invalid_no_angle() {
151 assert!(User::parse("Jane Doe jane@example.com").is_err());
152 }
153
154 #[test]
155 fn parse_invalid_empty_name() {
156 assert!(User::parse("<jane@example.com>").is_err());
157 }
158
159 #[test]
160 fn initials_multiple_words() {
161 let u = User::new("John Paul Smith", "j@x.com");
162 assert_eq!(u.initials(), "jps");
163 }
164
165 #[test]
166 fn none_user() {
167 let n = User::none();
168 assert!(n.is_none());
169 assert_eq!(n.name(), "(none)");
170 assert_eq!(n.email(), "");
171 }
172
173 #[test]
174 fn combine_two_users() {
175 let a = User::new("Alice", "a@x.com");
176 let b = User::new("Bob", "b@x.com");
177 let c = a.combine(&b, "dev@example.com");
178 assert_eq!(c.name(), "Alice and Bob");
179 assert_eq!(c.email(), "dev+a+b@example.com");
180 }
181
182 #[test]
183 fn combine_with_none() {
184 let a = User::new("Alice", "a@x.com");
185 let n = User::none();
186 let c = n.combine(&a, "dev@x.com");
187 assert_eq!(c.name(), "Alice");
188 assert_eq!(c.email(), "a@x.com");
189 }
190
191 #[test]
192 fn user_equality() {
193 let a = User::parse("Jane Doe <jane@example.com>").unwrap();
194 let b = User::new("Jane Doe", "jane@example.com");
195 assert_eq!(a, b);
196 }
197}