prelude_xml_parser/native/
site_native.rs

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