Skip to main content

prelude_xml_parser/native/
subject_native.rs

1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4
5#[cfg(feature = "python")]
6use pyo3::{
7    exceptions::PyValueError,
8    prelude::*,
9    types::{PyDateTime, PyDict},
10};
11
12use quick_xml::events::BytesStart;
13
14use crate::native::deserializers::{
15    checked_datetime, deserialize_empty_string_as_none_datetime, optional_string,
16    required_attribute, visit_attributes,
17};
18
19#[cfg(feature = "python")]
20use crate::native::deserializers::{deserialize_empty_string_as_none, to_py_datetime};
21
22use serde::{Deserialize, Serialize};
23
24pub use crate::native::common::{
25    Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
26};
27
28#[cfg(not(feature = "python"))]
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Patient {
31    #[serde(rename = "patientId")]
32    pub patient_id: String,
33    #[serde(rename = "uniqueId")]
34    pub unique_id: String,
35    #[serde(rename = "whenCreated")]
36    pub when_created: Option<DateTime<Utc>>,
37    #[serde(rename = "passwordChangeDate")]
38    #[serde(
39        default,
40        deserialize_with = "deserialize_empty_string_as_none_datetime"
41    )]
42    pub password_change_date: Option<DateTime<Utc>>,
43    pub creator: String,
44    #[serde(rename = "siteName")]
45    pub site_name: String,
46    #[serde(rename = "siteUniqueId")]
47    pub site_unique_id: String,
48    #[serde(rename = "lastLanguage")]
49    pub last_language: Option<String>,
50    #[serde(rename = "numberOfForms")]
51    pub number_of_forms: usize,
52    pub forms: Option<Arc<Vec<Form>>>,
53}
54
55impl Patient {
56    pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
57        let mut patient_id: Option<&str> = None;
58        let mut unique_id: Option<&str> = None;
59        let mut when_created = "";
60        let mut password_change_date = "";
61        let mut creator: Option<&str> = None;
62        let mut site_name: Option<&str> = None;
63        let mut site_unique_id: Option<&str> = None;
64        let mut last_language = "";
65        let mut number_of_forms = "";
66
67        visit_attributes(e, |key, attr| match key {
68            b"patientId" => patient_id = Some(attr),
69            b"uniqueId" => unique_id = Some(attr),
70            b"whenCreated" => when_created = attr,
71            b"passwordChangeDate" => password_change_date = attr,
72            b"creator" => creator = Some(attr),
73            b"siteName" => site_name = Some(attr),
74            b"siteUniqueId" => site_unique_id = Some(attr),
75            b"lastLanguage" => last_language = attr,
76            b"numberOfForms" => number_of_forms = attr,
77            _ => {}
78        })?;
79
80        Ok(Patient {
81            patient_id: required_attribute(patient_id, "patientId")?,
82            unique_id: required_attribute(unique_id, "uniqueId")?,
83            when_created: checked_datetime(when_created)?,
84            password_change_date: checked_datetime(password_change_date)?,
85            creator: required_attribute(creator, "creator")?,
86            site_name: required_attribute(site_name, "siteName")?,
87            site_unique_id: required_attribute(site_unique_id, "siteUniqueId")?,
88            last_language: optional_string(last_language),
89            number_of_forms: number_of_forms.parse().unwrap_or(0),
90            forms: None,
91        })
92    }
93
94    pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
95        self.forms = if forms.is_empty() {
96            None
97        } else {
98            Some(Arc::new(forms))
99        };
100    }
101}
102
103#[cfg(feature = "python")]
104#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
105#[serde(rename_all = "camelCase")]
106#[pyclass(skip_from_py_object)]
107pub struct Patient {
108    #[serde(rename = "patientId")]
109    #[serde(alias = "@patientId")]
110    #[serde(alias = "patientId")]
111    pub patient_id: String,
112    #[serde(rename = "uniqueId")]
113    #[serde(alias = "@uniqueId")]
114    #[serde(alias = "uniqueId")]
115    pub unique_id: String,
116    #[serde(rename = "whenCreated")]
117    #[serde(alias = "@whenCreated")]
118    #[serde(alias = "whenCreated")]
119    pub when_created: Option<DateTime<Utc>>,
120    #[serde(rename = "passwordChangeDate")]
121    #[serde(alias = "@passwordChangeDate")]
122    #[serde(alias = "passwordChangeDate")]
123    #[serde(
124        default,
125        deserialize_with = "deserialize_empty_string_as_none_datetime"
126    )]
127    pub password_change_date: Option<DateTime<Utc>>,
128
129    #[serde(rename = "creator")]
130    #[serde(alias = "@creator")]
131    #[serde(alias = "creator")]
132    pub creator: String,
133    #[serde(rename = "siteName")]
134    #[serde(alias = "@siteName")]
135    #[serde(alias = "siteName")]
136    pub site_name: String,
137    #[serde(rename = "siteUniqueId")]
138    #[serde(alias = "@siteUniqueId")]
139    #[serde(alias = "siteUniqueId")]
140    pub site_unique_id: String,
141
142    #[serde(rename = "lastLanguage")]
143    #[serde(alias = "@lastLanguage")]
144    #[serde(alias = "lastLanguage")]
145    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
146    pub last_language: Option<String>,
147
148    #[serde(rename = "numberOfForms")]
149    #[serde(alias = "@numberOfForms")]
150    #[serde(alias = "numberOfForms")]
151    pub number_of_forms: usize,
152
153    #[serde(alias = "form")]
154    pub forms: Option<Arc<Vec<Form>>>,
155}
156
157#[cfg(feature = "python")]
158#[pymethods]
159impl Patient {
160    #[getter]
161    fn patient_id(&self) -> PyResult<String> {
162        Ok(self.patient_id.clone())
163    }
164
165    #[getter]
166    fn unique_id(&self) -> PyResult<String> {
167        Ok(self.unique_id.clone())
168    }
169
170    #[getter]
171    fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
172        self.when_created
173            .as_ref()
174            .map(|dt| to_py_datetime(py, dt))
175            .transpose()
176    }
177
178    #[getter]
179    fn password_change_date<'py>(
180        &self,
181        py: Python<'py>,
182    ) -> PyResult<Option<Bound<'py, PyDateTime>>> {
183        self.password_change_date
184            .as_ref()
185            .map(|dt| to_py_datetime(py, dt))
186            .transpose()
187    }
188
189    #[getter]
190    fn creator(&self) -> PyResult<String> {
191        Ok(self.creator.clone())
192    }
193
194    #[getter]
195    fn site_name(&self) -> PyResult<String> {
196        Ok(self.site_name.clone())
197    }
198
199    #[getter]
200    fn site_unique_id(&self) -> PyResult<String> {
201        Ok(self.site_unique_id.clone())
202    }
203
204    #[getter]
205    fn last_language(&self) -> PyResult<Option<String>> {
206        Ok(self.last_language.clone())
207    }
208
209    #[getter]
210    fn number_of_forms(&self) -> PyResult<usize> {
211        Ok(self.number_of_forms)
212    }
213
214    #[getter]
215    fn forms(&self) -> PyResult<Option<Vec<Form>>> {
216        Ok(self.forms.as_deref().cloned())
217    }
218
219    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
220        let dict = PyDict::new(py);
221        dict.set_item("patient_id", &self.patient_id)?;
222        dict.set_item("unique_id", &self.unique_id)?;
223        dict.set_item(
224            "when_created",
225            self.when_created
226                .as_ref()
227                .map(|dt| to_py_datetime(py, dt))
228                .transpose()?,
229        )?;
230        dict.set_item(
231            "password_change_date",
232            self.password_change_date
233                .as_ref()
234                .map(|dt| to_py_datetime(py, dt))
235                .transpose()?,
236        )?;
237        dict.set_item("creator", &self.creator)?;
238        dict.set_item("site_name", &self.site_name)?;
239        dict.set_item("site_unique_id", &self.site_unique_id)?;
240        dict.set_item("last_language", &self.last_language)?;
241        dict.set_item("number_of_forms", self.number_of_forms)?;
242
243        let mut form_dicts = Vec::new();
244        if let Some(forms) = &self.forms {
245            for form in forms.iter() {
246                let form_dict = form.to_dict(py)?;
247                form_dicts.push(form_dict);
248            }
249            dict.set_item("forms", form_dicts)?;
250        } else {
251            dict.set_item("forms", py.None())?;
252        }
253
254        Ok(dict)
255    }
256}
257
258#[cfg(not(feature = "python"))]
259/// Contains the information from the Prelude native subject XML.
260#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
261pub struct SubjectNative {
262    #[serde(default)]
263    pub export: Option<Export>,
264    pub patients: Vec<Patient>,
265}
266
267#[cfg(not(feature = "python"))]
268impl SubjectNative {
269    /// Convert to a JSON string
270    ///
271    /// # Example
272    ///
273    /// ```
274    /// use std::path::Path;
275    ///
276    /// use prelude_xml_parser::parse_subject_native_file;
277    ///
278    /// let file_path = Path::new("tests/assets/subject_native_small.xml");
279    /// let native = parse_subject_native_file(&file_path).unwrap();
280    /// let json = native.to_json().unwrap();
281    /// // Verify the JSON contains expected patient data
282    /// assert!(json.contains("ABC-001"));
283    /// assert!(json.contains("Paul Sanders"));
284    /// assert!(json.contains("Labrador"));
285    /// ```
286    pub fn to_json(&self) -> serde_json::Result<String> {
287        let json = serde_json::to_string(&self)?;
288
289        Ok(json)
290    }
291}
292
293#[cfg(feature = "python")]
294/// Contains the information from the Prelude native subject XML.
295#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
296#[serde(rename_all = "camelCase")]
297#[pyclass(get_all, skip_from_py_object)]
298pub struct SubjectNative {
299    #[serde(default)]
300    pub export: Option<Export>,
301
302    #[serde(alias = "patient")]
303    pub patients: Vec<Patient>,
304}
305
306#[cfg(feature = "python")]
307#[pymethods]
308impl SubjectNative {
309    #[getter]
310    fn export(&self) -> PyResult<Option<Export>> {
311        Ok(self.export.clone())
312    }
313
314    #[getter]
315    fn patients(&self) -> PyResult<Vec<Patient>> {
316        Ok(self.patients.clone())
317    }
318
319    /// Convert the class instance to a dictionary
320    fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
321        let dict = PyDict::new(py);
322        let mut patient_dicts = Vec::new();
323        for patient in &self.patients {
324            let patient_dict = patient.to_dict(py)?;
325            patient_dicts.push(patient_dict);
326        }
327        match &self.export {
328            Some(export) => dict.set_item("export", export.to_dict(py)?)?,
329            None => dict.set_item("export", py.None())?,
330        }
331        dict.set_item("patients", patient_dicts)?;
332        Ok(dict)
333    }
334
335    /// Convert the class instance to a JSON string
336    fn to_json(&self) -> PyResult<String> {
337        serde_json::to_string(&self)
338            .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use insta::assert_yaml_snapshot;
345
346    use super::*;
347
348    #[test]
349    fn deserialize_subject_native_json() {
350        let json_str = r#"{
351    "patients": [
352        {
353            "patientId": "ABC-001",
354            "uniqueId": "1681574905819",
355            "whenCreated": "2023-04-15T16:09:02Z",
356            "creator": "Paul Sanders",
357            "siteName": "Some Site",
358            "siteUniqueId": "1681574834910",
359            "lastLanguage": "English",
360            "numberOfForms": 6,
361            "forms": [
362                {
363                    "name": "day.0.form.name.demographics",
364                    "lastModified": "2023-04-15T16:09:15Z",
365                    "whoLastModifiedName": "Paul Sanders",
366                    "whoLastModifiedRole": "Project Manager",
367                    "whenCreated": 1681574905839,
368                    "hasErrors": false,
369                    "hasWarnings": false,
370                    "locked": false,
371                    "user": null,
372                    "dateTimeChanged": null,
373                    "formTitle": "Demographics",
374                    "formIndex": 1,
375                    "formGroup": "Day 0",
376                    "formState": "In-Work",
377                    "states": [
378                        {
379                            "value": "form.state.in.work",
380                            "signer": "Paul Sanders - Project Manager",
381                            "signerUniqueId": "1681162687395",
382                            "dateSigned": "2023-04-15T16:09:02Z"
383                        }
384                    ],
385                    "categories": [
386                        {
387                            "name": "Demographics",
388                            "categoryType": "normal",
389                            "highestIndex": 0,
390                            "fields": [
391                                {
392                                    "name": "breed",
393                                    "fieldType": "combo-box",
394                                    "dataType": "string",
395                                    "errorCode": "valid",
396                                    "whenCreated": "2023-04-15T16:08:26Z",
397                                    "keepHistory": true,
398                                    "entries": [
399                                        {
400                                            "entryId": "1",
401                                            "value": {
402                                                "by": "Paul Sanders",
403                                                "byUniqueId": "1681162687395",
404                                                "role": "Project Manager",
405                                                "when": "2023-04-15T16:09:02Z",
406                                                "value": "Labrador"
407                                            },
408                                            "reason": null
409                                        }
410                                    ]
411                                }
412                            ]
413                        }
414                    ]
415                }
416            ]
417        },
418        {
419            "patientId": "DEF-002",
420            "uniqueId": "1681574905820",
421            "whenCreated": "2023-04-16T16:10:02Z",
422            "creator": "Wade Watts",
423            "siteName": "Another Site",
424            "siteUniqueId": "1681574834911",
425            "lastLanguage": null,
426            "numberOfForms": 8,
427            "forms": [
428                {
429                    "name": "day.0.form.name.demographics",
430                    "lastModified": "2023-04-16T16:10:15Z",
431                    "whoLastModifiedName": "Barney Rubble",
432                    "whoLastModifiedRole": "Technician",
433                    "whenCreated": 1681574905838,
434                    "hasErrors": false,
435                    "hasWarnings": false,
436                    "locked": false,
437                    "user": null,
438                    "dateTimeChanged": null,
439                    "formTitle": "Demographics",
440                    "formIndex": 1,
441                    "formGroup": "Day 0",
442                    "formState": "In-Work",
443                    "states": [
444                        {
445                            "value": "form.state.in.work",
446                            "signer": "Paul Sanders - Project Manager",
447                            "signerUniqueId": "1681162687395",
448                            "dateSigned": "2023-04-16T16:10:02Z"
449                        }
450                    ],
451                    "categories": [
452                        {
453                            "name": "Demographics",
454                            "categoryType": "normal",
455                            "highestIndex": 0,
456                            "fields": [
457                                {
458                                    "name": "breed",
459                                    "fieldType": "combo-box",
460                                    "dataType": "string",
461                                    "errorCode": "valid",
462                                    "whenCreated": "2023-04-15T16:08:26Z",
463                                    "keepHistory": true,
464                                    "entries": [
465                                        {
466                                            "entryId": "1",
467                                            "value": {
468                                                "by": "Paul Sanders",
469                                                "byUniqueId": "1681162687395",
470                                                "role": "Project Manager",
471                                                "when": "2023-04-15T16:09:02Z",
472                                                "value": "Labrador"
473                                            },
474                                            "reason": null
475                                        }
476                                    ]
477                                }
478                            ]
479                        }
480                    ]
481                }
482            ]
483        }
484    ]
485}
486        "#;
487
488        let result: SubjectNative = serde_json::from_str(json_str).unwrap();
489
490        assert_yaml_snapshot!(result);
491    }
492}