xapi_rs/data/
extensions.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::data::{DataError, Fingerprint};
4use core::fmt;
5use iri_string::types::{IriStr, IriString};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::{
9    collections::BTreeMap,
10    hash::{Hash, Hasher},
11};
12
13/// [Extensions] are available as part of [Activity Definitions][1], as part
14/// of a [Statement's][2] `context` or `result` properties. In each case,
15/// they're intended to provide a natural way to extend those properties for
16/// some specialized use.
17///
18/// The contents of these [Extensions] might be something valuable to just one
19/// application, or it might be a convention used by an entire _Community of
20/// Practice_.
21///
22/// From [4.2.7 Additional Requirements for Data Types / Extension][3]:
23/// * The LRS shall reject any Statement where a key of an extensions map is
24///   not an IRI.
25/// * An LRS shall not reject a Statement based on the values of the extensions
26///   map.
27///
28/// [1]: crate::ActivityDefinition
29/// [2]: crate::Statement
30/// [3]: https://opensource.ieee.org/xapi/xapi-base-standard-documentation/-/blob/main/9274.1.1%20xAPI%20Base%20Standard%20for%20LRSs.md#extensions
31
32#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
33pub struct Extensions(BTreeMap<IriString, Value>);
34
35/// The empty [Extensions] singleton.
36pub const EMPTY_EXTENSIONS: Extensions = Extensions(BTreeMap::new());
37
38impl Extensions {
39    /// Construct an empty instance.
40    pub fn new() -> Self {
41        Extensions(BTreeMap::new())
42    }
43
44    /// Whether this is an empty collection (TRUE) or not (FALSE).
45    pub fn is_empty(&self) -> bool {
46        self.0.is_empty()
47    }
48
49    /// Return the [Value] associated w/ the given `key` if present in this
50    /// collection; `None` otherwise.
51    pub fn get(&self, key: &IriStr) -> Option<&Value> {
52        self.0.get(key)
53    }
54
55    /// Return the number of entries in the collection.
56    pub fn len(&self) -> usize {
57        self.0.len()
58    }
59
60    /// Returns TRUE if the collection contains a value for the given `key`.
61    /// Return FALSE otherwise.
62    pub fn contains_key(&self, key: &IriStr) -> bool {
63        self.0.contains_key(key)
64    }
65
66    /// Add a key-value pair to this collection.
67    pub fn add(&mut self, key_str: &str, v: &Value) -> Result<(), DataError> {
68        let iri = IriStr::new(key_str)?;
69        self.0.insert(iri.into(), v.to_owned());
70        Ok(())
71    }
72
73    /// Moves all elements from `other` into `self`, leaving `other` empty.
74    ///
75    /// If a key from `other` is already present in `self`, the respective
76    /// value from `self` will be overwritten with the respective value from
77    /// `other`.
78    pub fn append(&mut self, other: &mut Extensions) {
79        self.0.append(&mut other.0);
80    }
81}
82
83impl fmt::Display for Extensions {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        let mut vec = vec![];
86
87        if !self.0.is_empty() {
88            for (k, v) in self.0.iter() {
89                vec.push(format!("\"{k}\": {v}"))
90            }
91        }
92
93        let res = vec
94            .iter()
95            .map(|x| x.to_string())
96            .collect::<Vec<_>>()
97            .join(", ");
98        write!(f, "{{ {res} }}")
99    }
100}
101
102impl Fingerprint for Extensions {
103    fn fingerprint<H: Hasher>(&self, state: &mut H) {
104        self.0.hash(state)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_add() -> Result<(), DataError> {
114        const IRI: &str = "http://www.nowhere.net/togo";
115
116        let mut ext = Extensions::new();
117        assert_eq!(ext.len(), 0);
118
119        // try adding invalid arguments
120        let k = "aKey";
121        let v = serde_json::to_value("aValue").unwrap();
122        assert!(ext.add(k, &v).is_err());
123
124        // ...now w/ valid ones...
125        let faux = serde_json::to_value(false).unwrap();
126        assert!(ext.add(IRI, &faux).is_ok());
127
128        // make sure it's there...
129        let iri = IriStr::new(IRI).unwrap();
130        assert_eq!(ext.get(iri), Some(&faux));
131
132        Ok(())
133    }
134}