xapi_data/lib.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2
3#![warn(missing_docs)]
4
5//! This LIBRARY consists of Rust bindings for the [IEEE Std 9274.1.1][101], IEEE Standard
6//! for Learning Technology— JavaScript Object Notation (JSON) Data Model Format and
7//! Representational State Transfer (RESTful) Web Service for Learner Experience Data Tracking
8//! and Access.
9//!
10//! The standard describes a JSON[^101] data model format and a RESTful[^102] Web Service
11//! API[^103] for communication between Activities experienced by an individual, group, or other
12//! entity and an LRS[^104]. The LRS is a system that exposes the RESTful Web Service API for
13//! the purpose of tracking and accessing experiential data, especially in learning and human
14//! performance.
15//!
16//! In this document, "xAPI" means the collection of published documents found
17//! [here](https://opensource.ieee.org/xapi>).
18//!
19//!
20//! ## The [`Validate`] trait
21//! Types defined in this library rely on [`serde`][102] and a couple of other libraries to
22//! deserialize xAPI data. Unit and Integration Tests for these types use the published
23//! [examples][103] to _partly_ ensure their correctness in at least they will consume
24//! the input stream and will produce instances of those types that can be later manipulated.
25//!
26//! I said partly b/c not all the _rules_ specified by the specifications can or are encoded
27//! in the techniques used for unmarshelling the input data stream.
28//!
29//! For example when xAPI specifies that a property must be an IRL the corresponding field is
30//! defined as an [`IriString`][104]. But an _IRL_ is not just an _IRI_! As xAPI (3. Definitions,
31//! acronyms, and abbreviations) states...
32//!
33//! > _...an IRL is an IRI that when translated into a URI (per the IRI to
34//! > URI rules), is a URL._
35//!
36//! Unfortunately the [`iri-string`][105] library does not offer out-of-the-box support for
37//! _IRLs_.
38//!
39//! Another example of the limitations of solely relying on [`serde`][102] for instantiating
40//! _correct_ types is the email address (`mbox`) property of [Agent]s and [Group]s. xAPI (4.2.2.1
41//! Actor) states that an `mbox` is a...
42//!
43//! > _mailto IRI. The required format is "mailto:email address"._
44//!
45//! For those reasons a [`Validate`] trait is defined and implemented to ensure that an instance
46//! of a type that implements this trait **is** valid, in that it satisfies the xAPI constraints,
47//! if + when it passes the `validate()` call.
48//!
49//! If a `validate()` call returns `None` when it shouldn't it's a bug.
50//!
51//! ## Equality and Equivalence - The [`Fingerprint`] trait
52//! There's the classical _Equality_ concept ubiquitous in software that deals w/ object Equality.
53//! That concept affects (in Rust) the `Hash`, `PartialEq` and `Eq` _Traits_. Ensuring that our
54//! xAPI Data Types implement those Traits mean they can be used as Keys in `HashMap`s and distinct
55//! elements in `HashSet`s.
56//!
57//! The xAPI describes (and relies) on a concept of _Equivalence_ that determines if two instances
58//! of the same Data Type (say [Statement]s) are equal. That _Equivalence_ is **different** from
59//! _Equality_ introduced earlier. So it's possible to have two [Statement]s that have different
60//! `hash`[^12] values yet are _equivalent_. Note though that if two [Statement]s (and more
61//! generally two instances of the same Data Type) are **equal** then they're also **equivalent**.
62//! In other words, _Equality_ implies _Equivalence_ but not the other way around.
63//!
64//! To satisfy _Equivalence_ between instances of a Data Type we introduce a [Fingerprint] Trait.
65//! The _required_ function of this Trait; i.e. [`fingerprint()`][crate::Fingerprint#fingerprint],
66//! is used to test for _Equivalence_ between two instances of the same Data Type.
67//!
68//! For most xAPI Data Types, both the `hash` and `fingerprint` functions yield the same result.
69//! When they differ the _Equivalence_ only considers properties the xAPI standard qualifies as
70//! **_preserving immutability requirements_**, for example for [Statement]s those are...
71//!
72//! * [Actor], except the ordering of Group members,
73//! * [Verb], except for `display` property,
74//! * [Object][107],
75//! * Duration, excluding precision beyond 0.01 second.
76//!
77//! Note though that even when they yield the same result, the implementations take into
78//! consideration the following constraints --and hence apply the required conversions before
79//! the test for equality...
80//!
81//! * Case-insensitive string comparisons when the property can be safely compared this way
82//! such as the `mbox` (email address) of an [Actor] but not the `name` of an [Account].
83//! * IRI Normalization by first splitting it into _Absolute_ and _Fragment_ parts then
84//! [_normalizing_][108] the _Absolute_ part before hashing the two in sequence.
85//!
86//!
87//! ## The [`Canonical`] trait
88//! xAPI requires LRS implementations to sometimes produce results in _canonical_ form --See
89//! [Language Filtering Requirements for Canonical Format Statements][109] for example.
90//!
91//! This trait defines a method implemented by types required to produce such format.
92//!
93//!
94//! ## Getters, Builders and Setters
95//! Once a type is instantiated, access to any of its fields --sometimes referred to in the
96//! documentation as _properties_ using the _camel case_ form mostly used in xAPI-- is done
97//! through methods that mirror the Rust field names of the structures representing said types.
98//!
99//! For example the _homePage_ property of an [Account] is obtained by calling the method
100//! `home_page()` of an [Account] instance which returns a reference to the IRI string as
101//! `&IriStr`.
102//!
103//! Sometimes however it is convenient to access the field as another type. Using the same
104//! example as above, the [Account] implementation offers a `home_page_as_str()` which returns
105//! a reference to the same field as `&str`.
106//!
107//! This pattern is generalized thoughtout this library.
108//!
109//! The library so far, except for rare use-cases, does NOT offer setters for any type field.
110//! Creating new instances of types by hand --as opposed to deserializing (from the wire)-- is
111//! done by (a) instantiating a _Builder_ for a type, (b) calling the _Builder_ setters (using
112//! the same field names as those of the to-be built type) to set the desired values, and when
113//! ready, (c) calling the `build()` method.
114//!
115//! _Builders_ signal the occurrence of errors by returning a `Result` w/ the error part being
116//! a [DataError] instance. Here's an example...
117//!
118//! ```rust
119//! # use core::result::Result;
120//! # use xapi_data::{Account, DataError};
121//! # fn dummy() -> Result<(), DataError> {
122//! let act = Account::builder()
123//! .home_page("https://inter.net/login")?
124//! .name("example")?
125//! .build()?;
126//! // ...
127//! assert_eq!(act.home_page_as_str(), "https://inter.net/login");
128//! assert_eq!(act.name(), "example");
129//! # Ok(())
130//! # }
131//! ```
132//!
133//!
134//! ## Naming
135//! Naming properties in xAPI _Objects_ is inconsistent. Sometimes the singular form is used
136//! to refer to a collection of items; e.g. _member_ instead of _members_ when referring to a
137//! [Group]'s list of [Agent]s. In other places the plural form is correctly used; e.g.
138//! [_attachments_][Attachment] in a [SubStatement], or [_extensions_][Extensions] everywhere
139//! it's referenced.
140//!
141//! We tried to be consistent in naming the fields of the corresponding types while ensuring that
142//! their serialization to, and deserialization from, streams respect the label assigned to them
143//! in xAPI and backed by the accompanying examples. So to access a [Group]'s [Agent]s one would
144//! call `members()`. To add an [Agent] to a [Group] one would call `member()` on a [GroupBuilder].
145//!
146//! [101]: https://opensource.ieee.org/xapi/xapi-base-standard-documentation
147//! [102]: https://crates.io/crates/serde
148//! [103]: https://opensource.ieee.org/xapi/xapi-base-standard-examples
149//! [104]: https://docs.rs/iri-string/0.7.2/iri_string/types/type.IriString.html
150//! [105]: https://crates.io/crates/iri-string
151//! [106]: https://dotat.at/tmp/ISO_8601-2004_E.pdf
152//! [107]: crate::StatementObject
153//! [108]: <https://www.rfc-editor.org/rfc/rfc3987#section-5>
154//! [109]: https://opensource.ieee.org/xapi/xapi-base-standard-documentation/-/blob/main/9274.1.1%20xAPI%20Base%20Standard%20for%20LRSs.md#language-filtering-requirements-for-canonical-format-statements
155//!
156//! [^101]: JSON: JavaScript Object Notation.
157//! [^102]: REST: Representational State Transfer.
158//! [^103]: API: Application Programming Interface.
159//! [^104]: LRS: Learning Record Store.
160//! [^10]: Durations in [ISO 8601:2004(E)][106] sections 4.4.3.2 and 4.4.3.3.
161//! [^12]: Just to be clear, `hash` here means the result of computing a message digest over the non-null values of an object's field(s).
162//!
163
164mod about;
165mod account;
166mod activity;
167mod activity_definition;
168mod actor;
169mod agent;
170mod attachment;
171mod canonical;
172mod ci_string;
173mod context;
174mod context_activities;
175mod context_agent;
176mod context_group;
177mod duration;
178mod email_address;
179mod error;
180mod extensions;
181mod fingerprint;
182mod format;
183mod group;
184mod interaction_component;
185mod interaction_type;
186mod language_map;
187mod language_tag;
188mod multi_lingual;
189mod object_type;
190mod person;
191pub mod prelude;
192mod result;
193mod score;
194mod statement;
195mod statement_ids;
196mod statement_object;
197mod statement_ref;
198mod statement_type;
199mod statement_result;
200mod sub_statement;
201mod sub_statement_object;
202mod timestamp;
203mod validate;
204mod verb;
205mod version;
206
207pub use prelude::*;
208
209use chrono::{DateTime, SecondsFormat, Utc};
210use serde::Serializer;
211use serde_json::Value;
212
213/// Given `$map` (a [LanguageMap] dictionary) insert `$label` keyed by `$tag`
214/// creating the collection in the process if it was `None`.
215///
216/// Raise [LanguageTag][1] error if the `tag` is invalid.
217///
218/// Example
219/// ```rust
220/// # use core::result::Result;
221/// # use std::str::FromStr;
222/// # use xapi_data::{DataError, add_language, LanguageMap, MyLanguageTag};
223/// # fn main() -> Result<(), DataError> {
224/// let mut greetings = None;
225/// let en = MyLanguageTag::from_str("en")?;
226/// add_language!(greetings, &en, "Hello");
227///
228/// assert_eq!(greetings.unwrap().get(&en).unwrap(), "Hello");
229/// # Ok(())
230/// # }
231/// ```
232///
233/// [1]: crate::DataError#variant.LanguageTag
234#[macro_export]
235macro_rules! add_language {
236 ( $map: expr, $tag: expr, $label: expr ) => {
237 if !$label.trim().is_empty() {
238 let label = $label.trim();
239 if $map.is_none() {
240 $map = Some(LanguageMap::new());
241 }
242 let _ = $map.as_mut().unwrap().insert($tag, label);
243 }
244 };
245}
246
247/// Both [Agent] and [Group] have an `mbox` property which captures an _email
248/// address_. This macro eliminates duplication of the logic involved in (a)
249/// parsing an argument `$val` into a valid [EmailAddress][1], (b) raising a
250/// [DataError] if an error occurs, (b) assigning the result when successful
251/// to the appropriate field of the given `$builder` instance, and (c) resetting
252/// the other three IFI (Inverse Functional Identifier) fields to `None`.
253///
254/// [1]: [email_address::EmailAddress]
255#[macro_export]
256macro_rules! set_email {
257 ( $builder: expr, $val: expr ) => {
258 if $val.trim().is_empty() {
259 $crate::emit_error!(DataError::Validation(ValidationError::Empty("mbox".into())))
260 } else {
261 $builder._mbox = Some(if let Some(x) = $val.trim().strip_prefix("mailto:") {
262 MyEmailAddress::from_str(x)?
263 } else {
264 MyEmailAddress::from_str($val.trim())?
265 });
266 $builder._sha1sum = None;
267 $builder._openid = None;
268 $builder._account = None;
269 Ok($builder)
270 }
271 };
272}
273
274/// Given `dst` and `src` as two [BTreeMap][1]s wrapped in [Option], replace
275/// or augment `dst`' entries w/ `src`'s.
276///
277/// [1]: std::collections::BTreeMap
278#[macro_export]
279macro_rules! merge_maps {
280 ( $dst: expr, $src: expr ) => {
281 if $dst.is_none() {
282 if let Some(mut z_src) = $src {
283 let x = std::mem::take(&mut z_src);
284 let mut y = Some(x);
285 std::mem::swap($dst, &mut y);
286 }
287 } else if $src.is_some() {
288 let mut x = std::mem::take($dst.as_mut().unwrap());
289 let mut y = std::mem::take(&mut $src.unwrap());
290 x.append(&mut y);
291 let mut y = Some(x);
292 std::mem::swap($dst, &mut y);
293 }
294 };
295}
296
297/// Recursively check if a JSON Object contains 'null' values.
298fn check_for_nulls(val: &Value) -> Result<(), ValidationError> {
299 if let Some(obj) = val.as_object() {
300 // NOTE (rsn) 20241104 - from "4.2.1 Table Guidelines": "The LRS
301 // shall reject Statements with any null values (except inside
302 // extensions)."
303 for (k, v) in obj.iter() {
304 if v.is_null() {
305 emit_error!(ValidationError::ConstraintViolation(
306 format!("Key '{k}' is 'null'").into()
307 ))
308 } else if k != "extensions" {
309 check_for_nulls(v)?
310 }
311 }
312 }
313 Ok(())
314}
315
316/// A Serializer implementation that ensures `stored` timestamps show
317/// milli-second precision.
318fn stored_ser<S>(this: &Option<DateTime<Utc>>, ser: S) -> Result<S::Ok, S::Error>
319where
320 S: Serializer,
321{
322 if this.is_some() {
323 let s = this
324 .as_ref()
325 .unwrap()
326 .to_rfc3339_opts(SecondsFormat::Millis, true);
327 ser.serialize_str(&s)
328 } else {
329 ser.serialize_none()
330 }
331}
332
333/// Generate a message (in the style of `format!` macro), log it at level
334/// _error_ and raise a [runtime error][crate::DataError#variant.Runtime].
335#[macro_export]
336macro_rules! runtime_error {
337 ( $( $arg: tt )* ) => {
338 {
339 let msg = std::fmt::format(core::format_args!($($arg)*));
340 tracing::error!("{}", msg);
341 return Err($crate::DataError::Runtime(msg.into()));
342 }
343 }
344}
345
346/// Log `$err` at level _error_ before returning it.
347#[macro_export]
348macro_rules! emit_error {
349 ( $err: expr ) => {{
350 tracing::error!("{}", $err);
351 return Err($err);
352 }};
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use std::str::FromStr;
359
360 #[test]
361 fn test_add_language() -> Result<(), DataError> {
362 let en = MyLanguageTag::from_str("en")?;
363 let mut lm = Some(LanguageMap::new());
364
365 add_language!(lm, &en, "it vorkz");
366 let binding = lm.unwrap();
367
368 let label = binding.get(&en);
369 assert!(label.is_some());
370 assert_eq!(label.unwrap(), "it vorkz");
371
372 Ok(())
373 }
374}