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}