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}