Skip to main content

prelude_xml_parser/native/
user_native.rs

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