Skip to main content

xapi_data/
context.rs

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