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 let Some(members) = value.members {
62 if members.is_empty() {
63 None
64 } else {
65 Some(members.into_iter().map(AgentId::from).collect())
66 }
67 } else {
68 None
69 }
70 },
71 mbox: value.mbox,
72 mbox_sha1sum: value.mbox_sha1sum,
73 openid: value.openid,
74 account: value.account,
75 }
76 }
77}
78
79impl From<GroupId> for Group {
80 fn from(value: GroupId) -> Self {
81 Group {
82 object_type: ObjectType::Group,
83 name: None,
84 members: {
85 if let Some(members) = value.members {
86 if members.is_empty() {
87 None
88 } else {
89 Some(members.into_iter().map(Agent::from).collect())
90 }
91 } else {
92 None
93 }
94 },
95 mbox: value.mbox,
96 mbox_sha1sum: value.mbox_sha1sum,
97 openid: value.openid,
98 account: value.account,
99 }
100 }
101}
102
103impl Group {
104 pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
106 for (k, v) in &map {
107 if v.is_null() {
108 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
109 format!("Key '{k}' is null").into()
110 )))
111 } else {
112 check_for_nulls(v)?
113 }
114 }
115 let group: Group = serde_json::from_value(Value::Object(map))?;
117 group.check_validity()?;
118 Ok(group)
119 }
120
121 pub fn builder() -> GroupBuilder {
123 GroupBuilder::default()
124 }
125
126 pub fn check_object_type(&self) -> bool {
130 self.object_type == ObjectType::Group
131 }
132
133 pub fn is_anonymous(&self) -> bool {
135 self.mbox.is_none()
136 && self.mbox_sha1sum.is_none()
137 && self.account.is_none()
138 && self.openid.is_none()
139 }
140
141 pub fn name(&self) -> Option<&CIString> {
143 self.name.as_ref()
144 }
145
146 pub fn name_as_str(&self) -> Option<&str> {
148 self.name.as_deref()
149 }
150
151 pub fn members(&self) -> Vec<&Agent> {
156 if let Some(z_members) = self.members.as_ref() {
157 z_members.as_slice().iter().collect::<Vec<_>>()
158 } else {
159 vec![]
160 }
161 }
162
163 pub fn mbox(&self) -> Option<&MyEmailAddress> {
165 self.mbox.as_ref()
166 }
167
168 pub fn mbox_sha1sum(&self) -> Option<&str> {
171 self.mbox_sha1sum.as_deref()
172 }
173
174 pub fn openid(&self) -> Option<&UriStr> {
176 self.openid.as_deref()
177 }
178
179 pub fn account(&self) -> Option<&Account> {
182 self.account.as_ref()
183 }
184
185 pub fn uid(&self) -> u64 {
187 fingerprint_it(self)
188 }
189
190 pub fn equivalent(&self, that: &Group) -> bool {
192 self.uid() == that.uid()
193 }
194}
195
196impl fmt::Display for Group {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 let mut vec = vec![];
199
200 if self.name.is_some() {
201 vec.push(format!("name: \"{}\"", self.name().unwrap()));
202 }
203 if self.mbox.is_some() {
204 vec.push(format!("mbox: \"{}\"", self.mbox().unwrap()));
205 }
206 if self.mbox_sha1sum.is_some() {
207 vec.push(format!(
208 "mbox_sha1sum: \"{}\"",
209 self.mbox_sha1sum().unwrap()
210 ));
211 }
212 if self.account.is_some() {
213 vec.push(format!("account: {}", self.account().unwrap()));
214 }
215 if self.openid.is_some() {
216 vec.push(format!("openid: \"{}\"", self.openid().unwrap()));
217 }
218 if self.members.is_some() {
219 let members = self.members.as_deref().unwrap();
220 vec.push(format!(
221 "members: [{}]",
222 members
223 .iter()
224 .map(|x| x.to_string())
225 .collect::<Vec<_>>()
226 .join(", ")
227 ))
228 }
229
230 let res = vec
231 .iter()
232 .map(|x| x.to_string())
233 .collect::<Vec<_>>()
234 .join(", ");
235 write!(f, "Group{{ {res} }}")
236 }
237}
238
239impl Fingerprint for Group {
240 fn fingerprint<H: Hasher>(&self, state: &mut H) {
241 if let Some(z_members) = &self.members {
243 let mut members = z_members.clone();
245 members.sort_unstable();
246 Fingerprint::fingerprint_slice(&members, state);
247 }
248 if let Some(z_mbox) = self.mbox.as_ref() {
249 z_mbox.fingerprint(state);
250 }
251 self.mbox_sha1sum.hash(state);
252 self.openid.hash(state);
253 if let Some(z_account) = self.account.as_ref() {
254 z_account.fingerprint(state);
255 }
256 }
257}
258
259impl Ord for Group {
260 fn cmp(&self, other: &Self) -> Ordering {
261 fingerprint_it(self).cmp(&fingerprint_it(other))
262 }
263}
264
265impl PartialOrd for Group {
266 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
267 Some(self.cmp(other))
268 }
269}
270
271impl Validate for Group {
272 fn validate(&self) -> Vec<ValidationError> {
273 let mut vec = vec![];
274
275 if !self.check_object_type() {
276 vec.push(ValidationError::WrongObjectType {
277 expected: ObjectType::Group,
278 found: self.object_type.to_string().into(),
279 })
280 }
281 if self.name.is_some() && self.name.as_ref().unwrap().is_empty() {
282 vec.push(ValidationError::Empty("name".into()))
283 }
284 let mut count = 0;
287 if self.mbox.is_some() {
288 count += 1;
289 }
291 if let Some(z_mbox_sha1sum) = self.mbox_sha1sum.as_ref() {
292 count += 1;
293 validate_sha1sum(z_mbox_sha1sum).unwrap_or_else(|x| vec.push(x))
294 }
295 if self.openid.is_some() {
296 count += 1;
297 }
298 if let Some(z_account) = self.account.as_ref() {
299 count += 1;
300 vec.extend(z_account.validate())
301 }
302 if self.is_anonymous() {
303 if self.members.is_none() {
305 vec.push(ValidationError::EmptyAnonymousGroup)
306 }
307 } else if count != 1 {
308 vec.push(ValidationError::ConstraintViolation(
309 "Exactly 1 IFI is required".into(),
310 ))
311 }
312 if let Some(z_members) = self.members.as_ref() {
314 z_members.iter().for_each(|x| vec.extend(x.validate()));
315 }
316
317 vec
318 }
319}
320
321impl FromStr for Group {
322 type Err = DataError;
323
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 let map = serde_json::from_str::<Map<String, Value>>(s)?;
326 Self::from_json_obj(map)
327 }
328}
329
330#[derive(Debug, Default)]
332pub struct GroupBuilder {
333 _name: Option<CIString>,
334 _members: Option<Vec<Agent>>,
335 _mbox: Option<MyEmailAddress>,
336 _sha1sum: Option<String>,
337 _openid: Option<UriString>,
338 _account: Option<Account>,
339}
340
341impl GroupBuilder {
342 pub fn name(mut self, s: &str) -> Result<Self, DataError> {
346 let s = s.trim();
347 if s.is_empty() {
348 emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
349 }
350 self._name = Some(CIString::from(s));
351 Ok(self)
352 }
353
354 pub fn member(mut self, val: Agent) -> Result<Self, DataError> {
358 val.check_validity()?;
359 if self._members.is_none() {
360 self._members = Some(vec![]);
361 }
362 self._members.as_mut().unwrap().push(val);
363 Ok(self)
364 }
365
366 pub fn mbox(mut self, s: &str) -> Result<Self, DataError> {
374 set_email!(self, s)
375 }
376
377 pub fn mbox_sha1sum(mut self, s: &str) -> Result<Self, DataError> {
385 let s = s.trim();
386 if s.is_empty() {
387 emit_error!(DataError::Validation(ValidationError::Empty(
388 "mbox_sha1sum".into()
389 )))
390 }
391
392 validate_sha1sum(s)?;
393
394 self._sha1sum = Some(s.to_owned());
395 self._mbox = None;
396 self._openid = None;
397 self._account = None;
398 Ok(self)
399 }
400
401 pub fn openid(mut self, s: &str) -> Result<Self, DataError> {
409 let s = s.trim();
410 if s.is_empty() {
411 emit_error!(DataError::Validation(ValidationError::Empty(
412 "openid".into()
413 )))
414 }
415
416 let uri = UriString::from_str(s)?;
417 self._openid = Some(uri);
418 self._mbox = None;
419 self._sha1sum = None;
420 self._account = None;
421 Ok(self)
422 }
423
424 pub fn account(mut self, val: Account) -> Result<Self, DataError> {
431 val.check_validity()?;
432 self._account = Some(val);
433 self._mbox = None;
434 self._sha1sum = None;
435 self._openid = None;
436 Ok(self)
437 }
438
439 pub fn build(mut self) -> Result<Group, DataError> {
443 if self._mbox.is_none()
444 && self._sha1sum.is_none()
445 && self._openid.is_none()
446 && self._account.is_none()
447 {
448 return Err(DataError::Validation(ValidationError::MissingIFI(
449 "Group".into(),
450 )));
451 }
452
453 if let Some(z_members) = &mut self._members {
455 z_members.sort_unstable();
456 }
457 Ok(Group {
458 object_type: ObjectType::Group,
459 name: self._name,
460 members: self._members,
461 mbox: self._mbox,
462 mbox_sha1sum: self._sha1sum,
463 openid: self._openid,
464 account: self._account,
465 })
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use tracing_test::traced_test;
473
474 #[traced_test]
475 #[test]
476 fn test_identified_group() {
477 const JSON: &str = r#"{
478 "objectType": "Group",
479 "name": "Z Group",
480 "account": {
481 "homePage": "http://inter.net/home",
482 "name": "ganon"
483 },
484 "member": [
485 { "objectType": "Agent", "name": "foo", "mbox": "mailto:foo@mail.inter.net" },
486 { "objectType": "Agent", "name": "bar", "openid": "https://inter.net/oid" }
487 ]
488 }"#;
489 let de_result = serde_json::from_str::<Group>(JSON);
490 assert!(de_result.is_ok());
491 let g = de_result.unwrap();
492
493 assert!(!g.is_anonymous());
494 }
495
496 #[traced_test]
497 #[test]
498 fn test_identified_0_agents() {
499 const JSON: &str = r#"{"objectType":"Group","name":"Z Group","account":{"homePage":"http://inter.net/home","name":"ganon"}}"#;
500
501 let de_result = serde_json::from_str::<Group>(JSON);
502 assert!(de_result.is_ok());
503 let g = de_result.unwrap();
504
505 assert!(!g.is_anonymous());
506 }
507
508 #[traced_test]
509 #[test]
510 fn test_anonymous_group() -> Result<(), DataError> {
511 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"}}"#;
512 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"}}"#;
513
514 let g1 = Group::builder()
515 .name("Z Group")?
516 .account(
517 Account::builder()
518 .home_page("http://inter.net/home")?
519 .name("ganon")?
520 .build()?,
521 )?
522 .member(
523 Agent::builder()
524 .with_object_type()
525 .name("foo")?
526 .mbox("foo@mail.inter.net")?
527 .build()?,
528 )?
529 .member(
530 Agent::builder()
531 .with_object_type()
532 .name("bar")?
533 .openid("https://inter.net/oid")?
534 .build()?,
535 )?
536 .build()?;
537 let se_result = serde_json::to_string(&g1);
538 assert!(se_result.is_ok());
539 let json = se_result.unwrap();
540 assert_eq!(json, JSON_OUT);
541
542 let de_result = serde_json::from_str::<Group>(JSON_IN_);
543 assert!(de_result.is_ok());
544 let g2 = de_result.unwrap();
545
546 assert_ne!(g1, g2);
550 assert!(g1.equivalent(&g2));
551
552 Ok(())
553 }
554
555 #[traced_test]
556 #[test]
557 fn test_long_group() {
558 const JSON: &str = r#"{
559 "name": "Team PB",
560 "mbox": "mailto:teampb@example.com",
561 "member": [
562 {
563 "name": "Andrew Downes",
564 "account": {
565 "homePage": "http://www.example.com",
566 "name": "13936749"
567 },
568 "objectType": "Agent"
569 },
570 {
571 "name": "Toby Nichols",
572 "openid": "http://toby.openid.example.org/",
573 "objectType": "Agent"
574 },
575 {
576 "name": "Ena Hills",
577 "mbox_sha1sum": "ebd31e95054c018b10727ccffd2ef2ec3a016ee9",
578 "objectType": "Agent"
579 }
580 ],
581 "objectType": "Group"
582 }"#;
583
584 let de_result = serde_json::from_str::<Group>(JSON);
585 assert!(de_result.is_ok());
586 let g = de_result.unwrap();
587
588 assert!(!g.is_anonymous());
589
590 assert!(g.name().is_some());
591 assert_eq!(g.name().unwrap(), "Team PB");
592
593 assert!(g.mbox().is_some());
594 assert_eq!(g.mbox().unwrap().to_uri(), "mailto:teampb@example.com");
595 assert!(g.mbox_sha1sum().is_none());
596 assert!(g.account().is_none());
597 assert!(g.openid().is_none());
598
599 assert_eq!(g.members().len(), 3);
600 }
601}