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