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