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)]
9pub struct Person {
11 first_name: NameString,
13 last_name: NameString,
15 email: Option<EmailAddress>,
17 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 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 pub fn update_email(&mut self, email: EmailAddress) {
87 self.email = Some(email);
88 }
89
90 pub fn rm_email(&mut self) {
106 self.email = None;
107 }
108
109 pub fn update_phone(&mut self, phone: PhoneNumber) {
126 self.phone = Some(phone);
127 }
128
129 pub fn rm_phone(&mut self) {
145 self.phone = None;
146 }
147
148 #[must_use]
162 pub const fn phone(&self) -> Option<&PhoneNumber> {
163 self.phone.as_ref()
164 }
165
166 #[must_use]
180 pub const fn email(&self) -> Option<&EmailAddress> {
181 self.email.as_ref()
182 }
183
184 #[must_use]
194 pub fn full_name(&self) -> String {
195 format!("{} {}", self.first_name, self.last_name)
196 }
197
198 #[must_use]
208 pub fn first_name(&self) -> &str {
209 &self.first_name
210 }
211
212 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 #[must_use]
244 pub fn last_name(&self) -> &str {
245 &self.last_name
246 }
247
248 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)]
327pub 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 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 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 pub fn valid_name() -> impl Strategy<Value = String> {
350 "[a-zA-Z]{1,50}"
351 }
352
353 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}