1use crate::{
4 data::{validate::validate_irl, DataError, Fingerprint, Validate, ValidationError},
5 emit_error,
6};
7use core::fmt;
8use iri_string::{
9 convert::MappedToUri,
10 format::ToDedicatedString,
11 types::{IriStr, IriString, UriString},
12};
13use serde::{Deserialize, Serialize};
14use std::{
15 cmp::Ordering,
16 hash::{Hash, Hasher},
17};
18
19const SEPARATOR: char = '~';
22
23#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
32pub struct Account {
33 #[serde(rename = "homePage")]
34 home_page: IriString,
35 name: String,
36}
37
38impl Account {
39 fn from(home_page: &str, name: &str) -> Result<Self, DataError> {
41 let home_page = home_page.trim();
42 if home_page.is_empty() {
43 emit_error!(DataError::Validation(ValidationError::Empty(
44 "home_page".into()
45 )))
46 }
47
48 let home_page = IriStr::new(home_page)?;
49 validate_irl(home_page)?;
50
51 let name = name.trim();
52 if name.is_empty() {
53 emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
54 }
55
56 Ok(Account {
57 home_page: home_page.into(),
58 name: name.to_owned(),
59 })
60 }
61
62 pub fn builder() -> AccountBuilder<'static> {
64 AccountBuilder::default()
65 }
66
67 pub fn home_page(&self) -> &IriStr {
69 &self.home_page
70 }
71
72 pub fn home_page_as_str(&self) -> &str {
74 self.home_page.as_str()
75 }
76
77 pub fn home_page_as_uri(&self) -> UriString {
79 let uri = MappedToUri::from(&self.home_page).to_dedicated_string();
80 uri.normalize().to_dedicated_string()
81 }
82
83 pub fn name(&self) -> &str {
85 &self.name
86 }
87
88 pub fn as_joined_str(&self) -> String {
91 format!("{}{}{}", self.home_page, SEPARATOR, self.name)
92 }
93}
94
95impl fmt::Display for Account {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(
98 f,
99 "Account{{ homePage: \"{}\", name: \"{}\" }}",
100 self.home_page_as_str(),
101 self.name
102 )
103 }
104}
105
106impl Fingerprint for Account {
107 fn fingerprint<H: Hasher>(&self, state: &mut H) {
108 let (x, y) = self.home_page.as_slice().to_absolute_and_fragment();
109 x.normalize().to_string().hash(state);
110 y.hash(state);
111 self.name.hash(state);
112 }
113}
114
115impl PartialOrd for Account {
116 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
117 let (x1, y1) = self.home_page.as_slice().to_absolute_and_fragment();
118 let (x2, y2) = other.home_page.as_slice().to_absolute_and_fragment();
119 match x1
120 .normalize()
121 .to_string()
122 .partial_cmp(&x2.normalize().to_string())
123 {
124 Some(Ordering::Equal) => match y1.partial_cmp(&y2) {
125 Some(Ordering::Equal) => {}
126 x => return x,
127 },
128 x => return x,
129 }
130 self.name.partial_cmp(&other.name)
131 }
132}
133
134impl Validate for Account {
135 fn validate(&self) -> Vec<ValidationError> {
136 let mut vec: Vec<ValidationError> = vec![];
137 validate_irl(self.home_page.as_ref()).unwrap_or_else(|x| vec.push(x));
138 if self.name.trim().is_empty() {
139 vec.push(ValidationError::Empty("name".into()))
140 }
141 vec
142 }
143}
144
145impl TryFrom<String> for Account {
146 type Error = DataError;
147
148 fn try_from(value: String) -> Result<Self, Self::Error> {
149 let mut parts = value.split(SEPARATOR);
150 Account::from(
151 parts.next().ok_or_else(|| {
152 DataError::Validation(ValidationError::MissingField("home_page".into()))
153 })?,
154 parts.next().ok_or_else(|| {
155 DataError::Validation(ValidationError::MissingField("name".into()))
156 })?,
157 )
158 }
159}
160
161#[derive(Debug, Default)]
163pub struct AccountBuilder<'a> {
164 _home_page: Option<&'a IriStr>,
165 _name: &'a str,
166}
167
168impl<'a> AccountBuilder<'a> {
169 pub fn from(s: &str) -> Result<Account, DataError> {
175 let parts: Vec<_> = s.trim().split(SEPARATOR).collect();
176 if parts.len() < 2 {
177 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
178 "Missing separator".into()
179 )))
180 }
181 Account::builder()
182 .home_page(parts[0])?
183 .name(parts[1])?
184 .build()
185 }
186
187 pub fn home_page(mut self, val: &'a str) -> Result<Self, DataError> {
192 let home_page = val.trim();
193 if home_page.is_empty() {
194 emit_error!(DataError::Validation(ValidationError::Empty(
195 "home_page".into()
196 )))
197 } else {
198 let home_page = IriStr::new(home_page)?;
199 validate_irl(home_page)?;
200 self._home_page = Some(home_page);
201 Ok(self)
202 }
203 }
204
205 pub fn name(mut self, val: &'a str) -> Result<Self, DataError> {
209 let name = val.trim();
210 if name.is_empty() {
211 emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
212 } else {
213 self._name = val;
214 Ok(self)
215 }
216 }
217
218 pub fn build(&self) -> Result<Account, DataError> {
222 if self._home_page.is_none() {
223 emit_error!(DataError::Validation(ValidationError::MissingField(
224 "hom_page".into()
225 )))
226 } else if self._name.is_empty() {
227 emit_error!(DataError::Validation(ValidationError::MissingField(
228 "name".into()
229 )))
230 } else {
231 Ok(Account {
232 home_page: self._home_page.unwrap().into(),
233 name: self._name.to_owned(),
234 })
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use tracing_test::traced_test;
243
244 #[traced_test]
245 #[test]
246 fn test_serde() -> Result<(), DataError> {
247 const JSON: &str = r#"{"homePage":"https://inter.net/","name":"user"}"#;
248
249 let a1 = Account::builder()
250 .home_page("https://inter.net/")?
251 .name("user")?
252 .build()?;
253
254 let se_result = serde_json::to_string(&a1);
255 assert!(se_result.is_ok());
256 let json = se_result.unwrap();
257 assert_eq!(json, JSON);
258
259 let de_result = serde_json::from_str::<Account>(JSON);
260 assert!(de_result.is_ok());
261 let a2 = de_result.unwrap();
262 assert_eq!(a1, a2);
263
264 const JSON_: &str = r#"{"name":"user","homePage":"https://inter.net/"}"#;
266 let de_result = serde_json::from_str::<Account>(JSON_);
267 assert!(de_result.is_ok());
268 let a4 = de_result.unwrap();
269 assert_eq!(a1, a4);
270
271 Ok(())
272 }
273
274 #[traced_test]
275 #[test]
276 fn test_display() -> Result<(), DataError> {
277 const DISPLAY: &str =
278 r#"Account{ homePage: "http://résumé.example.org/", name: "zRésumé" }"#;
279 let a = Account::builder()
282 .home_page("http://résumé.example.org/")?
283 .name("zRésumé")?
284 .build()?;
285
286 let disp = a.to_string();
287 assert_eq!(disp, DISPLAY);
288
289 assert_eq!(a.home_page_as_str(), "http://résumé.example.org/");
291
292 assert_eq!(
294 a.home_page()
295 .encode_to_uri()
296 .to_dedicated_string()
297 .normalize()
298 .to_dedicated_string()
299 .as_str(),
300 "http://r%C3%A9sum%C3%A9.example.org/"
301 );
302 assert_eq!(
304 a.home_page()
305 .encode_to_uri()
306 .to_dedicated_string()
307 .normalize()
308 .to_dedicated_string()
309 .as_str(),
310 a.home_page_as_uri()
311 );
312
313 Ok(())
314 }
315
316 #[traced_test]
317 #[test]
318 fn test_validation() -> Result<(), DataError> {
319 let a = Account::builder()
320 .home_page("http://résumé.example.org/")?
321 .name("zRésumé")?
322 .build()?;
323
324 let r = a.validate();
325 assert!(r.is_empty());
326
327 Ok(())
328 }
329
330 #[test]
331 fn test_runtime_error_macro() -> Result<(), DataError> {
332 let r1 = Account::builder().home_page("");
333 let e1 = r1.err().unwrap();
334 assert!(matches!(e1, DataError::Validation { .. }));
335
336 let r2 = Account::builder()
337 .home_page("http://résumé.example.org/")?
338 .build();
339 let e2 = r2.err().unwrap();
340 assert!(matches!(e2, DataError::Validation { .. }));
341
342 Ok(())
343 }
344}