xapi_rs/data/
mod.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3#![warn(missing_docs)]
4
5mod about;
6mod account;
7mod activity;
8mod activity_definition;
9mod actor;
10mod agent;
11mod attachment;
12mod canonical;
13mod ci_string;
14mod context;
15mod context_activities;
16mod context_agent;
17mod context_group;
18mod data_error;
19mod duration;
20mod email_address;
21mod extensions;
22mod fingerprint;
23mod format;
24mod group;
25mod interaction_component;
26mod interaction_type;
27mod language_map;
28mod language_tag;
29mod multi_lingual;
30mod object_type;
31mod person;
32mod result;
33mod score;
34mod statement;
35mod statement_ids;
36mod statement_object;
37mod statement_ref;
38mod statement_result;
39pub(crate) mod statement_type;
40mod sub_statement;
41mod sub_statement_object;
42mod timestamp;
43mod validate;
44mod verb;
45mod version;
46
47use crate::emit_error;
48pub use about::*;
49pub use account::*;
50pub use activity::*;
51pub use activity_definition::*;
52pub use actor::*;
53pub use agent::*;
54pub use attachment::*;
55pub use canonical::*;
56use chrono::{DateTime, SecondsFormat, Utc};
57pub use ci_string::*;
58pub use context::*;
59pub use context_activities::*;
60pub use context_agent::*;
61pub use context_group::*;
62pub use data_error::DataError;
63pub use duration::*;
64pub use email_address::*;
65pub use extensions::{EMPTY_EXTENSIONS, Extensions};
66pub use fingerprint::*;
67pub use format::*;
68pub use group::*;
69pub use interaction_component::*;
70pub use interaction_type::*;
71pub use language_map::*;
72pub use language_tag::*;
73pub use multi_lingual::*;
74pub use object_type::*;
75pub use person::*;
76pub use result::*;
77pub use score::*;
78use serde::Serializer;
79use serde_json::Value;
80pub use statement::*;
81pub use statement_ids::*;
82pub use statement_object::*;
83pub use statement_ref::*;
84pub use statement_result::*;
85pub use sub_statement::*;
86pub use sub_statement_object::*;
87pub use timestamp::MyTimestamp;
88pub use validate::*;
89pub use verb::*;
90pub use version::*;
91
92/// Given `$map` (a [LanguageMap] dictionary) insert `$label` keyed by `$tag`
93/// creating the collection in the process if it was `None`.
94///
95/// Raise [LanguageTag][1] error if the `tag` is invalid.
96///
97/// Example
98/// ```rust
99/// # use core::result::Result;
100/// # use std::str::FromStr;
101/// # use xapi_rs::{MyError, add_language, LanguageMap, MyLanguageTag};
102/// # fn main() -> Result<(), MyError> {
103/// let mut greetings = None;
104/// let en = MyLanguageTag::from_str("en")?;
105/// add_language!(greetings, &en, "Hello");
106///
107/// assert_eq!(greetings.unwrap().get(&en).unwrap(), "Hello");
108/// #   Ok(())
109/// # }
110/// ```
111///
112/// [1]: crate::MyError#variant.LanguageTag
113/// [2]: https://crates.io/crates/language-tags
114#[macro_export]
115macro_rules! add_language {
116    ( $map: expr, $tag: expr, $label: expr ) => {
117        if !$label.trim().is_empty() {
118            let label = $label.trim();
119            if $map.is_none() {
120                $map = Some(LanguageMap::new());
121            }
122            let _ = $map.as_mut().unwrap().insert($tag, label);
123        }
124    };
125}
126
127/// Both [Agent] and [Group] have an `mbox` property which captures an _email
128/// address_. This macro eliminates duplication of the logic involved in (a)
129/// parsing an argument `$val` into a valid [EmailAddress][1], (b) raising a
130/// [DataError] if an error occurs, (b) assigning the result when successful
131/// to the appropriate field of the given `$builder` instance, and (c) resetting
132/// the other three IFI (Inverse Functional Identifier) fields to `None`.
133///
134/// [1]: [email_address::EmailAddress]
135#[macro_export]
136macro_rules! set_email {
137    ( $builder: expr, $val: expr ) => {
138        if $val.trim().is_empty() {
139            $crate::emit_error!(DataError::Validation(ValidationError::Empty("mbox".into())))
140        } else {
141            $builder._mbox = Some(if let Some(x) = $val.trim().strip_prefix("mailto:") {
142                MyEmailAddress::from_str(x)?
143            } else {
144                MyEmailAddress::from_str($val.trim())?
145            });
146            $builder._sha1sum = None;
147            $builder._openid = None;
148            $builder._account = None;
149            Ok($builder)
150        }
151    };
152}
153
154/// Given `dst` and `src` as two [BTreeMap][1]s wrapped in [Option], replace
155/// or augment `dst`' entries w/ `src`'s.
156///
157/// [1]: std::collections::BTreeMap
158#[macro_export]
159macro_rules! merge_maps {
160    ( $dst: expr, $src: expr ) => {
161        if $dst.is_none() {
162            if $src.is_some() {
163                let x = std::mem::take(&mut $src.unwrap());
164                let mut y = Some(x);
165                std::mem::swap($dst, &mut y);
166            }
167        } else if $src.is_some() {
168            let mut x = std::mem::take($dst.as_mut().unwrap());
169            let mut y = std::mem::take(&mut $src.unwrap());
170            x.append(&mut y);
171            let mut y = Some(x);
172            std::mem::swap($dst, &mut y);
173        }
174    };
175}
176
177/// Recursively check if a JSON Object contains 'null' values.
178fn check_for_nulls(val: &Value) -> Result<(), ValidationError> {
179    if let Some(obj) = val.as_object() {
180        // NOTE (rsn) 20241104 - from "4.2.1 Table Guidelines": "The LRS
181        // shall reject Statements with any null values (except inside
182        // extensions)."
183        for (k, v) in obj.iter() {
184            if v.is_null() {
185                emit_error!(ValidationError::ConstraintViolation(
186                    format!("Key '{k}' is 'null'").into()
187                ))
188            } else if k != "extensions" {
189                check_for_nulls(v)?
190            }
191        }
192    }
193    Ok(())
194}
195
196/// A Serializer implementation that ensures `stored` timestamps show
197/// milli-second precision.
198fn stored_ser<S>(this: &Option<DateTime<Utc>>, ser: S) -> Result<S::Ok, S::Error>
199where
200    S: Serializer,
201{
202    if this.is_some() {
203        let s = this
204            .as_ref()
205            .unwrap()
206            .to_rfc3339_opts(SecondsFormat::Millis, true);
207        ser.serialize_str(&s)
208    } else {
209        ser.serialize_none()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::MyError;
217    use std::str::FromStr;
218
219    #[test]
220    fn test_add_language() -> Result<(), MyError> {
221        let en = MyLanguageTag::from_str("en")?;
222        let mut lm = Some(LanguageMap::new());
223
224        add_language!(lm, &en, "it vorkz");
225        let binding = lm.unwrap();
226
227        let label = binding.get(&en);
228        assert!(label.is_some());
229        assert_eq!(label.unwrap(), "it vorkz");
230
231        Ok(())
232    }
233}