Skip to main content

xapi_data/
interaction_component.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    Canonical, DataError, LanguageMap, MyLanguageTag, Validate, ValidationError, add_language,
5    emit_error, merge_maps,
6};
7use core::fmt;
8use serde::{Deserialize, Serialize};
9use serde_with::skip_serializing_none;
10
11/// Depending on the value of the `interactionType` property of an
12/// [ActivityDefinition][1], an [Activity][2] can provide additional
13/// properties, each potentially being a list of [InteractionComponent]s.
14///
15/// [1]: crate::ActivityDefinition
16/// [2]: crate::Activity
17#[skip_serializing_none]
18#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
19pub struct InteractionComponent {
20    id: String,
21    description: Option<LanguageMap>,
22}
23
24impl InteractionComponent {
25    /// Return an [InteractionComponent] _Builder_.
26    pub fn builder() -> InteractionComponentBuilder<'static> {
27        InteractionComponentBuilder::default()
28    }
29
30    /// Return the `id` field.
31    pub fn id(&self) -> &str {
32        &self.id
33    }
34
35    /// Return `description` (e.g. the text for a given choice in a multiple-
36    /// choice interaction) in the designated language `tag` if it exists;
37    /// `None` otherwise.
38    pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
39        match &self.description {
40            Some(lm) => lm.get(tag),
41            None => None,
42        }
43    }
44
45    /// Consume and iterate over elements in `src` combining them w/ those in `dst`.
46    ///
47    /// Merging is done on matching `id` values. If the instance is new to `dst`
48    /// it's added as is. Otherwise, its `description` is merged with the existing
49    /// one in `dst`.
50    pub(crate) fn merge_collections(
51        dst: &mut Vec<InteractionComponent>,
52        src: Vec<InteractionComponent>,
53    ) {
54        for src_ic in src {
55            match dst.iter().position(|x| x.id == src_ic.id) {
56                Some(n) => {
57                    let dst_ic = &mut dst[n];
58                    merge_maps!(&mut dst_ic.description, src_ic.description);
59                }
60                None => dst.push(src_ic),
61            }
62        }
63    }
64}
65
66impl fmt::Display for InteractionComponent {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        let mut vec = vec![];
69
70        vec.push(format!("id: \"{}\"", self.id));
71        if let Some(z_description) = self.description.as_ref() {
72            vec.push(format!("description: {}", z_description));
73        }
74
75        let res = vec
76            .iter()
77            .map(|x| x.to_string())
78            .collect::<Vec<_>>()
79            .join(", ");
80        write!(f, "InteractionComponent{{ {res} }}")
81    }
82}
83
84impl Validate for InteractionComponent {
85    fn validate(&self) -> Vec<ValidationError> {
86        let mut vec = vec![];
87
88        if self.id.is_empty() {
89            vec.push(ValidationError::Empty("id".into()))
90        }
91
92        vec
93    }
94}
95
96impl Canonical for InteractionComponent {
97    fn canonicalize(&mut self, tags: &[MyLanguageTag]) {
98        if let Some(z_description) = self.description.as_mut() {
99            z_description.canonicalize(tags)
100        }
101    }
102}
103
104/// A Type that knows how to construct an [InteractionComponent].
105#[derive(Debug, Default)]
106pub struct InteractionComponentBuilder<'a> {
107    _id: Option<&'a str>,
108    _description: Option<LanguageMap>,
109}
110
111impl<'a> InteractionComponentBuilder<'a> {
112    /// Set the `id` field.
113    ///
114    /// Raise [DataError] if the argument is empty.
115    pub fn id(mut self, val: &'a str) -> Result<Self, DataError> {
116        let val = val.trim();
117        if val.is_empty() {
118            emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
119        } else {
120            self._id = Some(val);
121            Ok(self)
122        }
123    }
124
125    /// Add `label` tagged by language `tag` to the _description_ dictionary.
126    ///
127    /// Raise [DataError] if an error occurred; e.g. the `tag` is invalid.
128    pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
129        add_language!(self._description, tag, label);
130        Ok(self)
131    }
132
133    /// Create an [InteractionComponent] from set field values.
134    ///
135    /// Raise [DataError] if the `id` field is missing.
136    pub fn build(self) -> Result<InteractionComponent, DataError> {
137        if let Some(z_id) = self._id {
138            Ok(InteractionComponent {
139                id: z_id.to_owned(),
140                description: self._description,
141            })
142        } else {
143            emit_error!(DataError::Validation(ValidationError::MissingField(
144                "id".into()
145            )))
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use std::str::FromStr;
154    use tracing_test::traced_test;
155
156    #[test]
157    fn test_id_len() -> Result<(), DataError> {
158        let result = InteractionComponent::builder().id("a")?.build();
159        assert!(result.is_ok());
160
161        let result = InteractionComponent::builder().id("");
162        assert!(result.is_err());
163
164        Ok(())
165    }
166
167    #[test]
168    fn test_description() -> Result<(), DataError> {
169        let result = InteractionComponent::builder().id("foo")?.build();
170        assert!(result.is_ok());
171
172        let en = MyLanguageTag::from_str("en")?;
173
174        let ic = InteractionComponent::builder()
175            .id("foo")?
176            .description(&en, "label")?
177            .build()?;
178        assert!(ic.description(&en).is_some());
179        assert_eq!(ic.description(&en).unwrap(), "label");
180
181        Ok(())
182    }
183
184    #[traced_test]
185    #[test]
186    fn test_serde() -> Result<(), DataError> {
187        const JSON: &str = r#"{"id":"foo","description":{"en":"hello","it":"ciao"}}"#;
188
189        let en = MyLanguageTag::from_str("en")?;
190        let it = MyLanguageTag::from_str("it")?;
191
192        let ic = InteractionComponent::builder()
193            .id("foo")?
194            .description(&en, "hello")?
195            .description(&it, "ciao")?
196            .build()?;
197        let se_result = serde_json::to_string(&ic);
198        assert!(se_result.is_ok());
199        let json = se_result.unwrap();
200        assert_eq!(json, JSON);
201
202        let de_result = serde_json::from_str::<InteractionComponent>(JSON);
203        assert!(de_result.is_ok());
204        let ic2 = de_result.unwrap();
205        assert_eq!(ic, ic2);
206
207        Ok(())
208    }
209
210    #[test]
211    fn test_merge_disjoint_collections() -> Result<(), DataError> {
212        let en = MyLanguageTag::from_str("en")?;
213        let it = MyLanguageTag::from_str("it")?;
214
215        let ic1 = InteractionComponent::builder()
216            .id("foo")?
217            .description(&en, "hello")?
218            .build()?;
219        let mut dst = vec![ic1];
220        assert_eq!(dst.len(), 1);
221
222        let ic2 = InteractionComponent::builder()
223            .id("bar")?
224            .description(&it, "ciao")?
225            .build()?;
226        let src = vec![ic2];
227        assert_eq!(src.len(), 1);
228
229        InteractionComponent::merge_collections(&mut dst, src);
230        // no common-ground. `src` is added to `dst`...
231        assert_eq!(dst.len(), 2);
232
233        Ok(())
234    }
235
236    #[test]
237    fn test_merge_collections() -> Result<(), DataError> {
238        let en = MyLanguageTag::from_str("en")?;
239        let it = MyLanguageTag::from_str("it")?;
240        let de = MyLanguageTag::from_str("de")?;
241
242        let ic1 = InteractionComponent::builder()
243            .id("foo")?
244            .description(&en, "hello")?
245            .build()?;
246        let mut dst = vec![ic1];
247        assert_eq!(dst.len(), 1);
248
249        let ic2 = InteractionComponent::builder()
250            .id("foo")?
251            .description(&it, "ciao")?
252            .build()?;
253        let src = vec![ic2];
254        assert_eq!(src.len(), 1);
255
256        InteractionComponent::merge_collections(&mut dst, src);
257        // ic1 should remain the single member of `dst`...
258        assert_eq!(dst.len(), 1);
259        // ic1's description should now contain "ciao"...
260        assert!(dst[0].description.is_some());
261        assert_eq!(dst[0].description.as_ref().unwrap().len(), 2);
262        assert_eq!(dst[0].description(&en), Some("hello"));
263        assert_eq!(dst[0].description(&it), Some("ciao"));
264        assert_eq!(dst[0].description(&de), None);
265
266        Ok(())
267    }
268}