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}