Skip to main content

xapi_rs/data/
activity_definition.rs

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