xapi_rs/data/
interaction_component.rs

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