Skip to main content

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 let Some(z_description) = self.description.as_ref() {
73            vec.push(format!("description: {}", z_description));
74        }
75
76        let res = vec
77            .iter()
78            .map(|x| x.to_string())
79            .collect::<Vec<_>>()
80            .join(", ");
81        write!(f, "InteractionComponent{{ {res} }}")
82    }
83}
84
85impl Validate for InteractionComponent {
86    fn validate(&self) -> Vec<ValidationError> {
87        let mut vec = vec![];
88
89        if self.id.is_empty() {
90            vec.push(ValidationError::Empty("id".into()))
91        }
92
93        vec
94    }
95}
96
97impl Canonical for InteractionComponent {
98    fn canonicalize(&mut self, tags: &[MyLanguageTag]) {
99        if let Some(z_description) = self.description.as_mut() {
100            z_description.canonicalize(tags)
101        }
102    }
103}
104
105/// A Type that knows how to construct an [InteractionComponent].
106#[derive(Debug, Default)]
107pub struct InteractionComponentBuilder<'a> {
108    _id: Option<&'a str>,
109    _description: Option<LanguageMap>,
110}
111
112impl<'a> InteractionComponentBuilder<'a> {
113    /// Set the `id` field.
114    ///
115    /// Raise [DataError] if the argument is empty.
116    pub fn id(mut self, val: &'a str) -> Result<Self, DataError> {
117        let val = val.trim();
118        if val.is_empty() {
119            emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
120        } else {
121            self._id = Some(val);
122            Ok(self)
123        }
124    }
125
126    /// Add `label` tagged by language `tag` to the _description_ dictionary.
127    ///
128    /// Raise [DataError] if an error occurred; e.g. the `tag` is invalid.
129    pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
130        add_language!(self._description, tag, label);
131        Ok(self)
132    }
133
134    /// Create an [InteractionComponent] from set field values.
135    ///
136    /// Raise [DataError] if the `id` field is missing.
137    pub fn build(self) -> Result<InteractionComponent, DataError> {
138        if let Some(z_id) = self._id {
139            Ok(InteractionComponent {
140                id: z_id.to_owned(),
141                description: self._description,
142            })
143        } else {
144            emit_error!(DataError::Validation(ValidationError::MissingField(
145                "id".into()
146            )))
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::str::FromStr;
155    use tracing_test::traced_test;
156
157    #[test]
158    fn test_id_len() -> Result<(), DataError> {
159        let result = InteractionComponent::builder().id("a")?.build();
160        assert!(result.is_ok());
161
162        let result = InteractionComponent::builder().id("");
163        assert!(result.is_err());
164
165        Ok(())
166    }
167
168    #[test]
169    fn test_description() -> Result<(), DataError> {
170        let result = InteractionComponent::builder().id("foo")?.build();
171        assert!(result.is_ok());
172
173        let en = MyLanguageTag::from_str("en")?;
174
175        let ic = InteractionComponent::builder()
176            .id("foo")?
177            .description(&en, "label")?
178            .build()?;
179        assert!(ic.description(&en).is_some());
180        assert_eq!(ic.description(&en).unwrap(), "label");
181
182        Ok(())
183    }
184
185    #[traced_test]
186    #[test]
187    fn test_serde() -> Result<(), DataError> {
188        const JSON: &str = r#"{"id":"foo","description":{"en":"hello","it":"ciao"}}"#;
189
190        let en = MyLanguageTag::from_str("en")?;
191        let it = MyLanguageTag::from_str("it")?;
192
193        let ic = InteractionComponent::builder()
194            .id("foo")?
195            .description(&en, "hello")?
196            .description(&it, "ciao")?
197            .build()?;
198        let se_result = serde_json::to_string(&ic);
199        assert!(se_result.is_ok());
200        let json = se_result.unwrap();
201        assert_eq!(json, JSON);
202
203        let de_result = serde_json::from_str::<InteractionComponent>(JSON);
204        assert!(de_result.is_ok());
205        let ic2 = de_result.unwrap();
206        assert_eq!(ic, ic2);
207
208        Ok(())
209    }
210
211    #[test]
212    fn test_merge_disjoint_collections() -> Result<(), DataError> {
213        let en = MyLanguageTag::from_str("en")?;
214        let it = MyLanguageTag::from_str("it")?;
215
216        let ic1 = InteractionComponent::builder()
217            .id("foo")?
218            .description(&en, "hello")?
219            .build()?;
220        let mut dst = vec![ic1];
221        assert_eq!(dst.len(), 1);
222
223        let ic2 = InteractionComponent::builder()
224            .id("bar")?
225            .description(&it, "ciao")?
226            .build()?;
227        let src = vec![ic2];
228        assert_eq!(src.len(), 1);
229
230        InteractionComponent::merge_collections(&mut dst, src);
231        // no common-ground. `src` is added to `dst`...
232        assert_eq!(dst.len(), 2);
233
234        Ok(())
235    }
236
237    #[test]
238    fn test_merge_collections() -> Result<(), DataError> {
239        let en = MyLanguageTag::from_str("en")?;
240        let it = MyLanguageTag::from_str("it")?;
241        let de = MyLanguageTag::from_str("de")?;
242
243        let ic1 = InteractionComponent::builder()
244            .id("foo")?
245            .description(&en, "hello")?
246            .build()?;
247        let mut dst = vec![ic1];
248        assert_eq!(dst.len(), 1);
249
250        let ic2 = InteractionComponent::builder()
251            .id("foo")?
252            .description(&it, "ciao")?
253            .build()?;
254        let src = vec![ic2];
255        assert_eq!(src.len(), 1);
256
257        InteractionComponent::merge_collections(&mut dst, src);
258        // ic1 should remain the single member of `dst`...
259        assert_eq!(dst.len(), 1);
260        // ic1's description should now contain "ciao"...
261        assert!(dst[0].description.is_some());
262        assert_eq!(dst[0].description.as_ref().unwrap().len(), 2);
263        assert_eq!(dst[0].description(&en), Some("hello"));
264        assert_eq!(dst[0].description(&it), Some("ciao"));
265        assert_eq!(dst[0].description(&de), None);
266
267        Ok(())
268    }
269}