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    cfg_attr(feature = "serde", derive(Serialize, Deserialize)),
26)]
27pub struct NameString(String);
28
29impl Person {
30    /// Create a new `Person` with the given name and surname.
31    ///
32    /// # Arguments
33    /// * `name` - The first name of the person.
34    /// * `surname` - The surname of the person.
35    ///
36    /// # Returns
37    /// A new `Person` instance.
38    ///
39    /// # Errors
40    /// Returns an error if the name or surname is empty or exceeds the maximum length.
41    ///
42    /// # Examples
43    /// ```
44    /// use planter_core::person::Person;
45    ///
46    /// let person = Person::new("Margherita", "Hack").unwrap();
47    /// ```
48    pub fn new(name: impl Into<String>, surname: impl Into<String>) -> anyhow::Result<Self> {
49        let name = NameString::try_new(name).context("Invalid first name")?;
50        let surname = NameString::try_new(surname).context("Invalid last name")?;
51
52        Ok(Person {
53            first_name: name,
54            last_name: surname,
55            email: None,
56            phone: None,
57        })
58    }
59
60    /// Add or edit the email address of the person.
61    ///
62    /// # Arguments
63    /// * `email` - The new email address of the person.
64    ///
65    /// # Examples
66    /// ```
67    /// use planter_core::person::Person;
68    /// use email_address::EmailAddress;
69    /// use std::str::FromStr;
70    ///
71    /// let mut person = Person::new("Margherita", "Hack").unwrap();
72    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
73    /// person.update_email(email.clone());
74    /// assert_eq!(person.email(), Some(&email));
75    /// ```
76    pub fn update_email(&mut self, email: EmailAddress) {
77        self.email = Some(email);
78    }
79
80    /// Remove the email address of the person.
81    ///
82    /// # Examples
83    /// ```
84    /// use planter_core::person::Person;
85    /// use email_address::EmailAddress;
86    /// use std::str::FromStr;
87    ///
88    /// let mut person = Person::new("Margherita", "Hack").unwrap();
89    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
90    /// person.update_email(email.clone());
91    /// assert_eq!(person.email(), Some(&email));
92    /// person.rm_email();
93    /// assert!(person.email().is_none());
94    /// ```
95    pub fn rm_email(&mut self) {
96        self.email = None;
97    }
98
99    /// Add or edit the phone number of the person.
100    ///
101    /// # Arguments
102    /// * `phone` - The new phone number of the person.
103    ///
104    /// # Examples
105    /// ```
106    /// use planter_core::person::Person;
107    /// use std::str::FromStr;
108    /// use phonenumber::PhoneNumber;
109    ///
110    /// let mut person = Person::new("Margherita", "Hack").unwrap();
111    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
112    /// person.update_phone(phone.clone());
113    /// assert_eq!(person.phone(), Some(&phone));
114    /// ```
115    pub fn update_phone(&mut self, phone: PhoneNumber) {
116        self.phone = Some(phone);
117    }
118
119    /// Remove the phone number of the person.
120    ///
121    /// # Examples
122    /// ```
123    /// use planter_core::person::Person;
124    /// use std::str::FromStr;
125    /// use phonenumber::PhoneNumber;
126    ///
127    /// let mut person = Person::new("Margherita", "Hack").unwrap();
128    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
129    /// person.update_phone(phone.clone());
130    /// assert_eq!(person.phone(), Some(&phone));
131    /// person.rm_phone();
132    /// assert!(person.phone().is_none());
133    /// ```
134    pub fn rm_phone(&mut self) {
135        self.phone = None;
136    }
137
138    /// Get the phone number of the person.
139    ///
140    /// # Examples
141    /// ```
142    /// use planter_core::person::Person;
143    /// use phonenumber::PhoneNumber;
144    /// use std::str::FromStr;
145    ///
146    /// let mut person = Person::new("Margherita", "Hack").unwrap();
147    /// let phone = PhoneNumber::from_str("+1234567890").unwrap();
148    /// person.update_phone(phone.clone());
149    /// assert_eq!(person.phone(), Some(&phone));
150    /// ```
151    #[must_use]
152    pub const fn phone(&self) -> Option<&PhoneNumber> {
153        self.phone.as_ref()
154    }
155
156    /// Get the email of the person.
157    ///
158    /// # Examples
159    /// ```
160    /// use planter_core::person::Person;
161    /// use email_address::EmailAddress;
162    /// use std::str::FromStr;
163    ///
164    /// let mut person = Person::new("Margherita", "Hack").unwrap();
165    /// let email = EmailAddress::from_str("margherita.hack@example.com").unwrap();
166    /// person.update_email(email.clone());
167    /// assert_eq!(person.email(), Some(&email));
168    /// ```
169    #[must_use]
170    pub const fn email(&self) -> Option<&EmailAddress> {
171        self.email.as_ref()
172    }
173
174    /// Get the name of the person.
175    ///
176    /// # Examples
177    /// ```
178    /// use planter_core::person::Person;
179    ///
180    /// let mut person = Person::new("Margherita", "Hack").unwrap();
181    /// assert_eq!(person.full_name(), "Margherita Hack");
182    /// ```
183    #[must_use]
184    pub fn full_name(&self) -> String {
185        format!("{} {}", self.first_name, self.last_name)
186    }
187
188    /// Get the first name of the person.
189    ///
190    /// # Examples
191    /// ```
192    /// use planter_core::person::Person;
193    ///
194    /// let person = Person::new("Margherita", "Hack").unwrap();
195    /// assert_eq!(person.first_name(), "Margherita");
196    /// ```
197    #[must_use]
198    pub fn first_name(&self) -> &str {
199        &self.first_name
200    }
201
202    /// Update the first name of the person.
203    ///
204    /// # Errors
205    ///
206    /// It can return an error, if the input `name` can't be converted to
207    /// `NameString`
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use planter_core::person::Person;
213    ///
214    /// let mut person = Person::new("Margaret", "Hack").unwrap();
215    /// person.update_first_name("Margherita").unwrap();
216    /// assert_eq!(person.first_name(), "Margherita");
217    /// ```
218    pub fn update_first_name(&mut self, name: impl Into<String>) -> anyhow::Result<()> {
219        self.first_name =
220            NameString::try_new(name).context("Input can't be converted into NameString.")?;
221        Ok(())
222    }
223
224    /// Get the last name of the person.
225    ///
226    /// # Examples
227    /// ```
228    /// use planter_core::person::Person;
229    ///
230    /// let mut person = Person::new("Margherita", "Hack").unwrap();
231    /// assert_eq!(person.last_name(), "Hack");
232    /// ```
233    #[must_use]
234    pub fn last_name(&self) -> &str {
235        &self.last_name
236    }
237
238    /// Update the last name of the person.
239    ///
240    /// # Errors
241    ///
242    /// It can return an error, if the input `name` can't be converted to
243    /// `NameString`
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use planter_core::person::Person;
249    ///
250    /// let mut person = Person::new("Margherita", "Hacker").unwrap();
251    /// person.update_last_name("Hack").unwrap();
252    /// assert_eq!(person.last_name(), "Hack");
253    /// ```
254    pub fn update_last_name(&mut self, name: impl Into<String>) -> anyhow::Result<()> {
255        self.last_name =
256            NameString::try_new(name).context("Input can't be converted into NameString.")?;
257        Ok(())
258    }
259}
260
261#[cfg(feature = "serde")]
262impl serde::Serialize for Person {
263    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
264        use serde::ser::SerializeStruct;
265        let mut s = serializer.serialize_struct("Person", 4)?;
266        s.serialize_field("first_name", &*self.first_name)?;
267        s.serialize_field("last_name", &*self.last_name)?;
268        s.serialize_field(
269            "email",
270            &self.email.as_ref().map(std::string::ToString::to_string),
271        )?;
272        s.serialize_field(
273            "phone",
274            &self.phone.as_ref().map(std::string::ToString::to_string),
275        )?;
276        s.end()
277    }
278}
279
280#[cfg(feature = "serde")]
281impl<'de> serde::Deserialize<'de> for Person {
282    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
283        use serde::de;
284        use std::str::FromStr;
285
286        #[derive(serde::Deserialize)]
287        struct Helper {
288            first_name: String,
289            last_name: String,
290            email: Option<String>,
291            phone: Option<String>,
292        }
293
294        let helper = Helper::deserialize(deserializer)?;
295        let first_name = NameString::try_new(helper.first_name).map_err(de::Error::custom)?;
296        let last_name = NameString::try_new(helper.last_name).map_err(de::Error::custom)?;
297        let email = helper
298            .email
299            .map(|e| email_address::EmailAddress::from_str(&e))
300            .transpose()
301            .map_err(de::Error::custom)?;
302        let phone = helper
303            .phone
304            .map(|p| phonenumber::PhoneNumber::from_str(&p))
305            .transpose()
306            .map_err(de::Error::custom)?;
307        Ok(Person {
308            first_name,
309            last_name,
310            email,
311            phone,
312        })
313    }
314}
315
316#[cfg(test)]
317/// Test utilities for the `person` module.
318pub mod test_utils {
319    use std::str::FromStr;
320
321    use email_address::EmailAddress;
322    use phonenumber::PhoneNumber;
323    use proptest::prelude::*;
324
325    use crate::person::Person;
326
327    /// Generate a random email address.
328    pub fn email() -> impl Strategy<Value = EmailAddress> {
329        r"[a-z]{1,10}@[a-z]{1,10}\.[a-z]{2,4}"
330            .prop_map(|s: String| EmailAddress::from_str(&s).unwrap())
331    }
332
333    /// Generate a random phone number.
334    pub fn phone_number() -> impl Strategy<Value = PhoneNumber> {
335        r"\+39[0-9]{6,12}".prop_map(|s: String| PhoneNumber::from_str(&s).unwrap())
336    }
337
338    /// Generate a random valid name string (1-50 alpha chars).
339    pub fn valid_name() -> impl Strategy<Value = String> {
340        "[a-zA-Z]{1,50}"
341    }
342
343    /// Generate a random `Person` with optional email and phone.
344    pub fn person() -> impl Strategy<Value = Person> {
345        (
346            valid_name(),
347            valid_name(),
348            prop::option::of(email()),
349            prop::option::of(phone_number()),
350        )
351            .prop_map(|(first, last, email, phone)| {
352                let mut p = Person::new(first, last).unwrap();
353                if let Some(e) = email {
354                    p.update_email(e);
355                }
356                if let Some(ph) = phone {
357                    p.update_phone(ph);
358                }
359                p
360            })
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use proptest::prelude::*;
367
368    use super::test_utils::{email, phone_number, valid_name};
369    use crate::person::Person;
370
371    proptest! {
372        #[test]
373        fn full_name_equals_first_last(first in valid_name(), last in valid_name()) {
374            let person = Person::new(&first, &last).unwrap();
375            assert_eq!(person.full_name(), format!("{} {}", first, last));
376        }
377
378        #[test]
379        fn update_first_name_roundtrip(first in valid_name(), last in valid_name(), new_first in valid_name()) {
380            let mut person = Person::new(&first, &last).unwrap();
381            person.update_first_name(&new_first).unwrap();
382            assert_eq!(person.first_name(), new_first);
383        }
384
385        #[test]
386        fn update_last_name_roundtrip(first in valid_name(), last in valid_name(), new_last in valid_name()) {
387            let mut person = Person::new(&first, &last).unwrap();
388            person.update_last_name(&new_last).unwrap();
389            assert_eq!(person.last_name(), new_last);
390        }
391
392        #[test]
393        fn update_email_roundtrip(first in valid_name(), last in valid_name(), email in email()) {
394            let mut person = Person::new(&first, &last).unwrap();
395            person.update_email(email.clone());
396            assert_eq!(person.email(), Some(&email));
397            person.rm_email();
398            assert!(person.email().is_none());
399        }
400
401        #[test]
402        fn update_phone_roundtrip(first in valid_name(), last in valid_name(), phone in phone_number()) {
403            let mut person = Person::new(&first, &last).unwrap();
404            person.update_phone(phone.clone());
405            assert_eq!(person.phone(), Some(&phone));
406            person.rm_phone();
407            assert!(person.phone().is_none());
408        }
409
410        #[test]
411        fn new_rejects_empty_name(name in valid_name()) {
412            assert!(Person::new("", &name).is_err());
413            assert!(Person::new(&name, "").is_err());
414        }
415
416        #[test]
417        fn new_rejects_long_name(first in "[a-zA-Z]{51,100}", last in valid_name()) {
418            assert!(Person::new(&first, &last).is_err());
419        }
420
421        #[test]
422        fn update_first_name_rejects_invalid(name in valid_name(), bad in "[a-zA-Z]{51,100}") {
423            let mut person = Person::new("valid", &name).unwrap();
424            assert!(person.update_first_name(&bad).is_err());
425        }
426
427        #[test]
428        fn update_last_name_rejects_invalid(name in valid_name(), bad in "[a-zA-Z]{51,100}") {
429            let mut person = Person::new(&name, "valid").unwrap();
430            assert!(person.update_last_name(&bad).is_err());
431        }
432    }
433}
434
435#[cfg(all(test, feature = "serde"))]
436mod serde_tests {
437    use proptest::prelude::*;
438
439    use crate::person::Person;
440    use crate::person::test_utils::person;
441
442    proptest! {
443        #[test]
444        fn serde_roundtrip(p in person()) {
445            let json = serde_json::to_string(&p).unwrap();
446            let deserialized: Person = serde_json::from_str(&json).unwrap();
447            assert_eq!(p, deserialized);
448        }
449    }
450}