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