Skip to main content

git_su/
user.rs

1// User: parse "Name <email>", combine for pairing, initials
2
3use 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    /// Parse "Name <email@example.com>" format.
22    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}