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