1use crate::{
4 data::{DataError, Fingerprint, Validate, ValidationError, validate::validate_irl},
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 let Some(z_home_page) = self._home_page {
223 if self._name.is_empty() {
224 emit_error!(DataError::Validation(ValidationError::MissingField(
225 "name".into()
226 )))
227 } else {
228 Ok(Account {
229 home_page: z_home_page.into(),
230 name: self._name.to_owned(),
231 })
232 }
233 } else {
234 emit_error!(DataError::Validation(ValidationError::MissingField(
235 "home_page".into()
236 )))
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use tracing_test::traced_test;
245
246 #[traced_test]
247 #[test]
248 fn test_serde() -> Result<(), DataError> {
249 const JSON: &str = r#"{"homePage":"https://inter.net/","name":"user"}"#;
250
251 let a1 = Account::builder()
252 .home_page("https://inter.net/")?
253 .name("user")?
254 .build()?;
255
256 let se_result = serde_json::to_string(&a1);
257 assert!(se_result.is_ok());
258 let json = se_result.unwrap();
259 assert_eq!(json, JSON);
260
261 let de_result = serde_json::from_str::<Account>(JSON);
262 assert!(de_result.is_ok());
263 let a2 = de_result.unwrap();
264 assert_eq!(a1, a2);
265
266 const JSON_: &str = r#"{"name":"user","homePage":"https://inter.net/"}"#;
268 let de_result = serde_json::from_str::<Account>(JSON_);
269 assert!(de_result.is_ok());
270 let a4 = de_result.unwrap();
271 assert_eq!(a1, a4);
272
273 Ok(())
274 }
275
276 #[traced_test]
277 #[test]
278 fn test_display() -> Result<(), DataError> {
279 const DISPLAY: &str =
280 r#"Account{ homePage: "http://résumé.example.org/", name: "zRésumé" }"#;
281 let a = Account::builder()
284 .home_page("http://résumé.example.org/")?
285 .name("zRésumé")?
286 .build()?;
287
288 let disp = a.to_string();
289 assert_eq!(disp, DISPLAY);
290
291 assert_eq!(a.home_page_as_str(), "http://résumé.example.org/");
293
294 assert_eq!(
296 a.home_page()
297 .encode_to_uri()
298 .to_dedicated_string()
299 .normalize()
300 .to_dedicated_string()
301 .as_str(),
302 "http://r%C3%A9sum%C3%A9.example.org/"
303 );
304 assert_eq!(
306 a.home_page()
307 .encode_to_uri()
308 .to_dedicated_string()
309 .normalize()
310 .to_dedicated_string()
311 .as_str(),
312 a.home_page_as_uri()
313 );
314
315 Ok(())
316 }
317
318 #[traced_test]
319 #[test]
320 fn test_validation() -> Result<(), DataError> {
321 let a = Account::builder()
322 .home_page("http://résumé.example.org/")?
323 .name("zRésumé")?
324 .build()?;
325
326 let r = a.validate();
327 assert!(r.is_empty());
328
329 Ok(())
330 }
331
332 #[test]
333 fn test_runtime_error_macro() -> Result<(), DataError> {
334 let r1 = Account::builder().home_page("");
335 let e1 = r1.err().unwrap();
336 assert!(matches!(e1, DataError::Validation { .. }));
337
338 let r2 = Account::builder()
339 .home_page("http://résumé.example.org/")?
340 .build();
341 let e2 = r2.err().unwrap();
342 assert!(matches!(e2, DataError::Validation { .. }));
343
344 Ok(())
345 }
346}