Skip to main content

xapi_data/
activity_definition.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    Canonical, DataError, Extensions, InteractionComponent, InteractionType, LanguageMap,
5    MyLanguageTag, Validate, ValidationError, add_language, emit_error, merge_maps,
6    validate::validate_irl,
7};
8use core::fmt;
9use iri_string::types::{IriStr, IriString};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use serde_with::skip_serializing_none;
13
14/// Structure that provides additional information (metadata) related to an
15/// [Activity][crate::Activity].
16#[skip_serializing_none]
17#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ActivityDefinition {
20    name: Option<LanguageMap>,
21    description: Option<LanguageMap>,
22    #[serde(rename = "type")]
23    type_: Option<IriString>,
24    more_info: Option<IriString>,
25    // IMPORTANT (20240925) - 'interactionType' property must be present if any
26    // of the 'correctResponsesPattern', 'choices', 'scale', 'source', 'target',
27    // or 'steps' are.
28    interaction_type: Option<InteractionType>,
29    correct_responses_pattern: Option<Vec<String>>,
30    choices: Option<Vec<InteractionComponent>>,
31    scale: Option<Vec<InteractionComponent>>,
32    source: Option<Vec<InteractionComponent>>,
33    target: Option<Vec<InteractionComponent>>,
34    steps: Option<Vec<InteractionComponent>>,
35    extensions: Option<Extensions>,
36}
37
38impl ActivityDefinition {
39    /// Return an [ActivityDefinition] _Builder_.
40    pub fn builder() -> ActivityDefinitionBuilder<'static> {
41        ActivityDefinitionBuilder::default()
42    }
43
44    /// Return the `name` for the given language `tag` if it exists; `None`
45    /// otherwise.
46    pub fn name(&self, tag: &MyLanguageTag) -> Option<&str> {
47        match &self.name {
48            Some(lm) => lm.get(tag),
49            None => None,
50        }
51    }
52
53    /// Return the `description` for the given language `tag` if it exists;
54    /// `None` otherwise.
55    pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
56        match &self.description {
57            Some(lm) => lm.get(tag),
58            None => None,
59        }
60    }
61
62    /// Return the `type_` field if set; `None` otherwise.
63    pub fn type_(&self) -> Option<&IriStr> {
64        self.type_.as_deref()
65    }
66
67    /// Return the `more_info` field if set; `None` otherwise.
68    ///
69    /// When set, it's an IRL that points to information about the associated
70    /// [Activity][crate::Activity] possibly incl. a way to launch it.
71    pub fn more_info(&self) -> Option<&IriStr> {
72        self.more_info.as_deref()
73    }
74
75    /// Return the `interaction_type` field if set; `None` otherwise.
76    ///
77    /// Possible values are: [`true-false`][InteractionType#variant.TrueFalse],
78    /// [`choice`][InteractionType#variant.Choice],
79    /// [`fill-in`][InteractionType#variant.FillIn],
80    /// [`long-fill-in`][InteractionType#variant.LongFillIn],
81    /// [`matching`][InteractionType#variant.Matching],
82    /// [`performance`][InteractionType#variant.Performance],
83    /// [`sequencing`][InteractionType#variant.Sequencing],
84    /// [`likert`][InteractionType#variant.Likert],
85    /// [`numeric`][InteractionType#variant.Numeric], and
86    /// [`other`][InteractionType#variant.Other],
87    pub fn interaction_type(&self) -> Option<&InteractionType> {
88        self.interaction_type.as_ref()
89    }
90
91    /// Return the `correct_responses_pattern` field if set; `None` otherwise.
92    ///
93    /// When set, it's a Vector of patterns representing the correct response
94    /// to the interaction.
95    ///
96    /// The structure of the patterns vary depending on the `interaction_type`.
97    pub fn correct_responses_pattern(&self) -> Option<&Vec<String>> {
98        self.correct_responses_pattern.as_ref()
99    }
100
101    /// Return the `choices` field if set; `None` otherwise.
102    ///
103    /// When set, it's a vector of of [InteractionComponent]s representing the
104    /// correct response to the interaction.
105    ///
106    /// The contents of item(s) in the vector are specific to the given
107    /// `interaction_type`.
108    pub fn choices(&self) -> Option<&Vec<InteractionComponent>> {
109        self.choices.as_ref()
110    }
111
112    /// Return the `scale` field if set; `None` otherwise.
113    ///
114    /// When set, it's a vector of of [InteractionComponent]s representing the
115    /// correct response to the interaction.
116    ///
117    /// The contents of item(s) in the vector are specific to the given
118    /// `interaction_type`.
119    pub fn scale(&self) -> Option<&Vec<InteractionComponent>> {
120        self.scale.as_ref()
121    }
122
123    /// Return the `source` field if set; `None` otherwise.
124    ///
125    /// When set, it's a vector of of [InteractionComponent]s representing the
126    /// correct response to the interaction.
127    ///
128    /// The contents of item(s) in the vector are specific to the given
129    /// `interaction_type`.
130    pub fn source(&self) -> Option<&Vec<InteractionComponent>> {
131        self.source.as_ref()
132    }
133
134    /// Return the `target` field if set; `None` otherwise.
135    ///
136    /// When set, it's a vector of of [InteractionComponent]s representing the
137    /// correct response to the interaction.
138    ///
139    /// The contents of item(s) in the vector are specific to the given
140    /// `interaction_type`.
141    pub fn target(&self) -> Option<&Vec<InteractionComponent>> {
142        self.target.as_ref()
143    }
144
145    /// Return the `steps` field if set; `None` otherwise.
146    ///
147    /// When set, it's a vector of of [InteractionComponent]s representing the
148    /// correct response to the interaction.
149    ///
150    /// The contents of item(s) in the vector are specific to the given
151    /// `interaction_type`.
152    pub fn steps(&self) -> Option<&Vec<InteractionComponent>> {
153        self.steps.as_ref()
154    }
155
156    /// Return the [`extensions`][Extensions] field if set; `None` otherwise.
157    pub fn extensions(&self) -> Option<&Extensions> {
158        self.extensions.as_ref()
159    }
160
161    /// Return the _extension_ keyed by `key` if it exists; `None` otherwise.
162    pub fn extension(&self, key: &IriStr) -> Option<&Value> {
163        if let Some(z_extensions) = self.extensions.as_ref() {
164            z_extensions.get(key)
165        } else {
166            None
167        }
168    }
169
170    /// Consume `that` merging it into this instance.
171    pub fn merge(&mut self, that: Self) {
172        // merge two Option<Vec<InteractionComponents>>...
173        fn merge_opt_collections(
174            dst: &mut Option<Vec<InteractionComponent>>,
175            src: Option<Vec<InteractionComponent>>,
176        ) {
177            match dst {
178                Some(lhs) => {
179                    if let Some(rhs) = src {
180                        InteractionComponent::merge_collections(lhs, rhs)
181                    }
182                }
183                None => *dst = src,
184            }
185        }
186
187        // extend b-tree maps...
188        merge_maps!(&mut self.name, that.name);
189        merge_maps!(&mut self.description, that.description);
190        merge_maps!(&mut self.extensions, that.extensions);
191        // overwrite if none...
192        if self.type_.is_none() {
193            self.type_ = that.type_
194        }
195        if self.more_info.is_none() {
196            self.more_info = that.more_info
197        }
198        if self.interaction_type.is_none() {
199            self.interaction_type = that.interaction_type
200        }
201        // combine string collections...
202        match &mut self.correct_responses_pattern {
203            Some(this_field) => {
204                if let Some(that_field) = that.correct_responses_pattern {
205                    this_field.extend(that_field);
206                    // NOTE (rsn) 20250412 - ensure no dups...
207                    this_field.sort();
208                    this_field.dedup();
209                }
210            }
211            None => self.correct_responses_pattern = that.correct_responses_pattern,
212        }
213        // merge optional collections of InteractionComponents...
214        merge_opt_collections(&mut self.choices, that.choices);
215        merge_opt_collections(&mut self.scale, that.scale);
216        merge_opt_collections(&mut self.source, that.source);
217        merge_opt_collections(&mut self.target, that.target);
218        merge_opt_collections(&mut self.steps, that.steps);
219    }
220}
221
222impl fmt::Display for ActivityDefinition {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        let mut vec = vec![];
225        if let Some(z_name) = self.name.as_ref() {
226            vec.push(format!("name: {}", z_name));
227        }
228        if let Some(z_description) = self.description.as_ref() {
229            vec.push(format!("description: {}", z_description));
230        }
231        if let Some(z_type) = self.type_.as_ref() {
232            vec.push(format!("type: \"{}\"", z_type));
233        }
234        if let Some(z_more_info) = self.more_info.as_ref() {
235            vec.push(format!("moreInfo: \"{}\"", z_more_info));
236        }
237        if let Some(z_interaction_type) = self.interaction_type.as_ref() {
238            vec.push(format!("interactionType: {}", z_interaction_type));
239        }
240        if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
241            vec.push(format!(
242                "correctResponsesPattern: {}",
243                array_to_display_str(z_correct_responses_pattern)
244            ));
245        }
246        if let Some(z_choices) = self.choices.as_ref() {
247            vec.push(format!("choices: {}", vec_to_display_str(z_choices)));
248        }
249        if let Some(z_scale) = self.scale.as_ref() {
250            vec.push(format!("scale: {}", vec_to_display_str(z_scale)));
251        }
252        if let Some(z_source) = self.source.as_ref() {
253            vec.push(format!("source: {}", vec_to_display_str(z_source)));
254        }
255        if let Some(z_target) = self.target.as_ref() {
256            vec.push(format!("target: {}", vec_to_display_str(z_target)));
257        }
258        if let Some(z_steps) = self.steps.as_ref() {
259            vec.push(format!("steps: {}", vec_to_display_str(z_steps)));
260        }
261        if let Some(z_extensions) = self.extensions.as_ref() {
262            vec.push(format!("extensions: {}", z_extensions))
263        }
264        let res = vec
265            .iter()
266            .map(|x| x.to_string())
267            .collect::<Vec<_>>()
268            .join(", ");
269        write!(f, "ActivityDefinition{{ {res} }}")
270    }
271}
272
273impl Validate for ActivityDefinition {
274    fn validate(&self) -> Vec<ValidationError> {
275        let mut vec: Vec<ValidationError> = vec![];
276
277        // validate type
278        if self.type_.is_some() && self.type_.as_ref().unwrap().is_empty() {
279            vec.push(ValidationError::InvalidIRI("type".into()))
280        }
281        // validate more_info
282        if let Some(z_more_info) = self.more_info.as_ref() {
283            validate_irl(z_more_info).unwrap_or_else(|x| vec.push(x));
284        }
285        // interaction type is guaranteed to be valid when present; is it missing?
286        if (self.correct_responses_pattern.is_some()
287            || self.choices.is_some()
288            || self.scale.is_some()
289            || self.source.is_some()
290            || self.target.is_some()
291            || self.steps.is_some())
292            && self.interaction_type.is_none()
293        {
294            vec.push(ValidationError::ConstraintViolation(
295                "Activity definition interaction-type must be present when any Interaction Activities properties is too".into(),
296            ))
297        }
298        // validate correct response pattern
299        if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
300            for it in z_correct_responses_pattern.iter() {
301                if it.is_empty() {
302                    vec.push(ValidationError::Empty("correctResponsePattern".into()))
303                }
304            }
305        }
306        // validate choices
307        if let Some(z_choices) = self.choices.as_ref() {
308            z_choices.iter().for_each(|x| vec.extend(x.validate()));
309        }
310        // validate scale
311        if let Some(z_scale) = self.scale.as_ref() {
312            z_scale.iter().for_each(|x| vec.extend(x.validate()));
313        }
314        // validate source
315        if let Some(z_source) = self.source.as_ref() {
316            z_source.iter().for_each(|x| vec.extend(x.validate()));
317        }
318        // validate target
319        if let Some(z_target) = self.target.as_ref() {
320            z_target.iter().for_each(|x| vec.extend(x.validate()));
321        }
322        // validate steps
323        if let Some(z_steps) = self.steps.as_ref() {
324            z_steps.iter().for_each(|x| vec.extend(x.validate()));
325        }
326
327        vec
328    }
329}
330
331impl Canonical for ActivityDefinition {
332    fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
333        if let Some(z_name) = self.name.as_mut() {
334            z_name.canonicalize(language_tags)
335        }
336        if let Some(z_description) = self.description.as_mut() {
337            z_description.canonicalize(language_tags)
338        }
339        if let Some(z_choices) = self.choices.as_mut() {
340            for it in z_choices {
341                it.canonicalize(language_tags)
342            }
343        }
344        if let Some(z_scale) = self.scale.as_mut() {
345            for it in z_scale {
346                it.canonicalize(language_tags)
347            }
348        }
349        if let Some(z_source) = self.source.as_mut() {
350            for it in z_source {
351                it.canonicalize(language_tags)
352            }
353        }
354        if let Some(z_target) = self.target.as_mut() {
355            for it in z_target {
356                it.canonicalize(language_tags)
357            }
358        }
359        if let Some(z_steps) = self.steps.as_mut() {
360            for it in z_steps {
361                it.canonicalize(language_tags)
362            }
363        }
364    }
365}
366
367/// A Type that knows how to construct an [ActivityDefinition]
368#[derive(Debug, Default)]
369pub struct ActivityDefinitionBuilder<'a> {
370    _name: Option<LanguageMap>,
371    _description: Option<LanguageMap>,
372    _type_: Option<&'a IriStr>,
373    _more_info: Option<&'a IriStr>,
374    _interaction_type: Option<InteractionType>,
375    _correct_responses_pattern: Option<Vec<String>>,
376    _choices: Option<Vec<InteractionComponent>>,
377    _scale: Option<Vec<InteractionComponent>>,
378    _source: Option<Vec<InteractionComponent>>,
379    _target: Option<Vec<InteractionComponent>>,
380    _steps: Option<Vec<InteractionComponent>>,
381    _extensions: Option<Extensions>,
382}
383
384impl<'a> ActivityDefinitionBuilder<'a> {
385    /// Add the given `label` to the `name` dictionary keyed by the given `tag`.
386    ///
387    /// Raise [DataError] if `tag` is not a valid Language Tag.
388    pub fn name(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
389        add_language!(self._name, tag, label);
390        Ok(self)
391    }
392
393    /// Add the given `label` to the `description` dictionary keyed by the given
394    /// `tag`.
395    ///
396    /// Raise [DataError] if `tag` is not a valid Language Tag.
397    pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
398        add_language!(self._description, tag, label);
399        Ok(self)
400    }
401
402    /// Set the `type_` field.
403    pub fn type_(mut self, val: &'a str) -> Result<Self, DataError> {
404        let val = val.trim();
405        if val.is_empty() {
406            emit_error!(DataError::Validation(ValidationError::Empty("type".into())))
407        } else {
408            let iri = IriStr::new(val)?;
409            self._type_ = Some(iri);
410            Ok(self)
411        }
412    }
413
414    /// Set the `more_info` field.
415    pub fn more_info(mut self, val: &'a str) -> Result<Self, DataError> {
416        let val = val.trim();
417        if val.is_empty() {
418            emit_error!(DataError::Validation(ValidationError::Empty(
419                "more_info".into()
420            )))
421        } else {
422            let val = IriStr::new(val)?;
423            validate_irl(val)?;
424            self._more_info = Some(val);
425            Ok(self)
426        }
427    }
428
429    /// Set the `interaction_type` field.
430    pub fn interaction_type(mut self, val: InteractionType) -> Self {
431        self._interaction_type = Some(val);
432        self
433    }
434
435    /// Add `val` to correct responses pattern.
436    pub fn correct_responses_pattern(mut self, val: &str) -> Result<Self, DataError> {
437        let val = val.trim();
438        if val.is_empty() {
439            emit_error!(DataError::Validation(ValidationError::Empty(
440                "correct_responses_pattern".into()
441            )))
442        }
443        if self._correct_responses_pattern.is_none() {
444            self._correct_responses_pattern = Some(vec![])
445        }
446        self._correct_responses_pattern
447            .as_mut()
448            .unwrap()
449            .push(val.to_string());
450        Ok(self)
451    }
452
453    /// Add `val` to `choices`.
454    pub fn choices(mut self, val: InteractionComponent) -> Result<Self, DataError> {
455        val.check_validity()?;
456        if self._choices.is_none() {
457            self._choices = Some(vec![])
458        }
459        self._choices.as_mut().unwrap().push(val);
460        Ok(self)
461    }
462
463    /// Add `val` to `scale`.
464    pub fn scale(mut self, val: InteractionComponent) -> Result<Self, DataError> {
465        val.check_validity()?;
466        if self._scale.is_none() {
467            self._scale = Some(vec![])
468        }
469        self._scale.as_mut().unwrap().push(val);
470        Ok(self)
471    }
472
473    /// Add `val` to `source`.
474    pub fn source(mut self, val: InteractionComponent) -> Result<Self, DataError> {
475        val.check_validity()?;
476        if self._source.is_none() {
477            self._source = Some(vec![])
478        }
479        self._source.as_mut().unwrap().push(val);
480        Ok(self)
481    }
482
483    /// Add `val` to `target`.
484    pub fn target(mut self, val: InteractionComponent) -> Result<Self, DataError> {
485        val.check_validity()?;
486        if self._target.is_none() {
487            self._target = Some(vec![])
488        }
489        self._target.as_mut().unwrap().push(val);
490        Ok(self)
491    }
492
493    /// Add `val` to `steps`.
494    pub fn steps(mut self, val: InteractionComponent) -> Result<Self, DataError> {
495        val.check_validity()?;
496        if self._steps.is_none() {
497            self._steps = Some(vec![])
498        }
499        self._steps.as_mut().unwrap().push(val);
500        Ok(self)
501    }
502
503    /// Add an extension's `key` and `value` pair.
504    pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
505        if self._extensions.is_none() {
506            self._extensions = Some(Extensions::new());
507        }
508        let _ = self._extensions.as_mut().unwrap().add(key, value);
509        Ok(self)
510    }
511
512    /// Create an [ActivityDefinition] from set field values.
513    ///
514    /// Raise [DataError] if no field was set.
515    pub fn build(self) -> Result<ActivityDefinition, DataError> {
516        if self._name.is_none()
517            && self._description.is_none()
518            && self._type_.is_none()
519            && self._more_info.is_none()
520            && self._interaction_type.is_none()
521            && self._correct_responses_pattern.is_none()
522            && self._choices.is_none()
523            && self._scale.is_none()
524            && self._source.is_none()
525            && self._target.is_none()
526            && self._steps.is_none()
527            && self._extensions.is_none()
528        {
529            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
530                "At least 1 field must be set".into()
531            )))
532        }
533
534        if self._interaction_type.is_none()
535            && (self._correct_responses_pattern.is_some()
536                || self._choices.is_some()
537                || self._scale.is_some()
538                || self._source.is_some()
539                || self._target.is_some()
540                || self._steps.is_some())
541        {
542            emit_error!(DataError::Validation(ValidationError::MissingField(
543                "interaction_type".into()
544            )))
545        }
546
547        Ok(ActivityDefinition {
548            name: self._name,
549            description: self._description,
550            type_: self._type_.map(|x| x.into()),
551            more_info: self._more_info.map(|x| x.into()),
552            interaction_type: self._interaction_type,
553            correct_responses_pattern: self._correct_responses_pattern,
554            choices: self._choices,
555            scale: self._scale,
556            source: self._source,
557            target: self._target,
558            steps: self._steps,
559
560            extensions: self._extensions,
561        })
562    }
563}
564
565fn array_to_display_str(val: &[String]) -> String {
566    let mut vec = vec![];
567    for v in val.iter() {
568        vec.push(format!("\"{v}\""))
569    }
570    vec.iter()
571        .map(|x| x.to_string())
572        .collect::<Vec<_>>()
573        .join(", ")
574}
575
576fn vec_to_display_str(val: &Vec<InteractionComponent>) -> String {
577    let mut vec = vec![];
578    for ic in val {
579        vec.push(format!("{ic}"))
580    }
581    vec.iter()
582        .map(|x| x.to_string())
583        .collect::<Vec<_>>()
584        .join(", ")
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use tracing_test::traced_test;
591
592    #[traced_test]
593    #[test]
594    fn test_display() {
595        const DISPLAY: &str = r#"ActivityDefinition{ description: {"en-US":"Does the xAPI include the concept of statements?"}, type: "http://adlnet.gov/expapi/activities/cmi.interaction", interactionType: true-false, correctResponsesPattern: "true" }"#;
596
597        let json = r#"{
598            "description": {
599                "en-US": "Does the xAPI include the concept of statements?"
600            },
601            "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
602            "interactionType": "true-false",
603            "correctResponsesPattern": [
604                "true"
605            ]
606        }"#;
607
608        let de_result = serde_json::from_str::<ActivityDefinition>(json);
609        assert!(de_result.is_ok());
610        let ad = de_result.unwrap();
611        let display = format!("{}", ad);
612        assert_eq!(display, DISPLAY);
613    }
614
615    #[traced_test]
616    #[test]
617    fn test_missing_interaction_type() {
618        const BAD: &str = r#"{
619"name":{"en": "Fill-In"},
620"description":{"en": "Ben is often heard saying:"},
621"type":"http://adlnet.gov/expapi/activities/cmi.interaction",
622"moreInfo":"http://virtualmeeting.example.com/345256",
623"correctResponsesPattern":["Bob's your uncle"],
624"extensions":{
625 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
626 "http://example.com/profiles/meetings/extension/reporter":{"name":"Thomas","id":"http://openid.com/342"}
627}}"#;
628
629        let de_result = serde_json::from_str::<ActivityDefinition>(BAD);
630        assert!(de_result.is_ok());
631        let ad = de_result.unwrap();
632        // should not be valid b/c missing interaction_type!
633        assert!(!ad.is_valid());
634    }
635}