Skip to main content

xapi_rs/data/
group.rs

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