xapi_rs/data/
context.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    MyLanguageTag,
5    data::{
6        Actor, ActorId, ContextActivities, ContextActivitiesId, ContextAgent, ContextAgentId,
7        ContextGroup, ContextGroupId, DataError, Extensions, Fingerprint, Group, GroupId,
8        StatementRef, Validate, ValidationError,
9    },
10    emit_error,
11};
12use core::fmt;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use serde_with::skip_serializing_none;
16use std::{hash::Hasher, ops::Deref, str::FromStr};
17use tracing::error;
18use uuid::Uuid;
19
20/// Structure that gives a [Statement][1] more meaning like a team the
21/// [Actor][2] is working with, or the _altitude_ at which a scenario was
22/// attempted in a flight simulator exercise.
23///
24/// [1]: crate::Statement
25/// [2]: crate::Actor
26#[skip_serializing_none]
27#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28#[serde(deny_unknown_fields)]
29#[serde(rename_all = "camelCase")]
30pub struct Context {
31    registration: Option<Uuid>,
32    instructor: Option<Actor>,
33    team: Option<Group>,
34    context_activities: Option<ContextActivities>,
35    context_agents: Option<Vec<ContextAgent>>,
36    context_groups: Option<Vec<ContextGroup>>,
37    revision: Option<String>,
38    platform: Option<String>,
39    language: Option<MyLanguageTag>,
40    statement: Option<StatementRef>,
41    extensions: Option<Extensions>,
42}
43
44#[skip_serializing_none]
45#[derive(Debug, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub(crate) struct ContextId {
48    registration: Option<Uuid>,
49    instructor: Option<ActorId>,
50    team: Option<GroupId>,
51    context_activities: Option<ContextActivitiesId>,
52    context_agents: Option<Vec<ContextAgentId>>,
53    context_groups: Option<Vec<ContextGroupId>>,
54    revision: Option<String>,
55    platform: Option<String>,
56    language: Option<MyLanguageTag>,
57    statement: Option<StatementRef>,
58    extensions: Option<Extensions>,
59}
60
61impl From<Context> for ContextId {
62    fn from(value: Context) -> Self {
63        ContextId {
64            registration: value.registration,
65            instructor: value.instructor.map(ActorId::from),
66            team: value.team.map(GroupId::from),
67            context_activities: value.context_activities.map(ContextActivitiesId::from),
68            context_agents: {
69                if value.context_agents.is_some() {
70                    Some(
71                        value
72                            .context_agents
73                            .unwrap()
74                            .into_iter()
75                            .map(ContextAgentId::from)
76                            .collect(),
77                    )
78                } else {
79                    None
80                }
81            },
82            context_groups: {
83                if value.context_groups.is_some() {
84                    Some(
85                        value
86                            .context_groups
87                            .unwrap()
88                            .into_iter()
89                            .map(ContextGroupId::from)
90                            .collect(),
91                    )
92                } else {
93                    None
94                }
95            },
96            revision: value.revision,
97            platform: value.platform,
98            language: value.language,
99            statement: value.statement,
100            extensions: value.extensions,
101        }
102    }
103}
104
105impl From<ContextId> for Context {
106    fn from(value: ContextId) -> Self {
107        Context {
108            registration: value.registration,
109            instructor: value.instructor.map(Actor::from),
110            team: value.team.map(Group::from),
111            context_activities: value.context_activities.map(ContextActivities::from),
112            context_agents: if value.context_agents.is_none() {
113                None
114            } else {
115                Some(
116                    value
117                        .context_agents
118                        .unwrap()
119                        .into_iter()
120                        .map(ContextAgent::from)
121                        .collect(),
122                )
123            },
124            context_groups: if value.context_groups.is_none() {
125                None
126            } else {
127                Some(
128                    value
129                        .context_groups
130                        .unwrap()
131                        .into_iter()
132                        .map(ContextGroup::from)
133                        .collect(),
134                )
135            },
136            revision: value.revision,
137            platform: value.platform,
138            language: value.language,
139            statement: value.statement,
140            extensions: value.extensions,
141        }
142    }
143}
144
145impl Context {
146    /// Return a [Context] -Builder_.
147    pub fn builder() -> ContextBuilder {
148        ContextBuilder::default()
149    }
150
151    /// Return `registration` (a UUID) if set; `None` otherwise.
152    pub fn registration(&self) -> Option<&Uuid> {
153        self.registration.as_ref()
154    }
155
156    /// Return `instructor` if set; `None` otherwise.
157    pub fn instructor(&self) -> Option<&Actor> {
158        self.instructor.as_ref()
159    }
160
161    /// Return `team` if set; `None` otherwise.
162    pub fn team(&self) -> Option<&Group> {
163        self.team.as_ref()
164    }
165
166    /// Return `context_activities` if set; `None` otherwise.
167    pub fn context_activities(&self) -> Option<&ContextActivities> {
168        self.context_activities.as_ref()
169    }
170
171    /// Return `context_agents` if set; `None` otherwise.
172    pub fn context_agents(&self) -> Option<&[ContextAgent]> {
173        self.context_agents.as_deref()
174    }
175
176    /// Return `context_groups` if set; `None` otherwise.
177    pub fn context_groups(&self) -> Option<&[ContextGroup]> {
178        self.context_groups.as_deref()
179    }
180
181    /// Return `revision` if set; `None` otherwise.
182    pub fn revision(&self) -> Option<&str> {
183        self.revision.as_deref()
184    }
185
186    /// Return `platform` if set; `None` otherwise.
187    pub fn platform(&self) -> Option<&str> {
188        self.platform.as_deref()
189    }
190
191    /// Return `language` if set; `None` otherwise.
192    pub fn language(&self) -> Option<&MyLanguageTag> {
193        self.language.as_ref()
194    }
195
196    /// Return `language` as string reference if set; `None` otherwise.
197    pub fn language_as_str(&self) -> Option<&str> {
198        match &self.language {
199            Some(x) => Some(x.as_str()),
200            None => None,
201        }
202    }
203
204    /// Return `statement` if set; `None` otherwise.
205    pub fn statement(&self) -> Option<&StatementRef> {
206        self.statement.as_ref()
207    }
208
209    /// Return `extensions` if set; `None` otherwise.
210    pub fn extensions(&self) -> Option<&Extensions> {
211        self.extensions.as_ref()
212    }
213}
214
215impl Fingerprint for Context {
216    fn fingerprint<H: Hasher>(&self, state: &mut H) {
217        if self.registration.is_some() {
218            state.write(self.registration().unwrap().as_bytes());
219        }
220        if self.instructor.is_some() {
221            self.instructor().unwrap().fingerprint(state)
222        }
223        if self.team.is_some() {
224            self.team().unwrap().fingerprint(state)
225        }
226        if self.context_activities.is_some() {
227            self.context_activities().unwrap().fingerprint(state)
228        }
229        if self.context_agents.is_some() {
230            Fingerprint::fingerprint_slice(self.context_agents().unwrap(), state)
231        }
232        if self.context_groups.is_some() {
233            Fingerprint::fingerprint_slice(self.context_groups().unwrap(), state)
234        }
235        if self.revision.is_some() {
236            state.write(self.revision().unwrap().as_bytes())
237        }
238        if self.platform.is_some() {
239            state.write(self.platform().unwrap().as_bytes())
240        }
241        if self.language.is_some() {
242            state.write(self.language.as_ref().unwrap().as_str().as_bytes())
243        }
244        if self.statement.is_some() {
245            self.statement().unwrap().fingerprint(state)
246        }
247        if self.extensions.is_some() {
248            self.extensions().unwrap().fingerprint(state)
249        }
250    }
251}
252
253impl fmt::Display for Context {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        let mut vec = vec![];
256
257        if self.registration.is_some() {
258            vec.push(format!(
259                "registration: \"{}\"",
260                self.registration
261                    .as_ref()
262                    .unwrap()
263                    .hyphenated()
264                    .encode_lower(&mut Uuid::encode_buffer())
265            ))
266        }
267        if self.instructor.is_some() {
268            vec.push(format!("instructor: {}", self.instructor.as_ref().unwrap()))
269        }
270        if self.team.is_some() {
271            vec.push(format!("team: {}", self.team.as_ref().unwrap()))
272        }
273        if self.context_activities.is_some() {
274            vec.push(format!(
275                "contextActivities: {}",
276                self.context_activities.as_ref().unwrap()
277            ));
278        }
279        if self.context_agents.is_some() {
280            let items = self.context_agents.as_deref().unwrap();
281            vec.push(format!(
282                "contextAgents: [{}]",
283                items
284                    .iter()
285                    .map(|x| x.to_string())
286                    .collect::<Vec<_>>()
287                    .join(", ")
288            ));
289        }
290        if self.context_groups.is_some() {
291            let items = self.context_groups.as_deref().unwrap();
292            vec.push(format!(
293                "contextGroups: [{}]",
294                items
295                    .iter()
296                    .map(|x| x.to_string())
297                    .collect::<Vec<_>>()
298                    .join(", ")
299            ));
300        }
301        if self.revision.is_some() {
302            vec.push(format!("revision: \"{}\"", self.revision.as_ref().unwrap()))
303        }
304        if self.platform.is_some() {
305            vec.push(format!("platform: \"{}\"", self.platform.as_ref().unwrap()))
306        }
307        if self.language.is_some() {
308            vec.push(format!("language: \"{}\"", self.language.as_ref().unwrap()))
309        }
310        if self.statement.is_some() {
311            vec.push(format!("statement: {}", self.statement.as_ref().unwrap()))
312        }
313        if self.extensions.is_some() {
314            vec.push(format!("extensions: {}", self.extensions.as_ref().unwrap()))
315        }
316
317        let res = vec
318            .iter()
319            .map(|x| x.to_string())
320            .collect::<Vec<_>>()
321            .join(", ");
322        write!(f, "Context{{ {res} }}")
323    }
324}
325
326impl Validate for Context {
327    fn validate(&self) -> Vec<ValidationError> {
328        let mut vec = vec![];
329
330        if self.registration.is_some()
331            && (self.registration.as_ref().unwrap().is_nil()
332                || self.registration.as_ref().unwrap().is_max())
333        {
334            let msg = "UUID must not be all 0's or 1's";
335            error!("{}", msg);
336            vec.push(ValidationError::ConstraintViolation(msg.into()))
337        }
338        if self.instructor.is_some() {
339            vec.extend(self.instructor.as_ref().unwrap().validate())
340        }
341        if self.team.is_some() {
342            vec.extend(self.team.as_ref().unwrap().validate());
343        }
344        if self.context_activities.is_some() {
345            vec.extend(self.context_activities.as_ref().unwrap().validate());
346        }
347        if self.context_agents.is_some() {
348            for ca in self.context_agents.as_ref().unwrap().iter() {
349                vec.extend(ca.validate())
350            }
351        }
352        if self.context_groups.is_some() {
353            for cg in self.context_groups.as_ref().unwrap().iter() {
354                vec.extend(cg.validate())
355            }
356        }
357        if self.revision.is_some() && self.revision.as_ref().unwrap().is_empty() {
358            vec.push(ValidationError::Empty("revision".into()))
359        }
360        if self.platform.is_some() && self.platform.as_ref().unwrap().is_empty() {
361            vec.push(ValidationError::Empty("platform".into()))
362        }
363        if self.statement.is_some() {
364            vec.extend(self.statement.as_ref().unwrap().validate())
365        }
366
367        vec
368    }
369}
370
371/// A Type that knows how to construct a [Context].
372#[derive(Debug, Default)]
373pub struct ContextBuilder {
374    _registration: Option<Uuid>,
375    _instructor: Option<Actor>,
376    _team: Option<Group>,
377    _context_activities: Option<ContextActivities>,
378    _context_agents: Option<Vec<ContextAgent>>,
379    _context_groups: Option<Vec<ContextGroup>>,
380    _revision: Option<String>,
381    _platform: Option<String>,
382    _language: Option<MyLanguageTag>,
383    _statement: Option<StatementRef>,
384    _extensions: Option<Extensions>,
385}
386
387impl ContextBuilder {
388    /// Set the `registration` field from an `&str`.
389    ///
390    /// Raise [DataError] if the input string is empty.
391    pub fn registration(mut self, val: &str) -> Result<Self, DataError> {
392        let val = val.trim();
393        if val.is_empty() {
394            emit_error!(DataError::Validation(ValidationError::Empty(
395                "registration".into()
396            )))
397        } else {
398            let uuid = Uuid::parse_str(val)?;
399            if uuid.is_nil() || uuid.is_max() {
400                emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
401                    "UUID should not be all zeroes or ones".into()
402                )))
403            } else {
404                self._registration = Some(uuid);
405                Ok(self)
406            }
407        }
408    }
409
410    /// Set the `registration` field from a UUID value.
411    ///
412    /// Raise [DataError] if the input string is empty.
413    pub fn registration_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
414        if uuid.is_nil() || uuid.is_max() {
415            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
416                "UUID should not be all zeroes or ones".into()
417            )))
418        } else {
419            self._registration = Some(uuid);
420            Ok(self)
421        }
422    }
423
424    /// Set the `instructor` field.
425    ///
426    /// Raise [DataError] if the [Actor] argument is invalid.
427    pub fn instructor(mut self, val: Actor) -> Result<Self, DataError> {
428        val.check_validity()?;
429        self._instructor = Some(val);
430        Ok(self)
431    }
432
433    /// Set the `team` field.
434    ///
435    /// Raise [DataError] if the [Group] argument is invalid.
436    pub fn team(mut self, val: Group) -> Result<Self, DataError> {
437        val.check_validity()?;
438        self._team = Some(val);
439        Ok(self)
440    }
441
442    /// Set the `context_activities` field.
443    ///
444    /// Raise [DataError] if the [ContextActivities] argument is invalid.
445    pub fn context_activities(mut self, val: ContextActivities) -> Result<Self, DataError> {
446        val.check_validity()?;
447        self._context_activities = Some(val);
448        Ok(self)
449    }
450
451    /// Add a [ContextAgent] to `context_agents` field.
452    ///
453    /// Raise [DataError] if the [ContextAgent] argument is invalid.
454    pub fn context_agent(mut self, val: ContextAgent) -> Result<Self, DataError> {
455        val.check_validity()?;
456        if self._context_agents.is_none() {
457            self._context_agents = Some(vec![])
458        }
459        self._context_agents.as_mut().unwrap().push(val);
460        Ok(self)
461    }
462
463    /// Add a [ContextGroup] to `context_groups` field.
464    ///
465    /// Raise [DataError] if the [ContextGroup] argument is invalid.
466    pub fn context_group(mut self, val: ContextGroup) -> Result<Self, DataError> {
467        val.check_validity()?;
468        if self._context_groups.is_none() {
469            self._context_groups = Some(vec![])
470        }
471        self._context_groups.as_mut().unwrap().push(val);
472        Ok(self)
473    }
474
475    /// Set the `revision` field.
476    ///
477    /// Raise [DataError] if the input string is empty.
478    pub fn revision<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
479        let val = val.trim();
480        if val.is_empty() {
481            emit_error!(DataError::Validation(ValidationError::Empty(
482                "revision".into()
483            )))
484        } else {
485            self._revision = Some(val.to_owned());
486            Ok(self)
487        }
488    }
489
490    /// Set the `platform` field.
491    ///
492    /// Raise [DataError] if the input string is empty.
493    pub fn platform<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
494        let val = val.trim();
495        if val.is_empty() {
496            emit_error!(DataError::Validation(ValidationError::Empty(
497                "platform".into()
498            )))
499        } else {
500            self._platform = Some(val.to_owned());
501            Ok(self)
502        }
503    }
504
505    /// Set the `language` field.
506    ///
507    /// Raise [DataError] if the input string is empty.
508    pub fn language<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
509        let val = val.trim();
510        if val.is_empty() {
511            emit_error!(DataError::Validation(ValidationError::Empty(
512                "language".into()
513            )))
514        } else {
515            self._language = Some(MyLanguageTag::from_str(val)?);
516
517            Ok(self)
518        }
519    }
520
521    /// Set the `statement` field from given [StatementRef] instance.
522    ///
523    /// Raise [DataError] if the argument is invalid.
524    pub fn statement(mut self, val: StatementRef) -> Result<Self, DataError> {
525        val.check_validity()?;
526        self._statement = Some(val);
527        Ok(self)
528    }
529
530    /// Set the `statement` field from a Statement's UUID.
531    ///
532    /// Raise [DataError] if the argument is invalid.
533    pub fn statement_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
534        let val = StatementRef::builder().id_as_uuid(uuid)?.build()?;
535        self._statement = Some(val);
536        Ok(self)
537    }
538
539    /// Add to `extensions` an entry w/ (`key`, `value`) pair.
540    ///
541    /// Raise [DataError] if the `key` is empty.
542    pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
543        if self._extensions.is_none() {
544            self._extensions = Some(Extensions::new());
545        }
546        let _ = self._extensions.as_mut().unwrap().add(key, value);
547        Ok(self)
548    }
549
550    /// Set (as in replace) the `extensions` property of this instance  w/ the
551    /// given argument.
552    pub fn with_extensions(mut self, map: Extensions) -> Result<Self, DataError> {
553        self._extensions = Some(map);
554        Ok(self)
555    }
556
557    /// Create a [Context] from set field values.
558    pub fn build(self) -> Result<Context, DataError> {
559        if self._registration.is_none()
560            && self._instructor.is_none()
561            && self._team.is_none()
562            && self._context_activities.is_none()
563            && self._context_agents.is_none()
564            && self._context_groups.is_none()
565            && self._revision.is_none()
566            && self._platform.is_none()
567            && self._language.is_none()
568            && self._statement.is_none()
569            && self._extensions.is_none()
570        {
571            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
572                "At least one of the fields must not be empty".into()
573            )))
574        } else {
575            Ok(Context {
576                registration: self._registration,
577                instructor: self._instructor,
578                team: self._team,
579                context_activities: self._context_activities,
580                context_agents: self._context_agents,
581                context_groups: self._context_groups,
582                revision: self._revision,
583                platform: self._platform,
584                language: self._language,
585                statement: self._statement,
586                extensions: self._extensions,
587            })
588        }
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use tracing_test::traced_test;
596
597    #[traced_test]
598    #[test]
599    fn test_simple() {
600        const JSON: &str = r#"{
601            "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7",
602            "contextActivities": {
603                "parent": [
604                    {
605                        "id": "http://www.example.com/meetings/series/267",
606                        "objectType": "Activity"
607                    }
608                ],
609                "category": [
610                    {
611                        "id": "http://www.example.com/meetings/categories/teammeeting",
612                        "objectType": "Activity",
613                        "definition": {
614                            "name": {
615                                "en": "team meeting"
616                            },
617                            "description": {
618                                "en": "A category of meeting used for regular team meetings."
619                            },
620                            "type": "http://example.com/expapi/activities/meetingcategory"
621                        }
622                    }
623                ],
624                "other": [
625                    {
626                        "id": "http://www.example.com/meetings/occurances/34257",
627                        "objectType": "Activity"
628                    },
629                    {
630                        "id": "http://www.example.com/meetings/occurances/3425567",
631                        "objectType": "Activity"
632                    }
633                ]
634            },
635            "instructor": {
636                "name": "Andrew Downes",
637                "account": {
638                    "homePage": "http://www.example.com",
639                    "name": "13936749"
640                },
641                "objectType": "Agent"
642            },
643            "team": {
644                "name": "Team PB",
645                "mbox": "mailto:teampb@example.com",
646                "objectType": "Group"
647            },
648            "platform": "Example virtual meeting software",
649            "language": "tlh",
650            "statement": {
651                "objectType": "StatementRef",
652                "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee"
653            }
654        }"#;
655        let de_result = serde_json::from_str::<Context>(JSON);
656        assert!(de_result.is_ok());
657        let _ctx = de_result.unwrap();
658    }
659}