Skip to main content

xapi_data/
agent.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    Account, CIString, DataError, Fingerprint, MyEmailAddress, ObjectType, Validate,
5    ValidationError, check_for_nulls, emit_error, fingerprint_it, set_email, validate_sha1sum,
6};
7use core::fmt;
8use iri_string::types::{UriStr, UriString};
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value};
11use serde_with::skip_serializing_none;
12use std::{
13    cmp::Ordering,
14    hash::{Hash, Hasher},
15    str::FromStr,
16};
17
18/// Structure that provides combined information about an individual derived
19/// from an outside service, such as a _Directory Service_.
20#[skip_serializing_none]
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct Agent {
24    #[serde(rename = "objectType")]
25    object_type: Option<ObjectType>,
26    name: Option<CIString>,
27    mbox: Option<MyEmailAddress>,
28    mbox_sha1sum: Option<String>,
29    openid: Option<UriString>,
30    account: Option<Account>,
31}
32
33#[skip_serializing_none]
34#[derive(Debug, Serialize)]
35pub(crate) struct AgentId {
36    mbox: Option<MyEmailAddress>,
37    mbox_sha1sum: Option<String>,
38    openid: Option<UriString>,
39    account: Option<Account>,
40}
41
42impl From<Agent> for AgentId {
43    fn from(value: Agent) -> Self {
44        AgentId {
45            mbox: value.mbox,
46            mbox_sha1sum: value.mbox_sha1sum,
47            openid: value.openid,
48            account: value.account,
49        }
50    }
51}
52
53impl From<AgentId> for Agent {
54    fn from(value: AgentId) -> Self {
55        Agent {
56            object_type: None,
57            name: None,
58            mbox: value.mbox,
59            mbox_sha1sum: value.mbox_sha1sum,
60            openid: value.openid,
61            account: value.account,
62        }
63    }
64}
65
66impl Agent {
67    /// Construct and validate an [Agent] from a JSON Object.
68    pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
69        for (k, v) in &map {
70            if v.is_null() {
71                emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
72                    format!("Key '{k}' is null").into()
73                )))
74            } else {
75                check_for_nulls(v)?
76            }
77        }
78        // finally convert it to an agent...
79        let agent: Agent = serde_json::from_value(Value::Object(map))?;
80        agent.check_validity()?;
81        Ok(agent)
82    }
83
84    /// Return an [Agent] _Builder_.
85    pub fn builder() -> AgentBuilder {
86        AgentBuilder::default()
87    }
88
89    /// Return TRUE if the `objectType` property is as expected; FALSE otherwise.
90    pub fn check_object_type(&self) -> bool {
91        if let Some(z_object_type) = self.object_type.as_ref() {
92            z_object_type == &ObjectType::Agent
93        } else {
94            true
95        }
96    }
97
98    /// Return `name` if set; `None` otherwise.
99    pub fn name(&self) -> Option<&CIString> {
100        self.name.as_ref()
101    }
102
103    /// Return `name` as a string reference if set; `None` otherwise.
104    pub fn name_as_str(&self) -> Option<&str> {
105        self.name.as_deref()
106    }
107
108    /// Return `mbox` as an [MyEmailAddress] if set; `None` otherwise.
109    pub fn mbox(&self) -> Option<&MyEmailAddress> {
110        self.mbox.as_ref()
111    }
112
113    /// Return `mbox_sha1sum` field (hex-encoded SHA1 hash of this entity's
114    /// `mbox` URI) if set; `None` otherwise.
115    pub fn mbox_sha1sum(&self) -> Option<&str> {
116        self.mbox_sha1sum.as_deref()
117    }
118
119    /// Return `openid` field (openID URI of this entity) if set; `None` otherwise.
120    pub fn openid(&self) -> Option<&UriStr> {
121        self.openid.as_deref()
122    }
123
124    /// Return `account` field (reference to this entity's [Account]) if set;
125    /// `None` otherwise.
126    pub fn account(&self) -> Option<&Account> {
127        self.account.as_ref()
128    }
129
130    /// Return the fingerprint of this instance.
131    pub fn uid(&self) -> u64 {
132        fingerprint_it(self)
133    }
134
135    /// Return TRUE if this is _Equivalent_ to `that`; FALSE otherwise.
136    pub fn equivalent(&self, that: &Agent) -> bool {
137        self.uid() == that.uid()
138    }
139}
140
141impl Ord for Agent {
142    fn cmp(&self, other: &Self) -> Ordering {
143        fingerprint_it(self).cmp(&fingerprint_it(other))
144    }
145}
146
147impl PartialOrd for Agent {
148    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
149        Some(self.cmp(other))
150    }
151}
152
153impl FromStr for Agent {
154    type Err = DataError;
155
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        let map: Map<String, Value> = serde_json::from_str(s)?;
158        Self::from_json_obj(map)
159    }
160}
161
162impl fmt::Display for Agent {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        let mut vec = vec![];
165        if self.name.is_some() {
166            vec.push(format!("name: \"{}\"", self.name().unwrap()));
167        }
168        if self.mbox.is_some() {
169            vec.push(format!("mbox: \"{}\"", self.mbox().unwrap()));
170        }
171        if self.mbox_sha1sum.is_some() {
172            vec.push(format!(
173                "mbox_sha1sum: \"{}\"",
174                self.mbox_sha1sum().unwrap()
175            ));
176        }
177        if self.account.is_some() {
178            vec.push(format!("account: {}", self.account().unwrap()));
179        }
180        if self.openid.is_some() {
181            vec.push(format!("openid: \"{}\"", self.openid().unwrap()));
182        }
183        let res = vec
184            .iter()
185            .map(|x| x.to_string())
186            .collect::<Vec<_>>()
187            .join(", ");
188        write!(f, "Agent{{ {res} }}")
189    }
190}
191
192impl Fingerprint for Agent {
193    fn fingerprint<H: Hasher>(&self, state: &mut H) {
194        // always discard `object_type` and `name`
195        if let Some(z_mbox) = self.mbox.as_ref() {
196            z_mbox.fingerprint(state);
197        }
198        self.mbox_sha1sum.hash(state);
199        self.openid.hash(state);
200        if let Some(z_account) = self.account.as_ref() {
201            z_account.fingerprint(state);
202        }
203    }
204}
205
206impl Validate for Agent {
207    fn validate(&self) -> Vec<ValidationError> {
208        let mut vec = vec![];
209
210        if let Some(z_object_type) = self.object_type.as_ref()
211            && z_object_type != &ObjectType::Agent
212        {
213            vec.push(ValidationError::WrongObjectType {
214                expected: ObjectType::Agent,
215                found: z_object_type.to_string().into(),
216            })
217        }
218        if self.name.is_some() && self.name.as_ref().unwrap().is_empty() {
219            vec.push(ValidationError::Empty("name".into()))
220        }
221        // xAPI mandates that "Exactly One of mbox, openid, mbox_sha1sum,
222        // account is required".
223        let mut count = 0;
224        if self.mbox.is_some() {
225            count += 1;
226            // no need to validate email address...
227        }
228        if let Some(z_mbox_sha1sum) = self.mbox_sha1sum.as_ref() {
229            count += 1;
230            validate_sha1sum(z_mbox_sha1sum).unwrap_or_else(|x| vec.push(x))
231        }
232        if self.openid.is_some() {
233            count += 1;
234        }
235        if let Some(z_account) = self.account.as_ref() {
236            count += 1;
237            vec.extend(z_account.validate())
238        }
239        if count != 1 {
240            vec.push(ValidationError::ConstraintViolation(
241                "Exactly 1 IFI is required".into(),
242            ))
243        }
244
245        vec
246    }
247}
248
249/// A Type that knows how to construct an [Agent].
250#[derive(Debug, Default)]
251pub struct AgentBuilder {
252    _object_type: Option<ObjectType>,
253    _name: Option<CIString>,
254    _mbox: Option<MyEmailAddress>,
255    _sha1sum: Option<String>,
256    _openid: Option<UriString>,
257    _account: Option<Account>,
258}
259
260impl AgentBuilder {
261    /// Set `objectType` property.
262    pub fn with_object_type(mut self) -> Self {
263        self._object_type = Some(ObjectType::Agent);
264        self
265    }
266
267    /// Set the `name` field.
268    ///
269    /// Raise [DataError] if the string is empty.
270    pub fn name(mut self, s: &str) -> Result<Self, DataError> {
271        let s = s.trim();
272        if s.is_empty() {
273            emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
274        }
275        self._name = Some(CIString::from(s));
276        Ok(self)
277    }
278
279    /// Set the `mbox` field prefixing w/ `mailto:` if scheme's missing.
280    ///
281    /// Built instance will have all of its other _Inverse Functional Identifier_
282    /// fields \[re\]set to `None`.
283    ///
284    /// Raise an [DataError] if the string is empty, a scheme was present but
285    /// wasn't `mailto`, or parsing the string as an IRI fails.
286    pub fn mbox(mut self, s: &str) -> Result<Self, DataError> {
287        set_email!(self, s)
288    }
289
290    /// Set the `mbox_sha1sum` field.
291    ///
292    /// Built instance will have all of its other _Inverse Functional Identifier_
293    /// fields \[re\]set to `None`.
294    ///
295    /// Raise a [DataError] if the string is empty, is not 40 characters long,
296    /// or contains non hexadecimal characters.
297    pub fn mbox_sha1sum(mut self, s: &str) -> Result<Self, DataError> {
298        let s = s.trim();
299        if s.is_empty() {
300            emit_error!(DataError::Validation(ValidationError::Empty(
301                "mbox_sha1sum".into()
302            )))
303        }
304
305        validate_sha1sum(s)?;
306        self._sha1sum = Some(s.to_owned());
307        self._mbox = None;
308        self._openid = None;
309        self._account = None;
310        Ok(self)
311    }
312
313    /// Set the `openid` field.
314    ///
315    /// Built instance will have all of its other _Inverse Functional Identifier_
316    /// fields \[re\]set to `None`.
317    ///
318    /// Raise a [DataError] if the string is empty, or fails parsing as a
319    /// valid URI.
320    pub fn openid(mut self, s: &str) -> Result<Self, DataError> {
321        let s = s.trim();
322        if s.is_empty() {
323            emit_error!(DataError::Validation(ValidationError::Empty(
324                "openid".into()
325            )))
326        }
327
328        let uri = UriString::from_str(s)?;
329        self._openid = Some(uri);
330        self._mbox = None;
331        self._sha1sum = None;
332        self._account = None;
333        Ok(self)
334    }
335
336    /// Set the `account` field.
337    ///
338    /// Built instance will have all of its other _Inverse Functional Identifier_
339    /// fields \[re\]set to `None`.
340    ///
341    /// Raise [DataError] if the [Account] is invalid.
342    pub fn account(mut self, val: Account) -> Result<Self, DataError> {
343        val.check_validity()?;
344        self._account = Some(val);
345        self._mbox = None;
346        self._sha1sum = None;
347        self._openid = None;
348        Ok(self)
349    }
350
351    /// Create an [Agent] instance.
352    ///
353    /// Raise [DataError] if no Inverse Functional Identifier field was set.
354    pub fn build(self) -> Result<Agent, DataError> {
355        if self._mbox.is_none()
356            && self._sha1sum.is_none()
357            && self._openid.is_none()
358            && self._account.is_none()
359        {
360            emit_error!(DataError::Validation(ValidationError::MissingIFI(
361                "Agent".into(),
362            )));
363        }
364
365        Ok(Agent {
366            object_type: self._object_type,
367            name: self._name,
368            mbox: self._mbox,
369            mbox_sha1sum: self._sha1sum,
370            openid: self._openid,
371            account: self._account,
372        })
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use tracing_test::traced_test;
380
381    #[test]
382    fn test_serde() -> Result<(), DataError> {
383        const JSON: &str =
384            r#"{"objectType":"Agent","name":"Z User","mbox":"mailto:zuser@inter.net"}"#;
385
386        let a1 = Agent::builder()
387            .with_object_type()
388            .name("Z User")?
389            .mbox("zuser@inter.net")?
390            .build()?;
391        let se_result = serde_json::to_string(&a1);
392        assert!(se_result.is_ok());
393        let json = se_result.unwrap();
394        assert_eq!(json, JSON);
395
396        let de_result = serde_json::from_str::<Agent>(JSON);
397        assert!(de_result.is_ok());
398        let a2 = de_result.unwrap();
399
400        assert_eq!(a1, a2);
401
402        Ok(())
403    }
404
405    #[test]
406    fn test_camel_and_snake() {
407        const JSON: &str = r#"{
408            "objectType": "Agent",
409            "name": "Ena Hills",
410            "mbox": "mailto:ena.hills@example.com",
411            "mbox_sha1sum": "ebd31e95054c018b10727ccffd2ef2ec3a016ee9",
412            "account": {
413                "homePage": "http://www.example.com",
414                "name": "13936749"
415            },
416            "openid": "http://toby.openid.example.org/"
417        }"#;
418        let de_result = serde_json::from_str::<Agent>(JSON);
419        assert!(de_result.is_ok());
420        let a = de_result.unwrap();
421
422        assert!(a.check_object_type());
423        assert!(a.name().is_some());
424        assert_eq!(a.name().unwrap(), &CIString::from("ena hills"));
425        assert_eq!(a.name_as_str().unwrap(), "Ena Hills");
426        assert!(a.mbox().is_some());
427        assert_eq!(a.mbox().unwrap().to_uri(), "mailto:ena.hills@example.com");
428        assert!(a.mbox_sha1sum().is_some());
429        assert_eq!(
430            a.mbox_sha1sum().unwrap(),
431            "ebd31e95054c018b10727ccffd2ef2ec3a016ee9"
432        );
433        assert!(a.account().is_some());
434        let act = a.account().unwrap();
435        assert_eq!(act.home_page_as_str(), "http://www.example.com");
436        assert_eq!(act.name(), "13936749");
437        assert!(a.openid().is_some());
438        assert_eq!(
439            a.openid().unwrap().to_string(),
440            "http://toby.openid.example.org/"
441        );
442    }
443
444    #[traced_test]
445    #[test]
446    fn test_validate() {
447        const JSON1: &str =
448            r#"{"objectType":"Agent","name":"Z User","openid":"http://résumé.net/zuser"}"#;
449
450        let de_result = serde_json::from_str::<Agent>(JSON1);
451        // should fail b/c of invalid OpenID URI...
452        assert!(de_result.as_ref().is_err_and(|x| x.is_data()));
453        let de_err = de_result.err().unwrap();
454        let (line, col) = (de_err.line(), de_err.column());
455        assert_eq!(line, 1);
456        assert_eq!(col, 74);
457
458        const JSON2: &str =
459            r#"{"objectType":"Activity","name":"Z User","openid":"http://inter.net/zuser"}"#;
460
461        let de_result = serde_json::from_str::<Agent>(JSON2);
462        // will succeed but is invalid --wrong object_type...
463        assert!(de_result.is_ok());
464        let agent = de_result.unwrap();
465        let errors = agent.validate();
466        assert!(!errors.is_empty());
467        assert_eq!(errors.len(), 1);
468        assert!(matches!(
469            &errors[0],
470            ValidationError::WrongObjectType { .. }
471        ));
472
473        const JSON3: &str = r#"{"name":"Rick James","objectType":"Agent"}"#;
474
475        let de_result = serde_json::from_str::<Agent>(JSON3);
476        // will succeed but is invalid --no IFIs...
477        assert!(de_result.is_ok());
478        let agent = de_result.unwrap();
479        let errors = agent.validate();
480        assert!(!errors.is_empty());
481        assert_eq!(errors.len(), 1);
482        assert!(matches!(
483            &errors[0],
484            ValidationError::ConstraintViolation { .. }
485        ))
486    }
487
488    #[ignore = "Partially Implemented"]
489    #[traced_test]
490    #[test]
491    fn test_null_optional_fields() {
492        const E1: &str = r#"{"objectType":"Agent","name":null}"#;
493        const E2: &str = r#"{"objectType":"Agent","mbox":null}"#;
494        const E3: &str = r#"{"objectType":"Agent","openid":null}"#;
495        const E4: &str = r#"{"objectType":"Agent","account":null}"#;
496
497        const OK1: &str = r#"{"objectType":"Agent","mbox":"foo@bar.org"}"#;
498
499        assert!(serde_json::from_str::<Agent>(E1).is_err());
500        assert!(serde_json::from_str::<Agent>(E2).is_err());
501        assert!(serde_json::from_str::<Agent>(E3).is_err());
502        assert!(serde_json::from_str::<Agent>(E4).is_err());
503
504        assert!(serde_json::from_str::<Agent>(OK1).is_ok());
505    }
506}