xapi_rs/data/
context_activities.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    ActivityId, DataError,
5    data::{Activity, Fingerprint, Validate, ValidationError},
6    emit_error,
7};
8use core::fmt;
9use serde::{Deserialize, Serialize};
10use serde_with::{OneOrMany, serde_as, skip_serializing_none};
11use std::hash::Hasher;
12
13#[serde_as]
14#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
15struct Activities(
16    #[serde_as(deserialize_as = "OneOrMany<_>", serialize_as = "Vec<_>")] Vec<Activity>,
17);
18
19#[serde_as]
20#[derive(Debug, Serialize)]
21struct ActivitiesId(#[serde_as(serialize_as = "Vec<_>")] Vec<ActivityId>);
22
23impl From<Activities> for ActivitiesId {
24    fn from(value: Activities) -> Self {
25        ActivitiesId(value.0.into_iter().map(ActivityId::from).collect())
26    }
27}
28
29impl From<ActivitiesId> for Activities {
30    fn from(value: ActivitiesId) -> Self {
31        Activities(value.0.into_iter().map(Activity::from).collect())
32    }
33}
34
35/// Map of types of learning activity context that a [Statement][1] is
36/// related to, represented as a structure (rather than the usual map).
37///
38/// The keys of this map, or fields of the structure are `parent`, `grouping`,
39/// `category`, or `other`. Their corresponding values, when set, are
40/// collections of 1 or more [Activities][2].
41///
42/// [1]: crate::Statement
43/// [2]: crate::Activity
44#[serde_as]
45#[skip_serializing_none]
46#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
47#[serde(deny_unknown_fields)]
48pub struct ContextActivities {
49    parent: Option<Activities>,
50    grouping: Option<Activities>,
51    category: Option<Activities>,
52    other: Option<Activities>,
53}
54
55#[skip_serializing_none]
56#[derive(Debug, Serialize)]
57pub(crate) struct ContextActivitiesId {
58    parent: Option<ActivitiesId>,
59    grouping: Option<ActivitiesId>,
60    category: Option<ActivitiesId>,
61    other: Option<ActivitiesId>,
62}
63
64impl From<ContextActivities> for ContextActivitiesId {
65    fn from(value: ContextActivities) -> Self {
66        ContextActivitiesId {
67            parent: value.parent.map(|x| x.into()),
68            grouping: value.grouping.map(|x| x.into()),
69            category: value.category.map(|x| x.into()),
70            other: value.other.map(|x| x.into()),
71        }
72    }
73}
74
75impl From<ContextActivitiesId> for ContextActivities {
76    fn from(value: ContextActivitiesId) -> Self {
77        ContextActivities {
78            parent: value.parent.map(Activities::from),
79            grouping: value.grouping.map(Activities::from),
80            category: value.category.map(Activities::from),
81            other: value.other.map(Activities::from),
82        }
83    }
84}
85
86impl ContextActivities {
87    /// Return a [ContextActivities] _Builder_.
88    pub fn builder() -> ContextActivitiesBuilder {
89        ContextActivitiesBuilder::default()
90    }
91
92    /// Return `parent` if set; `None` otherwise.
93    pub fn parent(&self) -> &[Activity] {
94        if self.parent.is_none() {
95            &[]
96        } else {
97            self.parent.as_ref().unwrap().0.as_slice()
98        }
99    }
100
101    /// Return `grouping` if set; `None` otherwise.
102    pub fn grouping(&self) -> &[Activity] {
103        if self.grouping.is_none() {
104            &[]
105        } else {
106            self.grouping.as_ref().unwrap().0.as_slice()
107        }
108    }
109
110    /// Return `category` if set; `None` otherwise.
111    pub fn category(&self) -> &[Activity] {
112        if self.category.is_none() {
113            &[]
114        } else {
115            self.category.as_ref().unwrap().0.as_slice()
116        }
117    }
118
119    /// Return `other` if set; `None` otherwise.
120    pub fn other(&self) -> &[Activity] {
121        if self.other.is_none() {
122            &[]
123        } else {
124            self.other.as_ref().unwrap().0.as_slice()
125        }
126    }
127}
128
129impl Fingerprint for ContextActivities {
130    fn fingerprint<H: Hasher>(&self, state: &mut H) {
131        if self.parent.is_some() {
132            Fingerprint::fingerprint_slice(self.parent(), state)
133        }
134        if self.grouping.is_some() {
135            Fingerprint::fingerprint_slice(self.grouping(), state)
136        }
137        if self.category.is_some() {
138            Fingerprint::fingerprint_slice(self.category(), state)
139        }
140        if self.other.is_some() {
141            Fingerprint::fingerprint_slice(self.other(), state)
142        }
143    }
144}
145
146impl fmt::Display for ContextActivities {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        let mut vec = vec![];
149
150        if self.parent.is_some() {
151            vec.push(format!(
152                "parent: [{}]",
153                self.parent()
154                    .iter()
155                    .map(|x| x.to_string())
156                    .collect::<Vec<_>>()
157                    .join(", ")
158            ))
159        }
160        if self.grouping.is_some() {
161            vec.push(format!(
162                "grouping: [{}]",
163                self.grouping()
164                    .iter()
165                    .map(|x| x.to_string())
166                    .collect::<Vec<_>>()
167                    .join(", ")
168            ))
169        }
170        if self.category.is_some() {
171            vec.push(format!(
172                "category: [{}]",
173                self.category()
174                    .iter()
175                    .map(|x| x.to_string())
176                    .collect::<Vec<_>>()
177                    .join(", ")
178            ))
179        }
180        if self.other.is_some() {
181            vec.push(format!(
182                "other: [{}]",
183                self.other()
184                    .iter()
185                    .map(|x| x.to_string())
186                    .collect::<Vec<_>>()
187                    .join(", ")
188            ))
189        }
190
191        let res = vec
192            .iter()
193            .map(|x| x.to_string())
194            .collect::<Vec<_>>()
195            .join(", ");
196        write!(f, "{{ {res} }}")
197    }
198}
199
200impl Validate for ContextActivities {
201    fn validate(&self) -> Vec<ValidationError> {
202        let mut vec = vec![];
203
204        if self.parent.is_some() {
205            self.parent().iter().for_each(|x| vec.extend(x.validate()));
206        }
207        if self.grouping.is_some() {
208            self.grouping()
209                .iter()
210                .for_each(|x| vec.extend(x.validate()));
211        }
212        if self.category.is_some() {
213            self.category()
214                .iter()
215                .for_each(|x| vec.extend(x.validate()));
216        }
217        if self.other.is_some() {
218            self.other().iter().for_each(|x| vec.extend(x.validate()));
219        }
220
221        vec
222    }
223}
224
225/// A Type that knows how to construct a [ContextActivities].
226#[derive(Debug, Default)]
227pub struct ContextActivitiesBuilder {
228    _parent: Vec<Activity>,
229    _grouping: Vec<Activity>,
230    _category: Vec<Activity>,
231    _other: Vec<Activity>,
232}
233
234impl ContextActivitiesBuilder {
235    /// Add `val` to `parent`'s list.
236    ///
237    /// Raise [DataError] if `val` is invalid.
238    pub fn parent(mut self, val: Activity) -> Result<Self, DataError> {
239        val.check_validity()?;
240        self._parent.push(val);
241        Ok(self)
242    }
243
244    /// Add `val` to `grouping`'s list.
245    ///
246    /// Raise [DataError] if `val` is invalid.
247    pub fn grouping(mut self, val: Activity) -> Result<Self, DataError> {
248        val.check_validity()?;
249        self._grouping.push(val);
250        Ok(self)
251    }
252
253    /// Add `val` to `category`'s list.
254    ///
255    /// Raise [DataError] if `val` is invalid.
256    pub fn category(mut self, val: Activity) -> Result<Self, DataError> {
257        val.check_validity()?;
258        self._category.push(val);
259        Ok(self)
260    }
261
262    /// Add `val` to `other`'s list.
263    ///
264    /// Raise [DataError] if `val` is invalid.
265    pub fn other(mut self, val: Activity) -> Result<Self, DataError> {
266        val.check_validity()?;
267        self._other.push(val);
268        Ok(self)
269    }
270
271    /// Create an [ContextActivities] from set field values.
272    ///
273    /// Raise a [DataError] if no _key_ is set.
274    pub fn build(self) -> Result<ContextActivities, DataError> {
275        if self._parent.is_empty()
276            && self._grouping.is_empty()
277            && self._category.is_empty()
278            && self._other.is_empty()
279        {
280            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
281                "At least one of the keys must be set".into()
282            )))
283        } else {
284            Ok(ContextActivities {
285                parent: if self._parent.is_empty() {
286                    None
287                } else {
288                    Some(Activities(self._parent))
289                },
290                grouping: if self._grouping.is_empty() {
291                    None
292                } else {
293                    Some(Activities(self._grouping))
294                },
295                category: if self._category.is_empty() {
296                    None
297                } else {
298                    Some(Activities(self._category))
299                },
300                other: if self._other.is_empty() {
301                    None
302                } else {
303                    Some(Activities(self._other))
304                },
305            })
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_missing_keys() -> Result<(), DataError> {
316        const CA: &str = r#"{}"#;
317
318        let ca = serde_json::from_str::<ContextActivities>(CA).map_err(|x| DataError::JSON(x))?;
319        assert_eq!(ca.parent, None);
320        assert!(ca.parent().is_empty());
321        assert_eq!(ca.grouping, None);
322        assert!(ca.grouping().is_empty());
323        assert_eq!(ca.category, None);
324        assert!(ca.category().is_empty());
325        assert_eq!(ca.other, None);
326        assert!(ca.other().is_empty());
327
328        Ok(())
329    }
330
331    #[test]
332    fn test_one_or_many() -> Result<(), DataError> {
333        const CA1: &str = r#"{"parent":{"id":"http://xapi.acticity/1"}}"#;
334        const CA2: &str =
335            r#"{"other":[{"id":"http://xapi.activity/1"},{"id":"http://xapi.activity/2"}]}"#;
336
337        let one = serde_json::from_str::<ContextActivities>(CA1).map_err(|x| DataError::JSON(x))?;
338        assert!(one.parent.is_some());
339        assert_eq!(one.parent().len(), 1);
340
341        let many =
342            serde_json::from_str::<ContextActivities>(CA2).map_err(|x| DataError::JSON(x))?;
343        assert!(many.other.is_some());
344        assert_eq!(many.other().len(), 2);
345
346        Ok(())
347    }
348
349    #[test]
350    fn test_serialize_as_array() {
351        const CA: &str = r#"{"parent":{"id":"http://xapi.acticity/1"}}"#;
352        const EXPECTED: &str = r#"{"parent":[{"id":"http://xapi.acticity/1"}]}"#;
353
354        let ca = serde_json::from_str::<ContextActivities>(CA).unwrap();
355        let actual = serde_json::to_string(&ca).unwrap();
356        assert_eq!(EXPECTED, actual);
357    }
358}