Skip to main content

planter_core/
person.rs

1use anyhow::Context;
2pub use email_address::EmailAddress;
3use nutype::nutype;
4pub use phonenumber::PhoneNumber;
5
6const NAME_LEN: usize = 50;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9/// Represents a person with a name and contact information.
10pub struct Person {
11    /// The first name of the person.
12    first_name: NameString,
13    /// The last name of the person.
14    last_name: NameString,
15    /// The email address of the person.
16    email: Option<EmailAddress>,
17    /// The phone number of the person.
18    phone: Option<PhoneNumber>,
19}
20
21#[nutype(
22    sanitize(trim),
23    validate(not_empty, len_char_max = NAME_LEN),
24    derive(Debug, Eq, PartialEq, Clone, Display, Deref)
25)]
26pub struct NameString(String);
27
28impl Person {
29    /// Create a new `Person` with the given name and surname.
30    ///
31    /// # Arguments
32    /// * `name` - The first name of the person.
33    /// * `surname` - The surname of the person.
34    ///
35    /// # Returns
36    /// A new `Person` instance.
37    ///
38    /// # Errors
39    /// Returns an error if the name or surname is empty or exceeds the maximum length.
40    ///
41    /// # Examples
42    /// ```
43    /// use planter_core::person::Person;
44    ///
45    /// let person = Person::new("Margherita", "Hack").unwrap();
46    /// ```
47    pub fn new(name: impl Into<String>, surname: impl Into<String>) -> anyhow::Result<Self> {
48        let name = NameString::try_new(name).context("Invalid first name")?;
49        let surname = NameString::try_new(surname).context("Invalid last name")?;
50
51        Ok(Person {
52            first_name: name,
53            last_name: surname,
54            email: None,
55            phone: None,
56        })
57    }
58
59    /// Add or edit the email address of the person.
60    ///
61    /// # Arguments
62    /// * `email` - The new email address of the person.
63    ///
64    /// # Examples
65    /// ```
66    /// use planter_core::person::Person;
67    /// use email_address::EmailAddress;
68    /// use std::str::FromStr;
69    ///
70    /// let mut person = Person::new("Margherita", "Hack").unwrap();
71    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
72    /// person.update_email(email.clone());
73    /// assert_eq!(person.email(), Some(&email));
74    /// ```
75    pub fn update_email(&mut self, email: EmailAddress) {
76        self.email = Some(email);
77    }
78
79    /// Remove the email address of the person.
80    ///
81    /// # Examples
82    /// ```
83    /// use planter_core::person::Person;
84    /// use email_address::EmailAddress;
85    /// use std::str::FromStr;
86    ///
87    /// let mut person = Person::new("Margherita", "Hack").unwrap();
88    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
89    /// person.update_email(email.clone());
90    /// assert_eq!(person.email(), Some(&email));
91    /// person.rm_email();
92    /// assert!(person.email().is_none());
93    /// ```
94    pub fn rm_email(&mut self) {
95        self.email = None;
96    }
97
98    /// Add or edit the phone number of the person.
99    ///
100    /// # Arguments
101    /// * `phone` - The new phone number of the person.
102    ///
103    /// # Examples
104    /// ```
105    /// use planter_core::person::Person;
106    /// use std::str::FromStr;
107    /// use phonenumber::PhoneNumber;
108    ///
109    /// let mut person = Person::new("Margherita", "Hack").unwrap();
110    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
111    /// person.update_phone(phone.clone());
112    /// assert_eq!(person.phone(), Some(&phone));
113    /// ```
114    pub fn update_phone(&mut self, phone: PhoneNumber) {
115        self.phone = Some(phone);
116    }
117
118    /// Remove the phone number of the person.
119    ///
120    /// # Examples
121    /// ```
122    /// use planter_core::person::Person;
123    /// use std::str::FromStr;
124    /// use phonenumber::PhoneNumber;
125    ///
126    /// let mut person = Person::new("Margherita", "Hack").unwrap();
127    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
128    /// person.update_phone(phone.clone());
129    /// assert_eq!(person.phone(), Some(&phone));
130    /// person.rm_phone();
131    /// assert!(person.phone().is_none());
132    /// ```
133    pub fn rm_phone(&mut self) {
134        self.phone = None;
135    }
136
137    /// Get the phone number of the person.
138    ///
139    /// # Examples
140    /// ```
141    /// use planter_core::person::Person;
142    /// use phonenumber::PhoneNumber;
143    /// use std::str::FromStr;
144    ///
145    /// let mut person = Person::new("Margherita", "Hack").unwrap();
146    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
147    /// person.update_phone(phone.clone());
148    /// assert_eq!(person.phone(), Some(&phone));
149    /// ```
150    #[must_use]
151    pub const fn phone(&self) -> Option<&PhoneNumber> {
152        self.phone.as_ref()
153    }
154
155    /// Get the email of the person.
156    ///
157    /// # Examples
158    /// ```
159    /// use planter_core::person::Person;
160    /// use email_address::EmailAddress;
161    /// use std::str::FromStr;
162    ///
163    /// let mut person = Person::new("Margherita", "Hack").unwrap();
164    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
165    /// person.update_email(email.clone());
166    /// assert_eq!(person.email(), Some(&email));
167    /// ```
168    #[must_use]
169    pub const fn email(&self) -> Option<&EmailAddress> {
170        self.email.as_ref()
171    }
172
173    /// Get the name of the person.
174    ///
175    /// # Examples
176    /// ```
177    /// use planter_core::person::Person;
178    ///
179    /// let mut person = Person::new("Margherita", "Hack").unwrap();
180    /// assert_eq!(person.full_name(), "Margherita Hack");
181    /// ```
182    #[must_use]
183    pub fn full_name(&self) -> String {
184        format!("{} {}", self.first_name, self.last_name)
185    }
186
187    /// Get the first name of the person.
188    ///
189    /// # Examples
190    /// ```
191    /// use planter_core::person::Person;
192    ///
193    /// let person = Person::new("Margherita", "Hack").unwrap();
194    /// assert_eq!(person.first_name(), "Margherita");
195    /// ```
196    #[must_use]
197    pub fn first_name(&self) -> &str {
198        &self.first_name
199    }
200
201    /// Update the first name of the person.
202    ///
203    /// # Errors
204    ///
205    /// It can return an error, if the input `name` can't be converted to
206    /// `NameString`
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use planter_core::person::Person;
212    ///
213    /// let mut person = Person::new("Margaret", "Hack").unwrap();
214    /// person.update_first_name("Margherita").unwrap();
215    /// assert_eq!(person.first_name(), "Margherita");
216    /// ```
217    pub fn update_first_name(&mut self, name: impl Into<String>) -> anyhow::Result<()> {
218        self.first_name =
219            NameString::try_new(name).context("Input can't be converted into NameString.")?;
220        Ok(())
221    }
222
223    /// Get the last name of the person.
224    ///
225    /// # Examples
226    /// ```
227    /// use planter_core::person::Person;
228    ///
229    /// let mut person = Person::new("Margherita", "Hack").unwrap();
230    /// assert_eq!(person.last_name(), "Hack");
231    /// ```
232    #[must_use]
233    pub fn last_name(&self) -> &str {
234        &self.last_name
235    }
236
237    /// Update the last name of the person.
238    ///
239    /// # Errors
240    ///
241    /// It can return an error, if the input `name` can't be converted to
242    /// `NameString`
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use planter_core::person::Person;
248    ///
249    /// let mut person = Person::new("Margherita", "Hacker").unwrap();
250    /// person.update_last_name("Hack").unwrap();
251    /// assert_eq!(person.last_name(), "Hack");
252    /// ```
253    pub fn update_last_name(&mut self, name: impl Into<String>) -> anyhow::Result<()> {
254        self.last_name =
255            NameString::try_new(name).context("Input can't be converted into NameString.")?;
256        Ok(())
257    }
258}
259
260#[cfg(test)]
261/// Test utilities for the `person` module.
262pub mod test_utils {
263    use std::str::FromStr;
264
265    use email_address::EmailAddress;
266    use phonenumber::PhoneNumber;
267    use proptest::prelude::Strategy;
268
269    /// Generate a random email address.
270    pub fn email() -> impl Strategy<Value = EmailAddress> {
271        r"[a-z]{1,10}@[a-z]{1,10}\.[a-z]{2,4}"
272            .prop_map(|s: String| EmailAddress::from_str(&s).unwrap())
273    }
274
275    /// Generate a random phone number.
276    pub fn phone_number() -> impl Strategy<Value = PhoneNumber> {
277        r"\+39[0-9]{6,12}".prop_map(|s: String| PhoneNumber::from_str(&s).unwrap())
278    }
279
280    /// Generate a random valid name string (1-50 alpha chars).
281    pub fn valid_name() -> impl Strategy<Value = String> {
282        "[a-zA-Z]{1,50}"
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use proptest::prelude::*;
289
290    use super::test_utils::{email, phone_number, valid_name};
291    use crate::person::Person;
292
293    proptest! {
294        #[test]
295        fn full_name_equals_first_last(first in valid_name(), last in valid_name()) {
296            let person = Person::new(&first, &last).unwrap();
297            assert_eq!(person.full_name(), format!("{} {}", first, last));
298        }
299
300        #[test]
301        fn update_first_name_roundtrip(first in valid_name(), last in valid_name(), new_first in valid_name()) {
302            let mut person = Person::new(&first, &last).unwrap();
303            person.update_first_name(&new_first).unwrap();
304            assert_eq!(person.first_name(), new_first);
305        }
306
307        #[test]
308        fn update_last_name_roundtrip(first in valid_name(), last in valid_name(), new_last in valid_name()) {
309            let mut person = Person::new(&first, &last).unwrap();
310            person.update_last_name(&new_last).unwrap();
311            assert_eq!(person.last_name(), new_last);
312        }
313
314        #[test]
315        fn update_email_roundtrip(first in valid_name(), last in valid_name(), email in email()) {
316            let mut person = Person::new(&first, &last).unwrap();
317            person.update_email(email.clone());
318            assert_eq!(person.email(), Some(&email));
319            person.rm_email();
320            assert!(person.email().is_none());
321        }
322
323        #[test]
324        fn update_phone_roundtrip(first in valid_name(), last in valid_name(), phone in phone_number()) {
325            let mut person = Person::new(&first, &last).unwrap();
326            person.update_phone(phone.clone());
327            assert_eq!(person.phone(), Some(&phone));
328            person.rm_phone();
329            assert!(person.phone().is_none());
330        }
331
332        #[test]
333        fn new_rejects_empty_name(name in valid_name()) {
334            assert!(Person::new("", &name).is_err());
335            assert!(Person::new(&name, "").is_err());
336        }
337
338        #[test]
339        fn new_rejects_long_name(first in "[a-zA-Z]{51,100}", last in valid_name()) {
340            assert!(Person::new(&first, &last).is_err());
341        }
342
343        #[test]
344        fn update_first_name_rejects_invalid(name in valid_name(), bad in "[a-zA-Z]{51,100}") {
345            let mut person = Person::new("valid", &name).unwrap();
346            assert!(person.update_first_name(&bad).is_err());
347        }
348
349        #[test]
350        fn update_last_name_rejects_invalid(name in valid_name(), bad in "[a-zA-Z]{51,100}") {
351            let mut person = Person::new(&name, "valid").unwrap();
352            assert!(person.update_last_name(&bad).is_err());
353        }
354    }
355}