Skip to main content

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