Skip to main content

prelude_xml_parser/native/
user_native.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "python")]
6use pyo3::{exceptions::PyValueError, prelude::*, types::PyDict};
7
8pub use crate::native::common::{
9    Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
10};
11use quick_xml::events::BytesStart;
12
13use crate::native::deserializers::{
14    deserialize_empty_string_as_none, optional_string, required_attribute, visit_attributes,
15};
16
17#[cfg(not(feature = "python"))]
18#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20pub struct User {
21    #[serde(rename = "uniqueId")]
22    #[serde(alias = "@uniqueId")]
23    #[serde(alias = "uniqueId")]
24    pub unique_id: String,
25
26    #[serde(rename = "lastLanguage")]
27    #[serde(alias = "@lastLanguage")]
28    #[serde(alias = "lastLanguage")]
29    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
30    pub last_language: Option<String>,
31    #[serde(rename = "creator")]
32    #[serde(alias = "@creator")]
33    #[serde(alias = "creator")]
34    pub creator: String,
35    #[serde(rename = "numberOfForms")]
36    #[serde(alias = "@numberOfForms")]
37    #[serde(alias = "numberOfForms")]
38    pub number_of_forms: usize,
39
40    #[serde(alias = "form")]
41    pub forms: Option<Arc<Vec<Form>>>,
42}
43
44#[cfg(not(feature = "python"))]
45impl User {
46    pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
47        let mut unique_id: Option<&str> = None;
48        let mut last_language = "";
49        let mut creator: Option<&str> = None;
50        let mut number_of_forms = "";
51
52        visit_attributes(e, |key, attr| match key {
53            b"uniqueId" => unique_id = Some(attr),
54            b"lastLanguage" => last_language = attr,
55            b"creator" => creator = Some(attr),
56            b"numberOfForms" => number_of_forms = attr,
57            _ => {}
58        })?;
59
60        Ok(User {
61            unique_id: required_attribute(unique_id, "uniqueId")?,
62            last_language: optional_string(last_language),
63            creator: required_attribute(creator, "creator")?,
64            number_of_forms: number_of_forms.parse().unwrap_or(0),
65            forms: None,
66        })
67    }
68
69    pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
70        self.forms = if forms.is_empty() {
71            None
72        } else {
73            Some(Arc::new(forms))
74        };
75    }
76}
77
78#[cfg(feature = "python")]
79#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
80#[serde(rename_all = "camelCase")]
81#[pyclass(skip_from_py_object)]
82pub struct User {
83    #[serde(rename = "uniqueId")]
84    #[serde(alias = "@uniqueId")]
85    #[serde(alias = "uniqueId")]
86    pub unique_id: String,
87
88    #[serde(rename = "lastLanguage")]
89    #[serde(alias = "@lastLanguage")]
90    #[serde(alias = "lastLanguage")]
91    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
92    pub last_language: Option<String>,
93    #[serde(rename = "creator")]
94    #[serde(alias = "@creator")]
95    #[serde(alias = "creator")]
96    pub creator: String,
97    #[serde(rename = "numberOfForms")]
98    #[serde(alias = "@numberOfForms")]
99    #[serde(alias = "numberOfForms")]
100    pub number_of_forms: usize,
101
102    #[serde(alias = "form")]
103    pub forms: Option<Arc<Vec<Form>>>,
104}
105
106#[cfg(feature = "python")]
107impl User {
108    pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
109        let mut unique_id: Option<&str> = None;
110        let mut last_language = "";
111        let mut creator: Option<&str> = None;
112        let mut number_of_forms = "";
113
114        visit_attributes(e, |key, attr| match key {
115            b"uniqueId" => unique_id = Some(attr),
116            b"lastLanguage" => last_language = attr,
117            b"creator" => creator = Some(attr),
118            b"numberOfForms" => number_of_forms = attr,
119            _ => {}
120        })?;
121
122        Ok(User {
123            unique_id: required_attribute(unique_id, "uniqueId")?,
124            last_language: optional_string(last_language),
125            creator: required_attribute(creator, "creator")?,
126            number_of_forms: number_of_forms.parse().unwrap_or(0),
127            forms: None,
128        })
129    }
130
131    pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
132        self.forms = if forms.is_empty() {
133            None
134        } else {
135            Some(Arc::new(forms))
136        };
137    }
138}
139
140#[cfg(feature = "python")]
141#[pymethods]
142impl User {
143    #[getter]
144    fn unique_id(&self) -> PyResult<String> {
145        Ok(self.unique_id.clone())
146    }
147
148    #[getter]
149    fn last_language(&self) -> PyResult<Option<String>> {
150        Ok(self.last_language.clone())
151    }
152
153    #[getter]
154    fn creator(&self) -> PyResult<String> {
155        Ok(self.creator.clone())
156    }
157
158    #[getter]
159    fn number_of_forms(&self) -> PyResult<usize> {
160        Ok(self.number_of_forms)
161    }
162
163    #[getter]
164    fn forms(&self) -> PyResult<Option<Vec<Form>>> {
165        Ok(self.forms.as_deref().cloned())
166    }
167
168    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
169        let dict = PyDict::new(py);
170        dict.set_item("unique_id", &self.unique_id)?;
171        dict.set_item("last_language", &self.last_language)?;
172        dict.set_item("creator", &self.creator)?;
173        dict.set_item("number_of_forms", self.number_of_forms)?;
174
175        let mut form_dicts = Vec::new();
176        if let Some(forms) = &self.forms {
177            for form in forms.iter() {
178                let form_dict = form.to_dict(py)?;
179                form_dicts.push(form_dict);
180            }
181            dict.set_item("forms", form_dicts)?;
182        } else {
183            dict.set_item("forms", py.None())?;
184        }
185
186        Ok(dict)
187    }
188}
189
190#[cfg(not(feature = "python"))]
191/// Contains the information from the Prelude native user XML.
192#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
193#[serde(rename_all = "camelCase")]
194pub struct UserNative {
195    #[serde(default)]
196    pub export: Option<Export>,
197
198    #[serde(alias = "user")]
199    pub users: Vec<User>,
200}
201
202#[cfg(not(feature = "python"))]
203impl UserNative {
204    /// Convert to a JSON string
205    ///
206    /// # Example
207    ///
208    /// ```
209    /// use std::path::Path;
210    ///
211    /// use prelude_xml_parser::parse_user_native_file;
212    ///
213    /// let file_path = Path::new("tests/assets/user_native_small.xml");
214    /// let native = parse_user_native_file(&file_path).unwrap();
215    /// let json = native.to_json().unwrap();
216    /// // Verify it's valid JSON and contains the expected user data
217    /// assert!(json.contains("uniqueId\":\"1691421275437\""));
218    /// assert!(json.contains("\"value\":\"jazz@artemis.com\""));
219    /// ```
220    pub fn to_json(&self) -> serde_json::Result<String> {
221        let json = serde_json::to_string(&self)?;
222
223        Ok(json)
224    }
225}
226
227#[cfg(feature = "python")]
228/// Contains the information from the Prelude native user XML.
229#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
230#[serde(rename_all = "camelCase")]
231#[pyclass(get_all, skip_from_py_object)]
232pub struct UserNative {
233    #[serde(default)]
234    pub export: Option<Export>,
235
236    #[serde(alias = "user")]
237    pub users: Vec<User>,
238}
239
240#[cfg(feature = "python")]
241#[pymethods]
242impl UserNative {
243    #[getter]
244    fn export(&self) -> PyResult<Option<Export>> {
245        Ok(self.export.clone())
246    }
247
248    #[getter]
249    fn users(&self) -> PyResult<Vec<User>> {
250        Ok(self.users.clone())
251    }
252
253    /// Convert the class instance to a dictionary
254    fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
255        let dict = PyDict::new(py);
256        let mut user_dicts = Vec::new();
257        for user in &self.users {
258            let user_dict = user.to_dict(py)?;
259            user_dicts.push(user_dict);
260        }
261        match &self.export {
262            Some(export) => dict.set_item("export", export.to_dict(py)?)?,
263            None => dict.set_item("export", py.None())?,
264        }
265        dict.set_item("users", user_dicts)?;
266        Ok(dict)
267    }
268
269    /// Convert the class instance to a JSON string
270    fn to_json(&self) -> PyResult<String> {
271        serde_json::to_string(&self)
272            .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use insta::assert_yaml_snapshot;
280
281    #[test]
282    fn deserialize_user_native_json() {
283        let json_str = r#"{
284    "users": [
285        {
286            "uniqueId": "1691421275437",
287            "lastLanguage": null,
288            "creator": "Paul Sanders(1681162687395)",
289            "numberOfForms": 1,
290            "forms": [
291                {
292                    "name": "form.name.demographics",
293                    "lastModified": "2023-08-07T15:15:41Z",
294                    "whoLastModifiedName": "Paul Sanders",
295                    "whoLastModifiedRole": "Project Manager",
296                    "whenCreated": 1691421341578,
297                    "hasErrors": false,
298                    "hasWarnings": false,
299                    "locked": false,
300                    "user": null,
301                    "dateTimeChanged": null,
302                    "formTitle": "User Demographics",
303                    "formIndex": 1,
304                    "formGroup": null,
305                    "formState": "In-Work",
306                    "states": [
307                        {
308                            "value": "form.state.in.work",
309                            "signer": "Paul Sanders - Project Manager",
310                            "signerUniqueId": "1681162687395",
311                            "dateSigned": "2023-08-07T15:15:41Z"
312                        }
313                    ],
314                    "categories": [
315                        {
316                            "name": "demographics",
317                            "categoryType": "normal",
318                            "highestIndex": 0,
319                            "fields": [
320                                {
321                                    "name": "address",
322                                    "fieldType": "text",
323                                    "dataType": "string",
324                                    "errorCode": "undefined",
325                                    "whenCreated": "2024-01-12T20:14:09Z",
326                                    "keepHistory": true,
327                                    "entries": null
328                                },
329                                {
330                                    "name": "email",
331                                    "fieldType": "text",
332                                    "dataType": "string",
333                                    "errorCode": "undefined",
334                                    "whenCreated": "2023-08-07T15:15:41Z",
335                                    "keepHistory": true,
336                                    "entries": [
337                                        {
338                                            "entryId": "1",
339                                            "value": {
340                                                "by": "Paul Sanders",
341                                                "byUniqueId": "1681162687395",
342                                                "role": "Project Manager",
343                                                "when": "2023-08-07T15:15:41Z",
344                                                "value": "jazz@artemis.com"
345                                            },
346                                            "reason": null
347                                        }
348                                    ]
349                                }
350                            ]
351                        },
352                        {
353                            "name": "Administrative",
354                            "categoryType": "normal",
355                            "highestIndex": 0,
356                            "fields": [
357                                {
358                                    "name": "study_assignment",
359                                    "fieldType": "text",
360                                    "dataType": null,
361                                    "errorCode": "undefined",
362                                    "whenCreated": "2023-08-07T15:15:41Z",
363                                    "keepHistory": true,
364                                    "entries": [
365                                        {
366                                            "entryId": "1",
367                                            "value": {
368                                                "by": "set from calculation",
369                                                "byUniqueId": null,
370                                                "role": "System",
371                                                "when": "2023-08-07T15:15:41Z",
372                                                "value": "On 07-Aug-2023 10:15 -0500, Paul Sanders assigned user from another study"
373                                            },
374                                            "reason": {
375                                                "by": "set from calculation",
376                                                "byUniqueId": null,
377                                                "role": "System",
378                                                "when": "2023-08-07T15:15:41Z",
379                                                "value": "calculated value"
380                                            }
381                                        }
382                                    ]
383                                }
384                            ]
385                        }
386                    ]
387                }
388            ]
389        }
390    ]
391}
392
393        "#;
394
395        let result: UserNative = serde_json::from_str(json_str).unwrap();
396
397        assert_yaml_snapshot!(result);
398    }
399}