Skip to main content

xapi_data/
context_activities.rs

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