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