xapi_rs/data/
score.rs

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