sofie_values/
person_name.rs1use std::fmt;
2
3use sofie_core::{Error, Result};
4
5const MAXIMUM_LENGTH: usize = 200;
6
7#[derive(Clone, PartialEq, Eq, Hash)]
9pub struct PersonName {
10 name: String,
11}
12
13impl PersonName {
14 pub fn parse(text: &str) -> Result<Self> {
15 let text = text.trim();
16 if text.len() > MAXIMUM_LENGTH * 4 {
18 return Err(invalid_name("must have at most 200 characters"));
19 }
20 let mut name = String::with_capacity(text.len());
21 for word in text.split_whitespace() {
22 if !name.is_empty() {
23 name.push(' ');
24 }
25 name.push_str(word);
26 }
27 Self::from_collapsed(name)
28 }
29
30 fn from_stored(text: String) -> Result<Self> {
31 if is_collapsed(&text) {
32 return Self::from_collapsed(text);
33 }
34 Self::parse(&text)
35 }
36
37 fn from_collapsed(name: String) -> Result<Self> {
38 if name.is_empty() {
39 return Err(invalid_name("is required"));
40 }
41 if name.chars().count() > MAXIMUM_LENGTH {
42 return Err(invalid_name("must have at most 200 characters"));
43 }
44 if name.chars().any(sofie_core::misleads_display) {
45 return Err(invalid_name(
46 "must not contain control or invisible formatting characters",
47 ));
48 }
49 Ok(Self { name })
50 }
51
52 #[must_use]
53 pub fn as_str(&self) -> &str {
54 &self.name
55 }
56}
57
58fn is_collapsed(text: &str) -> bool {
60 let mut after_space = true;
61 for character in text.chars() {
62 if !character.is_whitespace() {
63 after_space = false;
64 } else if character != ' ' || after_space {
65 return false;
66 } else {
67 after_space = true;
68 }
69 }
70 !after_space || text.is_empty()
71}
72
73fn invalid_name(message: &str) -> Error {
74 Error::invalid("name", message)
75}
76
77impl fmt::Debug for PersonName {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 let first = self.name.chars().next().unwrap_or_default();
80 write!(formatter, "PersonName({first}***)")
81 }
82}
83
84sofie_core::text_value!(PersonName, name, PersonName::from_stored);
85
86crate::present::present_text!(
87 PersonName,
88 crate::Input::typed(&[("type", "text"), ("autocomplete", "name")]),
89 Text
90);
91
92#[cfg(test)]
93mod tests {
94 use sofie_core::SqlValue;
95
96 use super::*;
97 use crate::testing::{assert_refused, round_trip};
98
99 #[test]
100 fn keeps_a_name_as_written_with_whitespace_collapsed() {
101 for (text, name) in [
102 ("Ada Lovelace", "Ada Lovelace"),
103 (" Gabriel García\tMárquez ", "Gabriel García Márquez"),
104 ("毛泽东", "毛泽东"),
105 ("O'Brien-Smith", "O'Brien-Smith"),
106 ("می\u{200C}خواهم", "می\u{200C}خواهم"),
107 ] {
108 assert_eq!(PersonName::parse(text).unwrap().as_str(), name);
109 }
110 assert!(PersonName::parse(&"é".repeat(200)).is_ok());
111 }
112
113 #[test]
114 fn refuses_empty_overlong_control_and_invisible_formatting_characters() {
115 assert_refused(
116 PersonName::parse,
117 &[
118 "",
119 " ",
120 "Ada\u{0}Lovelace",
121 "Ada\u{7f}",
122 "Ada\u{202E}ecalevoL",
123 "Ada\u{200B}",
124 &"é".repeat(201),
125 ],
126 );
127 }
128
129 #[test]
130 fn stored_text_is_checked_and_collapsed_like_input() {
131 use sofie_core::value::Value;
132 let stored = |text: &str| PersonName::try_from_inner(text.to_owned()).map(|name| name.name);
133
134 assert_eq!(stored("Ada Lovelace").ok().as_deref(), Some("Ada Lovelace"));
135 assert_eq!(
136 stored(" Ada Lovelace\t").ok().as_deref(),
137 Some("Ada Lovelace")
138 );
139 assert!(stored("").is_err());
140 assert!(stored("Ada\u{202E}").is_err());
141 }
142
143 #[test]
144 fn masks_itself_in_debug() {
145 let name = PersonName::parse("Ada Lovelace").unwrap();
146
147 assert_eq!(format!("{name:?}"), "PersonName(A***)");
148 }
149
150 #[test]
151 fn round_trips_through_a_text_column_and_json() {
152 let name = PersonName::parse("Ada Lovelace").unwrap();
153 assert_eq!(
154 round_trip(&name),
155 vec![SqlValue::Text("Ada Lovelace".into())]
156 );
157 let json = serde_json::to_value(&name).unwrap();
158 assert_eq!(serde_json::from_value::<PersonName>(json).unwrap(), name);
159 }
160}