Skip to main content

xapi_data/
score.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{DataError, Fingerprint, Validate, ValidationError, emit_error};
4use core::fmt;
5use serde::{
6    Deserialize, Deserializer, Serialize,
7    de::{self, MapAccess, Visitor},
8};
9use serde_with::skip_serializing_none;
10use std::{hash::Hasher, ops::RangeInclusive};
11
12const VALID_SCALE: RangeInclusive<f32> = -1.0..=1.0;
13
14/// Structure capturing the outcome of a graded [Activity][1] achieved
15/// by an [Actor][2].
16///
17/// [1]: crate::Activity
18/// [2]: crate::Actor
19#[skip_serializing_none]
20#[derive(Clone, Debug, PartialEq, Serialize)]
21#[serde(deny_unknown_fields)]
22pub struct Score {
23    scaled: Option<f32>,
24    raw: Option<f32>,
25    min: Option<f32>,
26    max: Option<f32>,
27}
28
29/// xAPI mandates few constraints on the values of a [Score] properties such
30/// as:
31/// 1. `scaled` must be w/in the range \[-1.0 .. +1.0 \].
32/// 2. `min` must be less than `max`.
33/// 3. `raw` must be w/in the range \[`min` .. `max` \].
34///
35/// We make sure these rules are respected while parsing the JSON stream and
36/// abort the process if they're not.
37impl<'de> Deserialize<'de> for Score {
38    fn deserialize<D>(des: D) -> Result<Self, D::Error>
39    where
40        D: Deserializer<'de>,
41    {
42        const FIELDS: &[&str] = &["scaled", "raw", "min", "max"];
43
44        #[derive(Deserialize)]
45        #[serde(field_identifier, rename_all = "lowercase")]
46        enum Field {
47            Scaled,
48            Raw,
49            Min,
50            Max,
51        }
52
53        struct ScoreVisitor;
54
55        impl<'de> Visitor<'de> for ScoreVisitor {
56            type Value = Score;
57
58            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
59                formatter.write_str("Score")
60            }
61
62            fn visit_map<V>(self, mut map: V) -> Result<Score, V::Error>
63            where
64                V: MapAccess<'de>,
65            {
66                let mut scaled = None;
67                let mut raw = None;
68                let mut min = None;
69                let mut max = None;
70                while let Some(key) = map.next_key()? {
71                    match key {
72                        Field::Scaled => {
73                            if scaled.is_some() {
74                                return Err(de::Error::duplicate_field("scaled"));
75                            }
76                            // value must be in [-1.0 .. +1.0]
77                            let value: f32 = map.next_value()?;
78                            if !VALID_SCALE.contains(&value) {
79                                return Err(de::Error::custom("scaled is out-of-bounds"));
80                            }
81                            scaled = Some(value);
82                        }
83                        Field::Raw => {
84                            if raw.is_some() {
85                                return Err(de::Error::duplicate_field("raw"));
86                            }
87                            let value: f32 = map.next_value()?;
88                            raw = Some(value);
89                        }
90                        Field::Min => {
91                            if min.is_some() {
92                                return Err(de::Error::duplicate_field("min"));
93                            }
94                            let value: f32 = map.next_value()?;
95                            min = Some(value);
96                        }
97                        Field::Max => {
98                            if max.is_some() {
99                                return Err(de::Error::duplicate_field("max"));
100                            }
101                            let value: f32 = map.next_value()?;
102                            max = Some(value);
103                        }
104                    }
105                }
106                // at least 1 field must be set...
107                if scaled.is_none() && raw.is_none() && min.is_none() && max.is_none() {
108                    return Err(de::Error::missing_field("scaled | raw | min | max"));
109                }
110                let lower = min.unwrap_or(f32::MIN);
111                let upper = max.unwrap_or(f32::MAX);
112                if upper < lower {
113                    return Err(de::Error::custom("max < min"));
114                }
115                if raw.is_some() && !(lower..upper).contains(raw.as_ref().unwrap()) {
116                    return Err(de::Error::custom("raw is out-of-bounds"));
117                }
118                Ok(Score {
119                    scaled,
120                    raw,
121                    min,
122                    max,
123                })
124            }
125        }
126
127        des.deserialize_struct("Score", FIELDS, ScoreVisitor)
128    }
129}
130
131impl Score {
132    /// Return a [Score] _Builder_.
133    pub fn builder() -> ScoreBuilder {
134        ScoreBuilder::default()
135    }
136
137    /// Return the score related to the experience as modified by scaling
138    /// and/or normalization.
139    ///
140    /// Valid values are expected to be w/in \[-1.0 .. +1.0\] range.
141    pub fn scaled(&self) -> Option<f32> {
142        self.scaled
143    }
144
145    /// Return the score achieved by the [Actor][1] in the experience described
146    /// in a [Statement][2]. It's expected not to be modified by any scaling or
147    /// normalization.
148    ///
149    /// [1]: crate::Actor
150    /// [2]: crate::Statement
151    pub fn raw(&self) -> Option<f32> {
152        self.raw
153    }
154
155    /// Return the lowest possible score for the experience described by a
156    /// [Statement][crate::Statement].
157    pub fn min(&self) -> Option<f32> {
158        self.min
159    }
160
161    /// Return the highest possible score for the experience described by a
162    /// [Statement][crate::Statement].
163    pub fn max(&self) -> Option<f32> {
164        self.max
165    }
166}
167
168impl Fingerprint for Score {
169    fn fingerprint<H: Hasher>(&self, state: &mut H) {
170        if self.scaled.is_some() {
171            state.write(&self.scaled().unwrap().to_le_bytes())
172        }
173        if self.raw.is_some() {
174            state.write(&self.raw().unwrap().to_le_bytes())
175        }
176        if self.min.is_some() {
177            state.write(&self.min().unwrap().to_le_bytes())
178        }
179        if self.max.is_some() {
180            state.write(&self.max().unwrap().to_le_bytes())
181        }
182    }
183}
184
185impl fmt::Display for Score {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        let mut vec = vec![];
188
189        if let Some(z_scaled) = self.scaled.as_ref() {
190            vec.push(format!("scaled: {}", z_scaled))
191        }
192        if let Some(z_raw) = self.raw.as_ref() {
193            vec.push(format!("raw: {}", z_raw))
194        }
195        if let Some(z_min) = self.min.as_ref() {
196            vec.push(format!("min: {}", z_min))
197        }
198        if let Some(z_max) = self.max.as_ref() {
199            vec.push(format!("max: {}", z_max))
200        }
201
202        let res = vec
203            .iter()
204            .map(|x| x.to_string())
205            .collect::<Vec<_>>()
206            .join(", ");
207        write!(f, "Score{{ {res} }}")
208    }
209}
210
211impl Validate for Score {
212    // since we now implement our own deserializer --which ensures all the
213    // validation constraints are honoured on parsing the input-- and given
214    // that our Builder does the same w/ its Setters, this implementation
215    // of Validate is NOOP.
216    fn validate(&self) -> Vec<ValidationError> {
217        vec![]
218    }
219}
220
221/// A Type that knows how to construct a [Score].
222#[derive(Debug, Default)]
223pub struct ScoreBuilder {
224    _scaled: Option<f32>,
225    _raw: Option<f32>,
226    _min: Option<f32>,
227    _max: Option<f32>,
228}
229
230impl ScoreBuilder {
231    /// Set the `scaled` field which must be w/in \[-1.0 .. +1.0\] range.
232    pub fn scaled(mut self, val: f32) -> Result<Self, DataError> {
233        if !VALID_SCALE.contains(&val) {
234            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
235                format!("'scaled' ({val}) is out-of-bounds").into()
236            )))
237        } else {
238            self._scaled = Some(val);
239            Ok(self)
240        }
241    }
242
243    /// Set the `raw` field.
244    pub fn raw(mut self, val: f32) -> Self {
245        self._raw = Some(val);
246        self
247    }
248
249    /// Set the `min` field.
250    pub fn min(mut self, val: f32) -> Self {
251        self._min = Some(val);
252        self
253    }
254
255    /// Set the `max` field.
256    pub fn max(mut self, val: f32) -> Self {
257        self._max = Some(val);
258        self
259    }
260
261    /// Create a [Score] from set field vaues.
262    ///
263    /// Raise [DataError] if no field was set or an inconsistency is detected.
264    pub fn build(self) -> Result<Score, DataError> {
265        if self._scaled.is_none()
266            && self._raw.is_none()
267            && self._min.is_none()
268            && self._max.is_none()
269        {
270            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
271                "At least one field must be set".into()
272            )))
273        }
274        // no need to validate scaled.  it's already done...
275        let min = self._min.unwrap_or(f32::MIN);
276        let max = self._max.unwrap_or(f32::MAX);
277        if max < min {
278            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
279                "'min', 'max', or both are set but 'max' is less than 'min'".into()
280            )))
281        } else if self._raw.is_some() && !(min..max).contains(self._raw.as_ref().unwrap()) {
282            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
283                "'raw' is out-of-bounds".into()
284            )))
285        }
286        Ok(Score {
287            scaled: self._scaled,
288            raw: self._raw,
289            min: self._min,
290            max: self._max,
291        })
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_1field_min() {
301        const SCORE: &str = r#"{ }"#;
302
303        let res = serde_json::from_str::<Score>(SCORE);
304        assert!(res.is_err());
305        let msg = res.err().unwrap().to_string();
306        assert!(msg.contains("missing field"));
307    }
308
309    #[test]
310    fn test_dup_field() {
311        const SCORE: &str = r#"{ "scaled": 0.9, "raw": 5, "min": 1, "scaled": 0.2, "max": 10 }"#;
312
313        let res = serde_json::from_str::<Score>(SCORE);
314        assert!(res.is_err());
315        let msg = res.err().unwrap().to_string();
316        assert!(msg.contains("duplicate field"));
317    }
318
319    #[test]
320    fn test_all_good() {
321        const SCORE: &str = r#"{ "scaled": 0.95, "raw": 42, "min": 10.0, "max": 100.0 }"#;
322
323        let res = serde_json::from_str::<Score>(SCORE);
324        assert!(res.is_ok());
325        let score = res.unwrap();
326        assert_eq!(score.scaled.unwrap(), 0.95);
327        assert_eq!(score.raw.unwrap(), 42.0);
328        assert_eq!(score.min.unwrap(), 10.0);
329        assert_eq!(score.max.unwrap(), 100.0);
330    }
331
332    #[test]
333    fn test_scaled_oob() {
334        const SCORE: &str = r#"{ "scaled": 1.1, "raw": 42 }"#;
335
336        let res = serde_json::from_str::<Score>(SCORE);
337        assert!(res.is_err());
338        let msg = res.err().unwrap().to_string();
339        assert!(msg.contains("scaled is out-of-bounds"));
340    }
341
342    #[test]
343    fn test_limits_bad() {
344        const SCORE: &str = r#"{ "scaled": 0.95, "raw": 42, "min": 50.0, "max": 10.0 }"#;
345
346        let res = serde_json::from_str::<Score>(SCORE);
347        assert!(res.is_err());
348        let msg = res.err().unwrap().to_string();
349        assert!(msg.contains("max < min"));
350    }
351
352    #[test]
353    fn test_raw_oob() {
354        const SCORE: &str = r#"{ "scaled": 0.95, "raw": 12.5, "min": 0.0, "max": 10.0 }"#;
355
356        let res = serde_json::from_str::<Score>(SCORE);
357        assert!(res.is_err());
358        let msg = res.err().unwrap().to_string();
359        assert!(msg.contains("raw is out-of-bounds"));
360    }
361
362    #[test]
363    fn test_builder() -> Result<(), DataError> {
364        // at least 1 field must be set...
365        let r = Score::builder().build();
366        assert!(r.is_err());
367
368        // scaled must be w/in [-1..+1]...
369        let r = Score::builder().scaled(1.1);
370        assert!(r.is_err());
371
372        // min must be < max...
373        let r = Score::builder().scaled(0.8)?.min(10.0).max(0.0).build();
374        assert!(r.is_err());
375
376        // raw must be w/in [min..max]...
377        let r = Score::builder()
378            .scaled(0.8)?
379            .raw(11.0)
380            .min(0.0)
381            .max(10.0)
382            .build();
383        assert!(r.is_err());
384
385        // should build valid instance when all rules pass...
386        let score = Score::builder()
387            .scaled(0.8)?
388            .raw(5.0)
389            .min(0.0)
390            .max(10.0)
391            .build()?;
392        assert_eq!(score.scaled.unwrap(), 0.8);
393        assert_eq!(score.raw.unwrap(), 5.0);
394        assert_eq!(score.min.unwrap(), 0.0);
395        assert_eq!(score.max.unwrap(), 10.0);
396
397        Ok(())
398    }
399}