Skip to main content

xapi_data/
group.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use 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/// Structure that represents a group of [Agent][1]s.
20///
21/// A [Group] can be **identified**, otherwise is considered to be
22/// **anonymous**.
23///
24/// [1]: crate::Agent
25#[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    /// Construct and validate a [Group] from a JSON Object.
103    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        // finally convert it to a group...
114        let group: Group = serde_json::from_value(Value::Object(map))?;
115        group.check_validity()?;
116        Ok(group)
117    }
118
119    /// Return a [Group] _Builder_.
120    pub fn builder() -> GroupBuilder {
121        GroupBuilder::default()
122    }
123
124    /// Return TRUE if the `objectType` property is [Group][1]; FALSE otherwise.
125    ///
126    /// [1]: ObjectType#variant.Group
127    pub fn check_object_type(&self) -> bool {
128        self.object_type == ObjectType::Group
129    }
130
131    /// Return TRUE if this Group is _anonymous_; FALSE otherwise.
132    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    /// Return `name` field if set; `None` otherwise.
140    pub fn name(&self) -> Option<&CIString> {
141        self.name.as_ref()
142    }
143
144    /// Return `name` field as a string reference if set; `None` otherwise.
145    pub fn name_as_str(&self) -> Option<&str> {
146        self.name.as_deref()
147    }
148
149    /// Return the unordered `members` list if it's set or `None` otherwise.
150    ///
151    /// When set, it's a vector of at least one [Agent]). This is expected to
152    /// be the case when the Group is _anonymous_.
153    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    /// Return `mbox` field if set; `None` otherwise.
162    pub fn mbox(&self) -> Option<&MyEmailAddress> {
163        self.mbox.as_ref()
164    }
165
166    /// Return `mbox_sha1sum` field (hex-encoded SHA1 hash of this entity's
167    /// `mbox` URI) if set; `None` otherwise.
168    pub fn mbox_sha1sum(&self) -> Option<&str> {
169        self.mbox_sha1sum.as_deref()
170    }
171
172    /// Return `openid` field (openID URI of this entity) if set; `None` otherwise.
173    pub fn openid(&self) -> Option<&UriStr> {
174        self.openid.as_deref()
175    }
176
177    /// Return `account` field (reference to this entity's [Account]) if set;
178    /// `None` otherwise.
179    pub fn account(&self) -> Option<&Account> {
180        self.account.as_ref()
181    }
182
183    /// Return the fingerprint of this instance.
184    pub fn uid(&self) -> u64 {
185        fingerprint_it(self)
186    }
187
188    /// Return TRUE if this is _Equivalent_ to `that` and FALSE otherwise.
189    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        // discard `object_type` and `name`
240        if let Some(z_members) = &self.members {
241            // ensure Agents are sorted...
242            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        // the xAPI specifications mandate that "Exactly One of mbox, openid,
283        // mbox_sha1sum, account is required".
284        let mut count = 0;
285        if self.mbox.is_some() {
286            count += 1;
287            // no need to validate email address...
288        }
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            // must contain at least 1 member...
302            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        // anonymous or identified, validate all members...
311        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/// A Type that knows how to construct a [Group].
329#[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    /// Set the `name` field.
341    ///
342    /// Raise [DataError] if the string is empty.
343    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    /// Add an [Agent] to this [Group].
353    ///
354    /// Raise [DataError] if the [Agent] is invalid.
355    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    /// Set the `mbox` field prefixing w/ `mailto:` if scheme's missing.
365    ///
366    /// Built instance will have all of its other _Inverse Functional Identifier_
367    /// fields \[re\]set to `None`.
368    ///
369    /// Raise an [DataError] if the string is empty, a scheme was present but
370    /// wasn't `mailto`, or parsing the string as an IRI fails.
371    pub fn mbox(mut self, s: &str) -> Result<Self, DataError> {
372        set_email!(self, s)
373    }
374
375    /// Set the `mbox_sha1sum` field.
376    ///
377    /// Built instance will have all of its other _Inverse Functional Identifier_
378    /// fields \[re\]set to `None`.
379    ///
380    /// Raise a [DataError] if the string is empty, is not 40 characters long,
381    /// or contains non hexadecimal characters.
382    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    /// Set the `openid` field.
400    ///
401    /// Built instance will have all of its other _Inverse Functional Identifier_
402    /// fields \[re\]set to `None`.
403    ///
404    /// Raise a [DataError] if the string is empty, or fails parsing as a
405    /// valid URI.
406    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    /// Set the `account` field.
423    ///
424    /// Built instance will have all of its other _Inverse Functional Identifier_
425    /// fields \[re\]set to `None`.
426    ///
427    /// Raise [DataError] if the [Account] is invalid.
428    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    /// Create a [Group] instance.
438    ///
439    /// Raise [DataError] if no Inverse Functional Identifier field was set.
440    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        // NOTE (rsn) 20240705 - sort Agents...
452        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        // NOTE (rsn) 20240605 - unpredictable Agent members order in a Group
545        // may cause an equality test to fail.  however if two Groups have
546        // equivalent data their fingerprints should match...
547        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}