Skip to main content

prelude_xml_parser/native/
site_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 serde::{Deserialize, Serialize};
13
14pub use crate::native::common::{
15    Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
16};
17
18use quick_xml::events::BytesStart;
19
20use crate::native::deserializers::{checked_datetime, required_attribute, visit_attributes};
21
22#[cfg(feature = "python")]
23use crate::native::deserializers::to_py_datetime;
24
25#[cfg(not(feature = "python"))]
26#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
27#[serde(rename_all = "camelCase")]
28pub struct Site {
29    #[serde(alias = "@name")]
30    #[serde(alias = "name")]
31    pub name: String,
32    #[serde(rename = "uniqueId")]
33    #[serde(alias = "@uniqueId")]
34    #[serde(alias = "uniqueId")]
35    pub unique_id: String,
36    #[serde(rename = "numberOfPatients")]
37    #[serde(alias = "@numberOfPatients")]
38    #[serde(alias = "numberOfPatients")]
39    pub number_of_patients: usize,
40    #[serde(rename = "countOfRandomizedPatients")]
41    #[serde(alias = "@countOfRandomizedPatients")]
42    #[serde(alias = "countOfRandomizedPatients")]
43    pub count_of_randomized_patients: usize,
44    #[serde(rename = "whenCreated")]
45    #[serde(alias = "@whenCreated")]
46    #[serde(alias = "whenCreated")]
47    pub when_created: Option<DateTime<Utc>>,
48    #[serde(alias = "@creator")]
49    #[serde(alias = "creator")]
50    pub creator: String,
51    #[serde(rename = "numberOfForms")]
52    #[serde(alias = "@numberOfForms")]
53    #[serde(alias = "numberOfForms")]
54    pub number_of_forms: usize,
55
56    #[serde(rename = "form")]
57    #[serde(alias = "form")]
58    pub forms: Option<Arc<Vec<Form>>>,
59}
60
61#[cfg(not(feature = "python"))]
62impl Site {
63    pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
64        let mut name: Option<&str> = None;
65        let mut unique_id: Option<&str> = None;
66        let mut number_of_patients = "";
67        let mut count_of_randomized_patients = "";
68        let mut when_created = "";
69        let mut creator: Option<&str> = None;
70        let mut number_of_forms = "";
71
72        visit_attributes(e, |key, attr| match key {
73            b"name" => name = Some(attr),
74            b"uniqueId" => unique_id = Some(attr),
75            b"numberOfPatients" => number_of_patients = attr,
76            b"countOfRandomizedPatients" => count_of_randomized_patients = attr,
77            b"whenCreated" => when_created = attr,
78            b"creator" => creator = Some(attr),
79            b"numberOfForms" => number_of_forms = attr,
80            _ => {}
81        })?;
82
83        Ok(Site {
84            name: required_attribute(name, "name")?,
85            unique_id: required_attribute(unique_id, "uniqueId")?,
86            number_of_patients: number_of_patients.parse().unwrap_or(0),
87            count_of_randomized_patients: count_of_randomized_patients.parse().unwrap_or(0),
88            when_created: checked_datetime(when_created)?,
89            creator: required_attribute(creator, "creator")?,
90            number_of_forms: number_of_forms.parse().unwrap_or(0),
91            forms: None,
92        })
93    }
94
95    pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
96        self.forms = if forms.is_empty() {
97            None
98        } else {
99            Some(Arc::new(forms))
100        };
101    }
102}
103
104#[cfg(feature = "python")]
105#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
106#[serde(rename_all = "camelCase")]
107#[pyclass(skip_from_py_object)]
108pub struct Site {
109    #[serde(alias = "@name")]
110    #[serde(alias = "name")]
111    pub name: String,
112    #[serde(rename = "uniqueId")]
113    #[serde(alias = "@uniqueId")]
114    #[serde(alias = "uniqueId")]
115    pub unique_id: String,
116    #[serde(rename = "numberOfPatients")]
117    #[serde(alias = "@numberOfPatients")]
118    #[serde(alias = "numberOfPatients")]
119    pub number_of_patients: usize,
120    #[serde(rename = "countOfRandomizedPatients")]
121    #[serde(alias = "@countOfRandomizedPatients")]
122    #[serde(alias = "countOfRandomizedPatients")]
123    pub count_of_randomized_patients: usize,
124    #[serde(rename = "whenCreated")]
125    #[serde(alias = "@whenCreated")]
126    #[serde(alias = "whenCreated")]
127    pub when_created: Option<DateTime<Utc>>,
128    #[serde(alias = "@creator")]
129    #[serde(alias = "creator")]
130    pub creator: String,
131    #[serde(rename = "numberOfForms")]
132    #[serde(alias = "@numberOfForms")]
133    #[serde(alias = "numberOfForms")]
134    pub number_of_forms: usize,
135
136    #[serde(rename = "form")]
137    #[serde(alias = "form")]
138    pub forms: Option<Arc<Vec<Form>>>,
139}
140
141#[cfg(feature = "python")]
142impl Site {
143    pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
144        let mut name: Option<&str> = None;
145        let mut unique_id: Option<&str> = None;
146        let mut number_of_patients = "";
147        let mut count_of_randomized_patients = "";
148        let mut when_created = "";
149        let mut creator: Option<&str> = None;
150        let mut number_of_forms = "";
151
152        visit_attributes(e, |key, attr| match key {
153            b"name" => name = Some(attr),
154            b"uniqueId" => unique_id = Some(attr),
155            b"numberOfPatients" => number_of_patients = attr,
156            b"countOfRandomizedPatients" => count_of_randomized_patients = attr,
157            b"whenCreated" => when_created = attr,
158            b"creator" => creator = Some(attr),
159            b"numberOfForms" => number_of_forms = attr,
160            _ => {}
161        })?;
162
163        Ok(Site {
164            name: required_attribute(name, "name")?,
165            unique_id: required_attribute(unique_id, "uniqueId")?,
166            number_of_patients: number_of_patients.parse().unwrap_or(0),
167            count_of_randomized_patients: count_of_randomized_patients.parse().unwrap_or(0),
168            when_created: checked_datetime(when_created)?,
169            creator: required_attribute(creator, "creator")?,
170            number_of_forms: number_of_forms.parse().unwrap_or(0),
171            forms: None,
172        })
173    }
174
175    pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
176        self.forms = if forms.is_empty() {
177            None
178        } else {
179            Some(Arc::new(forms))
180        };
181    }
182}
183
184#[cfg(feature = "python")]
185#[pymethods]
186impl Site {
187    #[getter]
188    fn name(&self) -> PyResult<String> {
189        Ok(self.name.clone())
190    }
191
192    #[getter]
193    fn unique_id(&self) -> PyResult<String> {
194        Ok(self.unique_id.clone())
195    }
196
197    #[getter]
198    fn number_of_patients(&self) -> PyResult<usize> {
199        Ok(self.number_of_patients)
200    }
201
202    #[getter]
203    fn count_of_randomized_patients(&self) -> PyResult<usize> {
204        Ok(self.count_of_randomized_patients)
205    }
206
207    #[getter]
208    fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
209        self.when_created
210            .as_ref()
211            .map(|dt| to_py_datetime(py, dt))
212            .transpose()
213    }
214
215    #[getter]
216    fn creator(&self) -> PyResult<String> {
217        Ok(self.creator.clone())
218    }
219
220    #[getter]
221    fn number_of_forms(&self) -> PyResult<usize> {
222        Ok(self.number_of_forms)
223    }
224
225    #[getter]
226    fn forms(&self) -> PyResult<Option<Vec<Form>>> {
227        Ok(self.forms.as_deref().cloned())
228    }
229
230    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
231        let dict = PyDict::new(py);
232        dict.set_item("name", &self.name)?;
233        dict.set_item("unique_id", &self.unique_id)?;
234        dict.set_item("number_of_patients", self.number_of_patients)?;
235        dict.set_item(
236            "count_of_randomized_patients",
237            self.count_of_randomized_patients,
238        )?;
239        dict.set_item(
240            "when_created",
241            self.when_created
242                .as_ref()
243                .map(|dt| to_py_datetime(py, dt))
244                .transpose()?,
245        )?;
246        dict.set_item("creator", &self.creator)?;
247        dict.set_item("number_of_forms", self.number_of_forms)?;
248
249        let mut form_dicts = Vec::new();
250        if let Some(forms) = &self.forms {
251            for form in forms.iter() {
252                let form_dict = form.to_dict(py)?;
253                form_dicts.push(form_dict);
254            }
255            dict.set_item("forms", form_dicts)?;
256        } else {
257            dict.set_item("forms", py.None())?;
258        }
259
260        Ok(dict)
261    }
262}
263
264#[cfg(not(feature = "python"))]
265/// Contains the information from the Prelude native site XML.
266#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
267#[serde(rename_all = "camelCase")]
268pub struct SiteNative {
269    #[serde(default)]
270    pub export: Option<Export>,
271
272    #[serde(alias = "site")]
273    pub sites: Vec<Site>,
274}
275
276#[cfg(not(feature = "python"))]
277impl SiteNative {
278    /// Convert to a JSON string
279    ///
280    /// # Example
281    ///
282    /// ```
283    /// use std::path::Path;
284    ///
285    /// use prelude_xml_parser::parse_site_native_file;
286    ///
287    /// let file_path = Path::new("tests/assets/site_native_small.xml");
288    /// let native = parse_site_native_file(&file_path).unwrap();
289    /// let json = native.to_json().unwrap();
290    /// println!("{json}");
291    /// // Verify the JSON contains expected site data
292    /// assert!(json.contains("Some Site"));
293    /// assert!(json.contains("Paul Sanders"));
294    /// assert!(json.contains("Some Company"));
295    /// ```
296    pub fn to_json(&self) -> serde_json::Result<String> {
297        let json = serde_json::to_string(&self)?;
298
299        Ok(json)
300    }
301}
302
303#[cfg(feature = "python")]
304/// Contains the information from the Prelude native site XML.
305#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
306#[serde(rename_all = "camelCase")]
307#[pyclass(get_all, skip_from_py_object)]
308pub struct SiteNative {
309    #[serde(default)]
310    pub export: Option<Export>,
311
312    #[serde(alias = "site")]
313    pub sites: Vec<Site>,
314}
315
316#[cfg(feature = "python")]
317#[pymethods]
318impl SiteNative {
319    #[getter]
320    fn export(&self) -> PyResult<Option<Export>> {
321        Ok(self.export.clone())
322    }
323
324    #[getter]
325    fn sites(&self) -> PyResult<Vec<Site>> {
326        Ok(self.sites.clone())
327    }
328
329    /// Convert the class instance to a dictionary
330    fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
331        let dict = PyDict::new(py);
332        let mut site_dicts = Vec::new();
333        for site in &self.sites {
334            let site_dict = site.to_dict(py)?;
335            site_dicts.push(site_dict);
336        }
337        match &self.export {
338            Some(export) => dict.set_item("export", export.to_dict(py)?)?,
339            None => dict.set_item("export", py.None())?,
340        }
341        dict.set_item("sites", site_dicts)?;
342        Ok(dict)
343    }
344
345    /// Convert the class instance to a JSON string
346    fn to_json(&self) -> PyResult<String> {
347        serde_json::to_string(&self)
348            .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use insta::assert_yaml_snapshot;
356
357    #[test]
358    fn deserialize_site_native_json() {
359        let json_str = r#"{
360  "sites": [
361    {
362      "name": "Some Site",
363      "uniqueId": "1681574834910",
364      "numberOfPatients": 4,
365      "countOfRandomizedPatients": 0,
366      "whenCreated": "2023-04-15T16:08:19Z",
367      "creator": "Paul Sanders",
368      "numberOfForms": 1,
369      "forms": [
370        {
371          "name": "demographic.form.name.site.demographics",
372          "lastModified": "2023-04-15T16:08:19Z",
373          "whoLastModifiedName": "Paul Sanders",
374          "whoLastModifiedRole": "Project Manager",
375          "whenCreated": 1681574834930,
376          "hasErrors": false,
377          "hasWarnings": false,
378          "locked": false,
379          "user": null,
380          "dateTimeChanged": null,
381          "formTitle": "Site Demographics",
382          "formIndex": 1,
383          "formGroup": "Demographic",
384          "formState": "In-Work",
385          "states": [
386            {
387              "value": "form.state.in.work",
388              "signer": "Paul Sanders - Project Manager",
389              "signerUniqueId": "1681162687395",
390              "dateSigned": "2023-04-15T16:08:19Z"
391            }
392          ],
393          "categories": [
394            {
395              "name": "Demographics",
396              "categoryType": "normal",
397              "highestIndex": 0,
398              "fields": [
399                {
400                  "name": "address",
401                  "fieldType": "text",
402                  "dataType": "string",
403                  "errorCode": "valid",
404                  "whenCreated": "2023-04-15T16:07:14Z",
405                  "keepHistory": true,
406                  "entry": null
407                },
408                {
409                  "name": "company",
410                  "fieldType": "text",
411                  "dataType": "string",
412                  "errorCode": "valid",
413                  "whenCreated": "2023-04-15T16:07:14Z",
414                  "keepHistory": true,
415                  "entries": [
416                    {
417                      "entryId": "1",
418                      "value": {
419                        "by": "Paul Sanders",
420                        "byUniqueId": "1681162687395",
421                        "role": "Project Manager",
422                        "when": "2023-04-15T16:08:19Z",
423                        "value": "Some Company"
424                      },
425                      "reason": null
426                    }
427                  ]
428                },
429                {
430                  "name": "site_code_name",
431                  "fieldType": "hidden",
432                  "dataType": "string",
433                  "errorCode": "valid",
434                  "whenCreated": "2023-04-15T16:07:14Z",
435                  "keepHistory": true,
436                  "entry": [
437                    {
438                      "entryId": "1",
439                      "value": {
440                        "by": "set from calculation",
441                        "byUniqueId": null,
442                        "role": "System",
443                        "when": "2023-04-15T16:08:19Z",
444                        "value": "ABC-Some Site"
445                      },
446                      "reason": {
447                        "by": "set from calculation",
448                        "byUniqueId": null,
449                        "role": "System",
450                        "when": "2023-04-15T16:08:19Z",
451                        "value": "calculated value"
452                      }
453                    },
454                    {
455                      "entryId": "2",
456                      "value": {
457                        "by": "set from calculation",
458                        "byUniqueId": null,
459                        "role": "System",
460                        "when": "2023-04-15T16:07:24Z",
461                        "value": "Some Site"
462                      },
463                      "reason": {
464                        "by": "set from calculation",
465                        "byUniqueId": null,
466                        "role": "System",
467                        "when": "2023-04-15T16:07:24Z",
468                        "value": "calculated value"
469                      }
470                    }
471                  ]
472                }
473              ]
474            },
475            {
476              "name": "Enrollment",
477              "categoryType": "normal",
478              "highestIndex": 0,
479              "field": [
480                {
481                  "name": "enrollment_closed_date",
482                  "fieldType": "popUpCalendar",
483                  "dataType": "date",
484                  "errorCode": "valid",
485                  "whenCreated": "2023-04-15T16:07:14Z",
486                  "keepHistory": true,
487                  "entry": null
488                },
489                {
490                  "name": "enrollment_open",
491                  "fieldType": "radio",
492                  "dataType": "string",
493                  "errorCode": "valid",
494                  "whenCreated": "2023-04-15T16:07:14Z",
495                  "keepHistory": true,
496                  "entry": [
497                    {
498                      "entryId": "1",
499                      "value": {
500                        "by": "Paul Sanders",
501                        "byUniqueId": "1681162687395",
502                        "role": "Project Manager",
503                        "when": "2023-04-15T16:08:19Z",
504                        "value": "Yes"
505                      },
506                      "reason": null
507                    }
508                  ]
509                },
510                {
511                  "name": "enrollment_open_date",
512                  "fieldType": "popUpCalendar",
513                  "dataType": "date",
514                  "errorCode": "valid",
515                  "whenCreated": "2023-04-15T16:07:14Z",
516                  "keepHistory": true,
517                  "entry": null
518                }
519              ]
520            }
521          ]
522        }
523      ]
524    },
525    {
526      "name": "Artemis",
527      "uniqueId": "1691420994591",
528      "numberOfPatients": 0,
529      "countOfRandomizedPatients": 0,
530      "whenCreated": "2023-08-07T15:14:23Z",
531      "creator": "Paul Sanders",
532      "numberOfForms": 1,
533      "forms": [
534        {
535          "name": "demographic.form.name.site.demographics",
536          "lastModified": "2023-08-07T15:14:23Z",
537          "whoLastModifiedName": "Paul Sanders",
538          "whoLastModifiedRole": "Project Manager",
539          "whenCreated": 1691420994611,
540          "hasErrors": false,
541          "hasWarnings": false,
542          "locked": false,
543          "user": null,
544          "dateTimeChanged": null,
545          "formTitle": "Site Demographics",
546          "formIndex": 1,
547          "formGroup": "Demographic",
548          "formState": "In-Work",
549          "states": [
550            {
551              "value": "form.state.in.work",
552              "signer": "Paul Sanders - Project Manager",
553              "signerUniqueId": "1681162687395",
554              "dateSigned": "2023-08-07T15:14:23Z"
555            }
556          ],
557          "categories": [
558            {
559              "name": "Demographics",
560              "categoryType": "normal",
561              "highestIndex": 0,
562              "fields": [
563                {
564                  "name": "address",
565                  "fieldType": "text",
566                  "dataType": "string",
567                  "errorCode": "valid",
568                  "whenCreated": "2023-08-07T15:09:54Z",
569                  "keepHistory": true,
570                  "entries": [
571                    {
572                      "entryId": "1",
573                      "value": {
574                        "by": "Paul Sanders",
575                        "byUniqueId": "1681162687395",
576                        "role": "Project Manager",
577                        "when": "2023-08-07T15:14:21Z",
578                        "value": "1111 Moon Drive"
579                      },
580                      "reason": null
581                    }
582                  ]
583                }
584              ]
585            }
586          ]
587        }
588      ]
589    }
590  ]
591}
592        "#;
593
594        let result: SiteNative = serde_json::from_str(json_str).unwrap();
595
596        assert_yaml_snapshot!(result);
597    }
598}