1use crate::{
4 data::{
5 Account, Agent, AgentId, CIString, DataError, Fingerprint, MyEmailAddress, ObjectType,
6 Validate, ValidationError, check_for_nulls, fingerprint_it, validate_sha1sum,
7 },
8 emit_error, set_email,
9};
10use core::fmt;
11use iri_string::types::{UriStr, UriString};
12use serde::{Deserialize, Serialize};
13use serde_json::{Map, Value};
14use serde_with::skip_serializing_none;
15use std::{
16 cmp::Ordering,
17 hash::{Hash, Hasher},
18 str::FromStr,
19};
20
21#[skip_serializing_none]
28#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
29#[serde(deny_unknown_fields)]
30pub struct Group {
31 #[serde(rename = "objectType")]
32 object_type: ObjectType,
33 name: Option<CIString>,
34 #[serde(rename = "member")]
35 members: Option<Vec<Agent>>,
36 mbox: Option<MyEmailAddress>,
37 mbox_sha1sum: Option<String>,
38 openid: Option<UriString>,
39 account: Option<Account>,
40}
41
42#[skip_serializing_none]
43#[derive(Debug, Serialize)]
44#[doc(hidden)]
45pub(crate) struct GroupId {
46 #[serde(rename = "objectType")]
47 object_type: ObjectType,
48 #[serde(rename = "member")]
49 members: Option<Vec<AgentId>>,
50 mbox: Option<MyEmailAddress>,
51 mbox_sha1sum: Option<String>,
52 openid: Option<UriString>,
53 account: Option<Account>,
54}
55
56impl From<Group> for GroupId {
57 fn from(value: Group) -> Self {
58 GroupId {
59 object_type: ObjectType::Group,
60 members: {
61 if value.members.is_some() {
62 let members = value.members.unwrap();
63 if members.is_empty() {
64 None
65 } else {
66 Some(members.into_iter().map(AgentId::from).collect())
67 }
68 } else {
69 None
70 }
71 },
72 mbox: value.mbox,
73 mbox_sha1sum: value.mbox_sha1sum,
74 openid: value.openid,
75 account: value.account,
76 }
77 }
78}
79
80impl From<GroupId> for Group {
81 fn from(value: GroupId) -> Self {
82 Group {
83 object_type: ObjectType::Group,
84 name: None,
85 members: {
86 if value.members.is_some() {
87 let members = value.members.unwrap();
88 if members.is_empty() {
89 None
90 } else {
91 Some(members.into_iter().map(Agent::from).collect())
92 }
93 } else {
94 None
95 }
96 },
97 mbox: value.mbox,
98 mbox_sha1sum: value.mbox_sha1sum,
99 openid: value.openid,
100 account: value.account,
101 }
102 }
103}
104
105impl Group {
106 pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
108 for (k, v) in &map {
109 if v.is_null() {
110 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
111 format!("Key '{k}' is null").into()
112 )))
113 } else {
114 check_for_nulls(v)?
115 }
116 }
117 let group: Group = serde_json::from_value(Value::Object(map))?;
119 group.check_validity()?;
120 Ok(group)
121 }
122
123 pub fn builder() -> GroupBuilder {
125 GroupBuilder::default()
126 }
127
128 pub fn check_object_type(&self) -> bool {
132 self.object_type == ObjectType::Group
133 }
134
135 pub fn is_anonymous(&self) -> bool {
137 self.mbox.is_none()
138 && self.mbox_sha1sum.is_none()
139 && self.account.is_none()
140 && self.openid.is_none()
141 }
142
143 pub fn name(&self) -> Option<&CIString> {
145 self.name.as_ref()
146 }
147
148 pub fn name_as_str(&self) -> Option<&str> {
150 self.name.as_deref()
151 }
152
153 pub fn members(&self) -> Vec<&Agent> {
158 if self.members.is_none() {
159 vec![]
160 } else {
161 self.members
162 .as_ref()
163 .unwrap()
164 .as_slice()
165 .iter()
166 .collect::<Vec<_>>()
167 }
168 }
169
170 pub fn mbox(&self) -> Option<&MyEmailAddress> {
172 self.mbox.as_ref()
173 }
174
175 pub fn mbox_sha1sum(&self) -> Option<&str> {
178 self.mbox_sha1sum.as_deref()
179 }
180
181 pub fn openid(&self) -> Option<&UriStr> {
183 self.openid.as_deref()
184 }
185
186 pub fn account(&self) -> Option<&Account> {
189 self.account.as_ref()
190 }
191
192 pub fn uid(&self) -> u64 {
194 fingerprint_it(self)
195 }
196
197 pub fn equivalent(&self, that: &Group) -> bool {
199 self.uid() == that.uid()
200 }
201}
202
203impl fmt::Display for Group {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 let mut vec = vec![];
206
207 if self.name.is_some() {
208 vec.push(format!("name: \"{}\"", self.name().unwrap()));
209 }
210 if self.mbox.is_some() {
211 vec.push(format!("mbox: \"{}\"", self.mbox().unwrap()));
212 }
213 if self.mbox_sha1sum.is_some() {
214 vec.push(format!(
215 "mbox_sha1sum: \"{}\"",
216 self.mbox_sha1sum().unwrap()
217 ));
218 }
219 if self.account.is_some() {
220 vec.push(format!("account: {}", self.account().unwrap()));
221 }
222 if self.openid.is_some() {
223 vec.push(format!("openid: \"{}\"", self.openid().unwrap()));
224 }
225 if self.members.is_some() {
226 let members = self.members.as_deref().unwrap();
227 vec.push(format!(
228 "members: [{}]",
229 members
230 .iter()
231 .map(|x| x.to_string())
232 .collect::<Vec<_>>()
233 .join(", ")
234 ))
235 }
236
237 let res = vec
238 .iter()
239 .map(|x| x.to_string())
240 .collect::<Vec<_>>()
241 .join(", ");
242 write!(f, "Group{{ {res} }}")
243 }
244}
245
246impl Fingerprint for Group {
247 fn fingerprint<H: Hasher>(&self, state: &mut H) {
248 if self.members.is_some() {
250 let mut members = self.members.clone().unwrap();
252 members.sort_unstable();
253 Fingerprint::fingerprint_slice(&members, state);
254 }
255 if self.mbox.is_some() {
256 self.mbox.as_ref().unwrap().fingerprint(state);
257 }
258 self.mbox_sha1sum.hash(state);
259 self.openid.hash(state);
260 if self.account.is_some() {
261 self.account.as_ref().unwrap().fingerprint(state);
262 }
263 }
264}
265
266impl Ord for Group {
267 fn cmp(&self, other: &Self) -> Ordering {
268 fingerprint_it(self).cmp(&fingerprint_it(other))
269 }
270}
271
272impl PartialOrd for Group {
273 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
274 Some(self.cmp(other))
275 }
276}
277
278impl Validate for Group {
279 fn validate(&self) -> Vec<ValidationError> {
280 let mut vec = vec![];
281
282 if !self.check_object_type() {
283 vec.push(ValidationError::WrongObjectType {
284 expected: ObjectType::Group,
285 found: self.object_type.to_string().into(),
286 })
287 }
288 if self.name.is_some() && self.name.as_ref().unwrap().is_empty() {
289 vec.push(ValidationError::Empty("name".into()))
290 }
291 let mut count = 0;
294 if self.mbox.is_some() {
295 count += 1;
296 }
298 if self.mbox_sha1sum.is_some() {
299 count += 1;
300 validate_sha1sum(self.mbox_sha1sum.as_ref().unwrap()).unwrap_or_else(|x| vec.push(x))
301 }
302 if self.openid.is_some() {
303 count += 1;
304 }
305 if self.account.is_some() {
306 count += 1;
307 vec.extend(self.account.as_ref().unwrap().validate())
308 }
309 if self.is_anonymous() {
310 if self.members.is_none() {
312 vec.push(ValidationError::EmptyAnonymousGroup)
313 }
314 } else if count != 1 {
315 vec.push(ValidationError::ConstraintViolation(
316 "Exactly 1 IFI is required".into(),
317 ))
318 }
319 if self.members.is_some() {
321 self.members
322 .as_ref()
323 .unwrap()
324 .iter()
325 .for_each(|x| vec.extend(x.validate()));
326 }
327
328 vec
329 }
330}
331
332impl FromStr for Group {
333 type Err = DataError;
334
335 fn from_str(s: &str) -> Result<Self, Self::Err> {
336 let map = serde_json::from_str::<Map<String, Value>>(s)?;
337 Self::from_json_obj(map)
338 }
339}
340
341#[derive(Debug, Default)]
343pub struct GroupBuilder {
344 _name: Option<CIString>,
345 _members: Option<Vec<Agent>>,
346 _mbox: Option<MyEmailAddress>,
347 _sha1sum: Option<String>,
348 _openid: Option<UriString>,
349 _account: Option<Account>,
350}
351
352impl GroupBuilder {
353 pub fn name(mut self, s: &str) -> Result<Self, DataError> {
357 let s = s.trim();
358 if s.is_empty() {
359 emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
360 }
361 self._name = Some(CIString::from(s));
362 Ok(self)
363 }
364
365 pub fn member(mut self, val: Agent) -> Result<Self, DataError> {
369 val.check_validity()?;
370 if self._members.is_none() {
371 self._members = Some(vec![]);
372 }
373 self._members.as_mut().unwrap().push(val);
374 Ok(self)
375 }
376
377 pub fn mbox(mut self, s: &str) -> Result<Self, DataError> {
385 set_email!(self, s)
386 }
387
388 pub fn mbox_sha1sum(mut self, s: &str) -> Result<Self, DataError> {
396 let s = s.trim();
397 if s.is_empty() {
398 emit_error!(DataError::Validation(ValidationError::Empty(
399 "mbox_sha1sum".into()
400 )))
401 }
402
403 validate_sha1sum(s)?;
404
405 self._sha1sum = Some(s.to_owned());
406 self._mbox = None;
407 self._openid = None;
408 self._account = None;
409 Ok(self)
410 }
411
412 pub fn openid(mut self, s: &str) -> Result<Self, DataError> {
420 let s = s.trim();
421 if s.is_empty() {
422 emit_error!(DataError::Validation(ValidationError::Empty(
423 "openid".into()
424 )))
425 }
426
427 let uri = UriString::from_str(s)?;
428 self._openid = Some(uri);
429 self._mbox = None;
430 self._sha1sum = None;
431 self._account = None;
432 Ok(self)
433 }
434
435 pub fn account(mut self, val: Account) -> Result<Self, DataError> {
442 val.check_validity()?;
443 self._account = Some(val);
444 self._mbox = None;
445 self._sha1sum = None;
446 self._openid = None;
447 Ok(self)
448 }
449
450 pub fn build(mut self) -> Result<Group, DataError> {
454 if self._mbox.is_none()
455 && self._sha1sum.is_none()
456 && self._openid.is_none()
457 && self._account.is_none()
458 {
459 return Err(DataError::Validation(ValidationError::MissingIFI(
460 "Group".into(),
461 )));
462 }
463
464 if self._members.is_some() {
466 self._members.as_mut().unwrap().sort_unstable();
467 }
468 Ok(Group {
469 object_type: ObjectType::Group,
470 name: self._name,
471 members: self._members,
472 mbox: self._mbox,
473 mbox_sha1sum: self._sha1sum,
474 openid: self._openid,
475 account: self._account,
476 })
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use tracing_test::traced_test;
484
485 #[traced_test]
486 #[test]
487 fn test_identified_group() {
488 const JSON: &str = r#"{
489 "objectType": "Group",
490 "name": "Z Group",
491 "account": {
492 "homePage": "http://inter.net/home",
493 "name": "ganon"
494 },
495 "member": [
496 { "objectType": "Agent", "name": "foo", "mbox": "mailto:foo@mail.inter.net" },
497 { "objectType": "Agent", "name": "bar", "openid": "https://inter.net/oid" }
498 ]
499 }"#;
500 let de_result = serde_json::from_str::<Group>(JSON);
501 assert!(de_result.is_ok());
502 let g = de_result.unwrap();
503
504 assert!(!g.is_anonymous());
505 }
506
507 #[traced_test]
508 #[test]
509 fn test_identified_0_agents() {
510 const JSON: &str = r#"{"objectType":"Group","name":"Z Group","account":{"homePage":"http://inter.net/home","name":"ganon"}}"#;
511
512 let de_result = serde_json::from_str::<Group>(JSON);
513 assert!(de_result.is_ok());
514 let g = de_result.unwrap();
515
516 assert!(!g.is_anonymous());
517 }
518
519 #[traced_test]
520 #[test]
521 fn test_anonymous_group() -> Result<(), DataError> {
522 const JSON_IN_: &str = r#"{"objectType":"Group","name":"Z Group","member":[{"objectType":"Agent","name":"foo","mbox":"mailto:foo@mail.inter.net"},{"objectType":"Agent","name":"bar","openid":"https://inter.net/oid"}],"account":{"homePage":"http://inter.net/home","name":"ganon"}}"#;
523 const JSON_OUT: &str = r#"{"objectType":"Group","name":"Z Group","member":[{"objectType":"Agent","name":"bar","openid":"https://inter.net/oid"},{"objectType":"Agent","name":"foo","mbox":"mailto:foo@mail.inter.net"}],"account":{"homePage":"http://inter.net/home","name":"ganon"}}"#;
524
525 let g1 = Group::builder()
526 .name("Z Group")?
527 .account(
528 Account::builder()
529 .home_page("http://inter.net/home")?
530 .name("ganon")?
531 .build()?,
532 )?
533 .member(
534 Agent::builder()
535 .with_object_type()
536 .name("foo")?
537 .mbox("foo@mail.inter.net")?
538 .build()?,
539 )?
540 .member(
541 Agent::builder()
542 .with_object_type()
543 .name("bar")?
544 .openid("https://inter.net/oid")?
545 .build()?,
546 )?
547 .build()?;
548 let se_result = serde_json::to_string(&g1);
549 assert!(se_result.is_ok());
550 let json = se_result.unwrap();
551 assert_eq!(json, JSON_OUT);
552
553 let de_result = serde_json::from_str::<Group>(JSON_IN_);
554 assert!(de_result.is_ok());
555 let g2 = de_result.unwrap();
556
557 assert_ne!(g1, g2);
561 assert!(g1.equivalent(&g2));
562
563 Ok(())
564 }
565
566 #[traced_test]
567 #[test]
568 fn test_long_group() {
569 const JSON: &str = r#"{
570 "name": "Team PB",
571 "mbox": "mailto:teampb@example.com",
572 "member": [
573 {
574 "name": "Andrew Downes",
575 "account": {
576 "homePage": "http://www.example.com",
577 "name": "13936749"
578 },
579 "objectType": "Agent"
580 },
581 {
582 "name": "Toby Nichols",
583 "openid": "http://toby.openid.example.org/",
584 "objectType": "Agent"
585 },
586 {
587 "name": "Ena Hills",
588 "mbox_sha1sum": "ebd31e95054c018b10727ccffd2ef2ec3a016ee9",
589 "objectType": "Agent"
590 }
591 ],
592 "objectType": "Group"
593 }"#;
594
595 let de_result = serde_json::from_str::<Group>(JSON);
596 assert!(de_result.is_ok());
597 let g = de_result.unwrap();
598
599 assert!(!g.is_anonymous());
600
601 assert!(g.name().is_some());
602 assert_eq!(g.name().unwrap(), "Team PB");
603
604 assert!(g.mbox().is_some());
605 assert_eq!(g.mbox().unwrap().to_uri(), "mailto:teampb@example.com");
606 assert!(g.mbox_sha1sum().is_none());
607 assert!(g.account().is_none());
608 assert!(g.openid().is_none());
609
610 assert_eq!(g.members().len(), 3);
611 }
612}